packages feed

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

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