packages feed

hanalyze-0.2.0.0: src/Hanalyze/Design/DSD.hs

{-# 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