hanalyze-0.2.0.0: src/Hanalyze/Model/Reliability.hs
{-# 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
}