Skip to main content

sokuto_core/
math.rs

1//! # 決定プリミティブ数理モジュール
2//!
3//! 確率較正、ソフトマックス、エントロピー確信度、およびスコア期待値の計算ユーティリティを提供する。
4
5use crate::error::{CoreError, Result};
6
7/// ロジット列から温度付きソフトマックス確率分布を計算し、指定されたバッファへ書き込む。
8///
9/// # 概要
10/// - 推論ランタイム等の高スループット処理向けに、ヒープ割り当てを行わずに確率分布を算出する。
11/// - 数値安定性を担保するため、最大値減算(Max-subtraction)および温度除算を適用する。
12/// - 算出された確率値は浮動小数点演算の累積丸め誤差を考慮し、`[0.0, 1.0]` の閉区間にクランプされる。
13///
14/// ```mermaid
15/// flowchart TD
16///     Start["入力: logits, out, temperature"] --> Val{"バリデーション<br>(空配列 / 長さ不一致 / 非有限値 / tau <= 0)"}
17///     Val -- 不正 --> Err["CoreError::MathError 返却"]
18///     Val -- 正常 --> KCheck{"候補数 K == 1 ?"}
19///     KCheck -- Yes --> Single["out[0] = 1.0 (ショートサーキット)"]
20///     KCheck -- No --> MaxFind["Pass 1: 最大値 z_max 探索<br>(同時 NaN/Inf 検出)"]
21///     MaxFind --> ExpSum["Pass 2: 指数計算 & 累積和<br>exp((z_i - z_max) / tau_eff)"]
22///     ExpSum --> Norm["Pass 3: 分母除算正規化 & clamp(0.0, 1.0)"]
23///     Single --> Done["Ok(()) 返却"]
24///     Norm --> Done
25/// ```
26///
27/// # 数理仕様
28/// $$p_i = \frac{\exp\left(\frac{z_i - z_{\max}}{\tau_{\text{eff}}}\right)}{\sum_{j=1}^K \exp\left(\frac{z_j - z_{\max}}{\tau_{\text{eff}}}\right)}, \quad z_{\max} = \max_{1 \le j \le K} z_j$$
29/// ここで、有効温度 $\tau_{\text{eff}} = \max(\tau, 10^{-4})$ である。
30///
31/// # 引数
32/// - `logits`: 未正規化の生ロジットスライス(`f64`)。
33/// - `out`: 算出された確率値を書き込む出力先バッファスライス(`f64`)。`logits` と同一の要素数が必要。
34/// - `temperature`: 較正温度パラメータ $\tau$。正の有限実数。
35///
36/// # 戻り値
37/// - 成功時は `Ok(())`、入力異常時は `CoreError::MathError` を返却する。
38#[aquamarine::aquamarine]
39pub fn softmax_into(logits: &[f64], out: &mut [f64], temperature: f64) -> Result<()> {
40    if logits.is_empty() {
41        return Err(CoreError::MathError {
42            message: "ロジット配列が空です。".to_string(),
43        });
44    }
45
46    if logits.len() != out.len() {
47        return Err(CoreError::MathError {
48            message: format!(
49                "出力バッファ長({out_len})がロジット長({logits_len})と一致しません。",
50                out_len = out.len(),
51                logits_len = logits.len()
52            ),
53        });
54    }
55
56    if !temperature.is_finite() || temperature <= 0.0 {
57        return Err(CoreError::MathError {
58            message: format!("温度パラメータは正の有限実数である必要があります: {temperature}。"),
59        });
60    }
61
62    // 単一候補(K=1)のエッジケースは指数計算をスキップして即座に 1.0 を設定する。
63    if logits.len() == 1 {
64        if !logits[0].is_finite() {
65            return Err(CoreError::MathError {
66                message: "ロジットに非有限値(NaNまたは無限大)が含まれています。".to_string(),
67            });
68        }
69        out[0] = 1.0;
70        return Ok(());
71    }
72
73    // Python 側の較正評価器(evaluator.py)と平仄を合わせ、極小温度によるオーバーフローを防ぐ安全下限クランプ。
74    let effective_tau = temperature.max(1e-4);
75
76    // Pass 1: 最大値探索および非有限値(NaN, Inf)の厳密な検出。
77    // IEEE 754 の f64::max による NaN すり抜けを防ぐため、明示的な走査を行う。
78    let mut max_logit = f64::NEG_INFINITY;
79    for &logit in logits {
80        if !logit.is_finite() {
81            return Err(CoreError::MathError {
82                message: "ロジットに非有限値(NaNまたは無限大)が含まれています。".to_string(),
83            });
84        }
85        if logit > max_logit {
86            max_logit = logit;
87        }
88    }
89
90    // Pass 2: 最大値減算後の温度スケーリング、指数計算、および総和累積。
91    // (logit - max_logit) <= 0.0 であるため、指数関数の引数は常に (-inf, 0.0] となりオーバーフローしない。
92    // かつ最大値要素自身は exp(0.0) = 1.0 となるため、sum_exp >= 1.0 が数学的に保証される。
93    let mut sum_exp = 0.0;
94    for (i, &logit) in logits.iter().enumerate() {
95        let scaled = (logit - max_logit) / effective_tau;
96        let exp_val = scaled.exp();
97        out[i] = exp_val;
98        sum_exp += exp_val;
99    }
100
101    if sum_exp <= 0.0 || !sum_exp.is_finite() {
102        return Err(CoreError::MathError {
103            message: "ソフトマックスの分母が不正な値になりました。".to_string(),
104        });
105    }
106
107    // Pass 3: 分母除算による正規化と丸め誤差対策のクランプ。
108    for val in out.iter_mut() {
109        *val = (*val / sum_exp).clamp(0.0, 1.0);
110    }
111
112    Ok(())
113}
114
115/// ロジット列から温度付きソフトマックス確率分布を計算し、新しいベクタとして返却する。
116///
117/// # 概要
118/// - 内部でゼロアロケーション版関数 [`softmax_into`] を呼び出す。
119/// - 返却される確率ベクタの総和はほぼ 1.0 となり、各要素は `[0.0, 1.0]` に収まる。
120///
121/// # 引数
122/// - `logits`: 未正規化の生ロジットスライス(`f64`)。
123/// - `temperature`: 較正温度パラメータ $\tau$。正の有限実数。
124///
125/// # 戻り値
126/// - 成功時は計算された確率分布ベクタ(`Vec<f64>`)、異常時は `CoreError::MathError`。
127pub fn softmax(logits: &[f64], temperature: f64) -> Result<Vec<f64>> {
128    let mut probs: Vec<f64> = vec![0.0; logits.len()];
129    softmax_into(logits, &mut probs, temperature)?;
130    Ok(probs)
131}
132
133/// 確率分布から候補数 $K$ に非依存な正規化シャノンエントロピー $H_{\text{norm}}(p) \in [0.0, 1.0]$ を算出する。
134///
135/// # 数理仕様
136/// $$H_{\text{norm}}(p) = \begin{cases} 0.0 & (K = 1) \\ \frac{-\sum_{k=1}^K p_k \ln p_k}{\ln K} & (K \ge 2) \end{cases}$$
137///
138/// - $p_k \le 0.0$ の要素は情報量 $0 \ln 0 = 0$ として対数計算をスキップする。
139/// - $K=1$ の特異点ではゼロ除算を回避し、決定的な $0.0$(不確実性なし)を即座に返却する。
140/// - 戻り値は浮動小数点誤差を考慮して `[0.0, 1.0]` の閉区間にクランプされる。
141///
142/// # 引数
143/// - `probabilities`: 各候補の確率分布スライス(`f64`)。
144///
145/// # 戻り値
146/// - 成功時は `[0.0, 1.0]` にクランプされた正規化エントロピー、不正時は `CoreError::MathError`。
147pub fn normalized_entropy(probabilities: &[f64]) -> Result<f64> {
148    let k = probabilities.len();
149    if k == 0 {
150        return Err(CoreError::MathError {
151            message: "確率分布配列が空です。".to_string(),
152        });
153    }
154    if k == 1 {
155        if !probabilities[0].is_finite() {
156            return Err(CoreError::MathError {
157                message: "確率値に非有限値(NaNまたは無限大)が含まれています。".to_string(),
158            });
159        }
160        if probabilities[0] < 0.0 {
161            return Err(CoreError::MathError {
162                message: format!("負の確率値が含まれています: {}。", probabilities[0]),
163            });
164        }
165        return Ok(0.0);
166    }
167
168    let mut entropy = 0.0;
169    for &p in probabilities {
170        if !p.is_finite() {
171            return Err(CoreError::MathError {
172                message: "確率値に非有限値(NaNまたは無限大)が含まれています。".to_string(),
173            });
174        }
175        if p < 0.0 {
176            return Err(CoreError::MathError {
177                message: format!("負の確率値が含まれています: {p}。"),
178            });
179        }
180        if p > 0.0 {
181            entropy -= p * p.ln();
182        }
183    }
184
185    let max_entropy = (k as f64).ln();
186    if max_entropy <= 0.0 {
187        return Ok(0.0);
188    }
189
190    let h_norm = entropy / max_entropy;
191    Ok(h_norm.clamp(0.0, 1.0))
192}
193
194/// 確率分布から上位2候補の確率差(Top-Margin) $M(p) \in [0.0, 1.0]$ を算出する。
195///
196/// # 数理仕様
197/// $$M(p) = \begin{cases} 1.0 & (K = 1) \\ p_{(1)} - p_{(2)} & (K \ge 2) \end{cases}$$
198///
199/// - $O(K)$ 単一パス走査により、ヒープメモリ割り当て(アロケーション)ゼロで計算する。
200/// - $K=1$ の特異点では $1.0$(他候補なし・完全なマージン)を即座に返却する。
201/// - 同率1位(タイ)の場合は $M(p) = 0.0$ となる。
202/// - 戻り値は `[0.0, 1.0]` の閉区間にクランプされる。
203///
204/// # 引数
205/// - `probabilities`: 各候補の確率分布スライス(`f64`)。
206///
207/// # 戻り値
208/// - 成功時は `[0.0, 1.0]` にクランプされた Top-Margin、不正時は `CoreError::MathError`。
209pub fn top_margin(probabilities: &[f64]) -> Result<f64> {
210    let k = probabilities.len();
211    if k == 0 {
212        return Err(CoreError::MathError {
213            message: "確率分布配列が空です。".to_string(),
214        });
215    }
216    if k == 1 {
217        if !probabilities[0].is_finite() {
218            return Err(CoreError::MathError {
219                message: "確率値に非有限値(NaNまたは無限大)が含まれています。".to_string(),
220            });
221        }
222        if probabilities[0] < 0.0 {
223            return Err(CoreError::MathError {
224                message: format!("負の確率値が含まれています: {}。", probabilities[0]),
225            });
226        }
227        return Ok(1.0);
228    }
229
230    let mut max1 = f64::NEG_INFINITY;
231    let mut max2 = f64::NEG_INFINITY;
232
233    for &p in probabilities {
234        if !p.is_finite() {
235            return Err(CoreError::MathError {
236                message: "確率値に非有限値(NaNまたは無限大)が含まれています。".to_string(),
237            });
238        }
239        if p < 0.0 {
240            return Err(CoreError::MathError {
241                message: format!("負の確率値が含まれています: {p}。"),
242            });
243        }
244
245        if p > max1 {
246            max2 = max1;
247            max1 = p;
248        } else if p > max2 {
249            max2 = p;
250        }
251    }
252
253    let margin = max1 - max2;
254    Ok(margin.clamp(0.0, 1.0))
255}
256
257/// 確率分布から、正規化エントロピーと Top-Margin を統合した複合確信度スコア $S_{\text{confidence}} \in [0.0, 1.0]$ を算出する。
258///
259/// # 数理仕様
260/// $$S_{\text{confidence}} = (1.0 - H_{\text{norm}}(p)) \times M(p)$$
261///
262/// - 分布全体が鋭利であり(平坦度 $H_{\text{norm}}$ が小)、かつ第1位と第2位の差分(マージン $M(p)$)が明瞭な場合にのみ
263///   高い確信度を出力する。
264/// - $K=1$ の特異点では $1.0$ を返却する。
265/// - 同率タイブレーク時はマージンが $0.0$ となるため、複合確信度も厳密に $0.0$ に収束する。
266/// - 戻り値は `[0.0, 1.0]` の閉区間にクランプされる。
267///
268/// # 引数
269/// - `probabilities`: 各候補の確率分布スライス(`f64`)。
270///
271/// # 戻り値
272/// - 成功時は `[0.0, 1.0]` にクランプされた複合確信度スコア、不正時は `CoreError::MathError`。
273pub fn composite_confidence(probabilities: &[f64]) -> Result<f64> {
274    let k = probabilities.len();
275    if k == 0 {
276        return Err(CoreError::MathError {
277            message: "確率分布配列が空です。".to_string(),
278        });
279    }
280    if k == 1 {
281        if !probabilities[0].is_finite() {
282            return Err(CoreError::MathError {
283                message: "確率値に非有限値(NaNまたは無限大)が含まれています。".to_string(),
284            });
285        }
286        if probabilities[0] < 0.0 {
287            return Err(CoreError::MathError {
288                message: format!("負の確率値が含まれています: {}。", probabilities[0]),
289            });
290        }
291        return Ok(1.0);
292    }
293
294    let h_norm = normalized_entropy(probabilities)?;
295    let margin = top_margin(probabilities)?;
296
297    let score = (1.0 - h_norm) * margin;
298    Ok(score.clamp(0.0, 1.0))
299}
300
301/// シャノンエントロピーに基づき、0.0〜1.0 の正規化確信度(Confidence)を算出する。
302///
303/// 候補数 $K=1$ の場合は完全に確信しているため 1.0 を返す。
304/// 一様分布のとき 0.0、単一の候補に確率が集中しているとき 1.0 となる。
305///
306/// # 備考
307/// 本関数は後方互換性のために維持されており、$1.0 - H_{\text{norm}}(p)$ を返却する。
308pub fn normalized_entropy_confidence(probabilities: &[f64]) -> Result<f64> {
309    let h_norm = normalized_entropy(probabilities)?;
310    Ok((1.0 - h_norm).clamp(0.0, 1.0))
311}
312
313/// スライス内から最大値を持つ要素のインデックスを取得する。
314///
315/// # 概要
316/// - 決定論的タイブレーク規則として、同一の最大値が複数存在する場合は「最小のインデックス(先勝ち)」を確定的に採択する。
317/// - 配列内に `NaN` が含まれている場合は `CoreError::MathError` を返却する。
318///
319/// # 引数
320/// - `values`: 探索対象の実数値スライス(`f64`)。
321///
322/// # 戻り値
323/// - 成功時は最大値のインデックス(`usize`)、配列が空または非有限値を含む場合は `CoreError::MathError`。
324pub fn argmax(values: &[f64]) -> Result<usize> {
325    if values.is_empty() {
326        return Err(CoreError::MathError {
327            message: "探索対象のスライスが空です。".to_string(),
328        });
329    }
330
331    let mut best_index = 0;
332    let mut max_val = f64::NEG_INFINITY;
333
334    for (i, &val) in values.iter().enumerate() {
335        if !val.is_finite() {
336            return Err(CoreError::MathError {
337                message: "スライス内に非有限値(NaNまたは無限大)が含まれています。".to_string(),
338            });
339        }
340        // 厳密な不等号(val > max_val)を用いることで、同率値の場合は先頭インデックスを維持する。
341        if val > max_val {
342            max_val = val;
343            best_index = i;
344        }
345    }
346
347    Ok(best_index)
348}
349
350/// Score 型の確率分布から加重平均スコア実数値を算出する。
351///
352/// 各段階レベル $k \in \{0, \dots, M-1\}$ に対して $\text{Score} = \sum k \cdot p_k$ を計算する。
353pub fn expected_score(probabilities: &[f64]) -> Result<f64> {
354    if probabilities.len() < 2 {
355        return Err(CoreError::MathError {
356            message: "Score の確率分布は 2 段階以上必要です。".to_string(),
357        });
358    }
359
360    let mut score = 0.0;
361    for (k, &p) in probabilities.iter().enumerate() {
362        if p < 0.0 {
363            return Err(CoreError::MathError {
364                message: format!("負の確率値が含まれています: {p}。"),
365            });
366        }
367        score += (k as f64) * p;
368    }
369
370    Ok(score)
371}
372
373/// Score 型の確率分布からスコアの事後分散 $\text{Var}\[S\]$ を算出する。
374///
375/// 段階レベル $k \in \{0, \dots, M-1\}$ に対し、加重平均期待値 $\hat{s} = E\[S\]$ を用いて
376/// $\text{Var}\[S\] = \sum_{k=0}^{M-1} (k - \hat{s})^2 p_k$ を計算する。
377pub fn score_variance(probabilities: &[f64]) -> Result<f64> {
378    let mean = expected_score(probabilities)?;
379
380    let mut variance = 0.0;
381    for (k, &p) in probabilities.iter().enumerate() {
382        let diff = (k as f64) - mean;
383        variance += diff * diff * p;
384    }
385
386    // 浮動小数点誤差による負数の発生を防止する。
387    Ok(variance.max(0.0))
388}
389
390/// Score 型の確率分布から、最大可能分散で正規化した確信度 $C_{\text{var}} \in [0.0, 1.0]$ を算出する。
391///
392/// # 概要
393/// - 順序尺度(Score)においてカテゴリカルエントロピーを用いると、両極端への分裂(高分散・バイモーダル)と
394///   隣接段階への分裂(低分散)を区別できず、誤った過信を招く問題がある。
395/// - 本関数では事後分散 $\text{Var}\[S\]$ を計算し、$M$ 段階における理論上の最大分散
396///   $V_{\max} = \frac{(M-1)^2}{4}$(両極端に 0.5 ずつ配分された二峰性分布)で正規化する。
397///
398/// # 数理仕様
399/// $$C_{\text{var}} = 1.0 - \frac{\text{Var}\[S\]}{V_{\max}} = 1.0 - \frac{4 \cdot \text{Var}\[S\]}{(M-1)^2}$$
400///
401/// # 引数
402/// - `probabilities`: 各段階レベルの確率分布スライス($M \ge 2$)。
403///
404/// # 戻り値
405/// - 成功時は `[0.0, 1.0]` の閉区間にクランプされた正規化確信度、不正時は `CoreError::MathError`。
406pub fn normalized_variance_confidence(probabilities: &[f64]) -> Result<f64> {
407    let m = probabilities.len();
408    if m < 2 {
409        return Err(CoreError::MathError {
410            message: "Score の確信度計算には 2 段階以上の確率分布が必要です。".to_string(),
411        });
412    }
413
414    let var = score_variance(probabilities)?;
415    let max_var = ((m - 1) as f64).powi(2) / 4.0;
416
417    if max_var <= 0.0 {
418        return Ok(1.0);
419    }
420
421    let confidence = 1.0 - (var / max_var);
422    Ok(confidence.clamp(0.0, 1.0))
423}
424
425/// Score 型の確率分布から、理論最大分散に対する事後分散の比率 $\text{VarRatio} \in [0.0, 1.0]$ を算出する。
426///
427/// # 概要
428/// - $M$ 段階における理論最大分散 $V_{\max} = \frac{(M-1)^2}{4}$ に対する比率を計算する。
429/// - 両端($0$ と $M-1$)に $0.5$ ずつ配分された完全二峰性分布では $1.0$ となる。
430///
431/// # 引数
432/// - `probabilities`: 各段階レベルの確率分布スライス ($M \ge 2$)。
433///
434/// # 戻り値
435/// - 成功時は `[0.0, 1.0]` にクランプされた分散比率、不正時は `CoreError::MathError`。
436pub fn score_variance_ratio(probabilities: &[f64]) -> Result<f64> {
437    let m = probabilities.len();
438    if m < 2 {
439        return Err(CoreError::MathError {
440            message: "Score の分散比率計算には 2 段階以上の確率分布が必要です。".to_string(),
441        });
442    }
443
444    let var = score_variance(probabilities)?;
445    let max_var = ((m - 1) as f64).powi(2) / 4.0;
446
447    if max_var <= 0.0 {
448        return Ok(0.0);
449    }
450
451    let ratio = var / max_var;
452    Ok(ratio.clamp(0.0, 1.0))
453}
454
455/// Score 型の確率分布から双峰性係数 (Bimodality Coefficient: $BC$) を算出する。
456///
457/// # 概要
458/// - 離散確率分布における母集団中心モーメント(2次、3次、4次)を直接算出し、
459///   標本補正項による $M \le 3$ でのゼロ除算クラッシュを完全に排除した閉じた数理計算を行う。
460/// - 単峰性分布では $BC < 0.555$ となり、完全一様分布では $BC \approx 0.555$、
461///   両端対立(二極化)分布では $BC \to 1.0$ へ漸近する。
462/// - 極小分散 ($\sigma^2 < 10^{-6}$) の場合は、完全単峰・高確信度と判定してゼロ除算を回避し、
463///   即座に $0.0$ を返却する。
464///
465/// # 数理仕様
466/// $$BC = \frac{\gamma^2 + 1}{\kappa}$$
467/// ここで、歪度 $\gamma = \frac{\mu_3}{\sigma^3}$、尖度 $\kappa = \frac{\mu_4}{\sigma^4}$、
468/// $\mu_r = \sum_{k=0}^{M-1} (k - \mu)^r p_k$ である。
469///
470/// # 引数
471/// - `probabilities`: 各段階レベルの確率分布スライス ($M \ge 2$)。
472///
473/// # 戻り値
474/// - 成功時は `[0.0, 1.0]` の閉区間にクランプされた双峰性係数、不正時は `CoreError::MathError`。
475pub fn bimodality_coefficient(probabilities: &[f64]) -> Result<f64> {
476    let m = probabilities.len();
477    if m < 2 {
478        return Err(CoreError::MathError {
479            message: "双峰性係数の計算には 2 段階以上の確率分布が必要です。".to_string(),
480        });
481    }
482
483    let mean = expected_score(probabilities)?;
484
485    // 2次中心モーメント (分散 sigma^2)
486    let mut variance = 0.0;
487    // 3次中心モーメント mu_3
488    let mut mu3 = 0.0;
489    // 4次中心モーメント mu_4
490    let mut mu4 = 0.0;
491
492    for (k, &p) in probabilities.iter().enumerate() {
493        if p < 0.0 {
494            return Err(CoreError::MathError {
495                message: format!("負の確率値が含まれています: {p}。"),
496            });
497        }
498        let diff = (k as f64) - mean;
499        let diff2 = diff * diff;
500        variance += diff2 * p;
501        mu3 += diff2 * diff * p;
502        mu4 += diff2 * diff2 * p;
503    }
504
505    // 極小分散 (ワンホット等) のショートサーキット判定
506    if variance < 1e-6 {
507        return Ok(0.0);
508    }
509
510    let sigma = variance.sqrt();
511    let sigma3 = variance * sigma;
512    let sigma4 = variance * variance;
513
514    if sigma4 <= 0.0 {
515        return Ok(0.0);
516    }
517
518    let gamma = mu3 / sigma3;
519    let kurtosis = mu4 / sigma4;
520
521    if kurtosis <= 0.0 {
522        return Ok(0.0);
523    }
524
525    let bc = (gamma * gamma + 1.0) / kurtosis;
526    Ok(bc.clamp(0.0, 1.0))
527}
528
529/// Noul 型の言明真実確率値 $P(\text{true}) \in [0.0, 1.0]$ を算出する。
530///
531/// # 概要
532/// - ロジット差分 $x = (z_{\text{true}} - z_{\text{false}}) / \tau_{\text{eff}}$ に対し、
533///   符号分岐型シグモイド $\sigma(x)$ を用いることで指数オーバーフローおよびアンダーフローを完全に排除する。
534/// - $x \ge 0$ のときは $\frac{1}{1 + \exp(-x)}$、$x < 0$ のときは $\frac{\exp(x)}{1 + \exp(x)}$ を計算する。
535///
536/// # 引数
537/// - `logit_true`: 真(True)候補の生ロジット。
538/// - `logit_false`: 偽(False)候補の生ロジット。
539/// - `temperature`: 較正温度パラメータ $\tau$。正の有限実数。
540///
541/// # 戻り値
542/// - 成功時は `[0.0, 1.0]` にクランプされた真実確率、不正時は `CoreError::MathError`。
543pub fn noul_probability(logit_true: f64, logit_false: f64, temperature: f64) -> Result<f64> {
544    if !temperature.is_finite() || temperature <= 0.0 {
545        return Err(CoreError::MathError {
546            message: format!("温度パラメータは正の実数である必要があります: {temperature}。"),
547        });
548    }
549
550    if !logit_true.is_finite() || !logit_false.is_finite() {
551        return Err(CoreError::MathError {
552            message: "ロジットに非有限値(NaNまたは無限大)が含まれています。".to_string(),
553        });
554    }
555
556    let effective_tau = temperature.max(1e-4);
557    let diff = (logit_true - logit_false) / effective_tau;
558
559    let prob = if diff >= 0.0 {
560        1.0 / (1.0 + (-diff).exp())
561    } else {
562        let exp_diff = diff.exp();
563        exp_diff / (1.0 + exp_diff)
564    };
565
566    Ok(prob.clamp(0.0, 1.0))
567}
568
569/// Noul 型の真実確率から、決定の尖り度に基づく正規化確信度 $C_{\text{noul}} \in [0.0, 1.0]$ を算出する。
570///
571/// # 数理仕様
572/// $$C_{\text{noul}} = |2 \cdot P(\text{true}) - 1.0|$$
573/// - $P(\text{true}) = 0.5$(完全な迷い)のとき 0.0。
574/// - $P(\text{true}) = 1.0$ または $0.0$(完全な確信)のとき 1.0。
575pub fn noul_confidence(probability: f64) -> Result<f64> {
576    if !probability.is_finite() || !(0.0..=1.0).contains(&probability) {
577        return Err(CoreError::MathError {
578            message: format!("確率は [0.0, 1.0] の有限実数である必要があります: {probability}。"),
579        });
580    }
581
582    let conf = (2.0 * probability - 1.0).abs();
583    Ok(conf.clamp(0.0, 1.0))
584}
585
586/// ロジット列からヘルムホルツ自由エネルギー (Free Energy) を算出する。
587///
588/// # 概要
589/// - Softmax 正規化前の全ロジットの絶対的な発火強度を測定し、未定義カテゴリや未知の入力 (Out-of-Distribution: OOD) を検知するための指標である。
590/// - 数値安定性を担保するため、最大値減算 (Max-trick) を適用して指数関数の浮動小数点オーバーフローを物理的に排除する。
591/// - ヒープ割り当てを一切行わないゼロアロケーション走査で計算する。
592///
593/// # 数理仕様
594/// $$E(x; C) = -T \cdot \ln \sum_{i=1}^K \exp\left( \frac{z_i}{T} \right) = -z_{\max} - T_{\text{eff}} \cdot \ln \sum_{i=1}^K \exp\left( \frac{z_i - z_{\max}}{T_{\text{eff}}} \right)$$
595/// ここで、有効温度 $T_{\text{eff}} = \max(T, 10^{-4})$、最大ロジット $z_{\max} = \max_{1 \le i \le K} z_i$ である。
596///
597/// # 引数
598/// - `logits`: 未正規化の生ロジットスライス (`f64`)。
599/// - `temperature`: 較正温度パラメータ $T$。正の有限実数。
600///
601/// # 戻り値
602/// - 成功時は自由エネルギー値 $E(x; C)$、入力不正時は `CoreError::MathError` を返却する。
603pub fn free_energy(logits: &[f64], temperature: f64) -> Result<f64> {
604    if logits.is_empty() {
605        return Err(CoreError::MathError {
606            message: "ロジット配列が空です。".to_string(),
607        });
608    }
609
610    if !temperature.is_finite() || temperature <= 0.0 {
611        return Err(CoreError::MathError {
612            message: format!("温度パラメータは正の有限実数である必要があります: {temperature}。"),
613        });
614    }
615
616    // 単一候補 (K=1) の場合は、-z_0 を直接返却する。
617    if logits.len() == 1 {
618        if !logits[0].is_finite() {
619            return Err(CoreError::MathError {
620                message: "ロジットに非有限値(NaNまたは無限大)が含まれています。".to_string(),
621            });
622        }
623        return Ok(-logits[0]);
624    }
625
626    let effective_tau = temperature.max(1e-4);
627
628    // Pass 1: 最大値探索および非有限値の検出。
629    let mut max_logit = f64::NEG_INFINITY;
630    for &logit in logits {
631        if !logit.is_finite() {
632            return Err(CoreError::MathError {
633                message: "ロジットに非有限値(NaNまたは無限大)が含まれています。".to_string(),
634            });
635        }
636        if logit > max_logit {
637            max_logit = logit;
638        }
639    }
640
641    // Pass 2: 最大値減算後の指数和累積 (Max-trick)。
642    // (logit - max_logit) <= 0.0 であるためオーバーフローしない。
643    // かつ最大要素は exp(0.0) = 1.0 であるため、sum_exp >= 1.0 が保証され ln(sum_exp) >= 0.0 となる。
644    let mut sum_exp = 0.0;
645    for &logit in logits {
646        let scaled = (logit - max_logit) / effective_tau;
647        sum_exp += scaled.exp();
648    }
649
650    let energy = -max_logit - effective_tau * sum_exp.ln();
651    Ok(energy)
652}
653
654/// 候補数 $K$ に対する基底エネルギー偏向を補正した、正規化ヘルムホルツ自由エネルギー (Normalized Free Energy) を算出する。
655///
656/// # 概要
657/// - 単純な自由エネルギー $E(x; C) = -T \ln \sum \exp(z_i/T)$ は、候補数 $K$ が増加するだけで理論的に自然低下 (約 $-T \ln K$) する。
658/// - sokuto では候補数 $K \in [1, 255]$ が動的に変動するため、候補数で補正した正規化自由エネルギーを採用することで、
659///   任意の候補数に対して単一の閾値 $\tau_{\text{energy}}$ で安定した境界判定を可能にする。
660/// - すべてのロジットが同一の定数 $c$ (完全な一様分布) であるとき、本関数の出力は厳密に $-c$ (ロジットが0のときは0.0) となる。
661///
662/// # 数理仕様
663/// $$E_{\text{norm}}(x; C) = E(x; C) + T \ln K = -T \cdot \ln\left( \frac{1}{K} \sum_{i=1}^K \exp\left(\frac{z_i}{T}\right) \right) = -z_{\max} - T_{\text{eff}} \cdot \ln\left( \frac{1}{K} \sum_{i=1}^K \exp\left(\frac{z_i - z_{\max}}{T_{\text{eff}}}\right) \right)$$
664///
665/// # 引数
666/// - `logits`: 未正規化の生ロジットスライス (`f64`)。
667/// - `temperature`: 較正温度パラメータ $T$。正の有限実数。
668///
669/// # 戻り値
670/// - 成功時は正規化自由エネルギー値 $E_{\text{norm}}(x; C)$、入力不正時は `CoreError::MathError` を返却する。
671pub fn normalized_free_energy(logits: &[f64], temperature: f64) -> Result<f64> {
672    let k = logits.len();
673    let raw_energy = free_energy(logits, temperature)?;
674    if k <= 1 {
675        return Ok(raw_energy);
676    }
677
678    let effective_tau = temperature.max(1e-4);
679    let shift = effective_tau * (k as f64).ln();
680    Ok(raw_energy + shift)
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686
687    #[test]
688    fn test_softmax_basic() {
689        let logits = vec![2.0, 1.0, 0.1];
690        let probs = softmax(&logits, 1.0).unwrap();
691        assert_eq!(probs.len(), 3);
692        let sum: f64 = probs.iter().sum();
693        assert!((sum - 1.0).abs() < 1e-6);
694        assert!(probs[0] > probs[1]);
695        assert!(probs[1] > probs[2]);
696    }
697
698    #[test]
699    fn test_softmax_into_basic() {
700        let logits = vec![2.0, 1.0, 0.1];
701        let mut probs = vec![0.0; 3];
702        softmax_into(&logits, &mut probs, 1.0).unwrap();
703        let sum: f64 = probs.iter().sum();
704        assert!((sum - 1.0).abs() < 1e-6);
705        assert!(probs[0] > probs[1]);
706        assert!(probs[1] > probs[2]);
707    }
708
709    #[test]
710    fn test_softmax_extreme_logits() {
711        // 巨大なロジット差でもオーバーフロー・アンダーフローでクラッシュしないことを検証する。
712        let logits = vec![10000.0, 0.0, -10000.0];
713        let probs = softmax(&logits, 1.0).unwrap();
714        assert_eq!(probs.len(), 3);
715        assert!((probs[0] - 1.0).abs() < 1e-6);
716        assert_eq!(probs[1], 0.0);
717        assert_eq!(probs[2], 0.0);
718        let sum: f64 = probs.iter().sum();
719        assert!((sum - 1.0).abs() < 1e-6);
720    }
721
722    #[test]
723    fn test_softmax_identical_logits() {
724        // 全要素が同一ロジットの場合、厳密に一様分布となることを検証する。
725        let logits = vec![5.0, 5.0, 5.0, 5.0];
726        let probs = softmax(&logits, 1.0).unwrap();
727        for &p in &probs {
728            assert!((p - 0.25).abs() < 1e-6);
729        }
730        let sum: f64 = probs.iter().sum();
731        assert!((sum - 1.0).abs() < 1e-6);
732    }
733
734    #[test]
735    fn test_softmax_single_candidate() {
736        // K=1 のエッジケースにおいて、即座に 1.0 が返却されることを検証する。
737        let logits = vec![std::f64::consts::PI];
738        let probs = softmax(&logits, 0.5).unwrap();
739        assert_eq!(probs.len(), 1);
740        assert_eq!(probs[0], 1.0);
741
742        let mut out = vec![0.0];
743        softmax_into(&logits, &mut out, 2.0).unwrap();
744        assert_eq!(out[0], 1.0);
745    }
746
747    #[test]
748    fn test_softmax_temperature_limits() {
749        let logits = vec![2.0, 1.0, 0.0];
750
751        // 高温 (tau -> inf): 一様分布へ平滑化される。
752        let probs_high = softmax(&logits, 1000.0).unwrap();
753        for &p in &probs_high {
754            assert!((p - 1.0 / 3.0).abs() < 1e-2);
755        }
756
757        // 低温 (tau -> 0): 最大値へ確率が 1.0 集中する (One-hot)。
758        let probs_low = softmax(&logits, 1e-4).unwrap();
759        assert!((probs_low[0] - 1.0).abs() < 1e-6);
760        assert!(probs_low[1] < 1e-6);
761        assert!(probs_low[2] < 1e-6);
762
763        // 安全下限クランプ (tau = 1e-9): ゼロ除算・オーバーフローなく計算できる。
764        let probs_micro = softmax(&logits, 1e-9).unwrap();
765        assert!((probs_micro[0] - 1.0).abs() < 1e-6);
766    }
767
768    #[test]
769    fn test_softmax_errors() {
770        // 空ロジット
771        let empty_logits: Vec<f64> = vec![];
772        assert!(softmax(&empty_logits, 1.0).is_err());
773
774        // 出力バッファ長不一致
775        let logits = vec![1.0, 2.0];
776        let mut short_out = vec![0.0];
777        assert!(softmax_into(&logits, &mut short_out, 1.0).is_err());
778
779        // 不正な温度 (tau <= 0, NaN, Inf)
780        assert!(softmax(&logits, 0.0).is_err());
781        assert!(softmax(&logits, -1.0).is_err());
782        assert!(softmax(&logits, f64::NAN).is_err());
783        assert!(softmax(&logits, f64::INFINITY).is_err());
784
785        // ロジットに NaN または Inf
786        let nan_logits = vec![1.0, f64::NAN, 3.0];
787        assert!(softmax(&nan_logits, 1.0).is_err());
788        let inf_logits = vec![1.0, f64::INFINITY, 3.0];
789        assert!(softmax(&inf_logits, 1.0).is_err());
790        let neg_inf_logits = vec![f64::NEG_INFINITY, 1.0];
791        assert!(softmax(&neg_inf_logits, 1.0).is_err());
792    }
793
794    #[test]
795    fn test_normalized_entropy_confidence() {
796        // 完全な一様分布 -> 確信度は 0.0
797        let uniform = vec![0.25, 0.25, 0.25, 0.25];
798        let conf_uniform = normalized_entropy_confidence(&uniform).unwrap();
799        assert!(conf_uniform < 1e-6);
800
801        // 完全にピークした分布 -> 確信度は 1.0
802        let peaked = vec![1.0, 0.0, 0.0, 0.0];
803        let conf_peaked = normalized_entropy_confidence(&peaked).unwrap();
804        assert!((conf_peaked - 1.0).abs() < 1e-6);
805    }
806
807    #[test]
808    fn test_expected_score() {
809        let probs = vec![0.1, 0.2, 0.7]; // k=0, 1, 2
810        let score = expected_score(&probs).unwrap();
811        let expected = 0.0 * 0.1 + 1.0 * 0.2 + 2.0 * 0.7;
812        assert!((score - expected).abs() < 1e-6);
813    }
814
815    #[test]
816    fn test_noul_probability() {
817        let prob_equal = noul_probability(1.0, 1.0, 1.0).unwrap();
818        assert!((prob_equal - 0.5).abs() < 1e-6);
819
820        let prob_high = noul_probability(10.0, 0.0, 1.0).unwrap();
821        assert!(prob_high > 0.99);
822
823        // 不正な温度
824        assert!(noul_probability(1.0, 0.0, 0.0).is_err());
825        assert!(noul_probability(1.0, 0.0, -1.0).is_err());
826        assert!(noul_probability(1.0, 0.0, f64::NAN).is_err());
827    }
828
829    #[test]
830    fn test_argmax() {
831        // 基本的な最大値探索
832        let values = vec![0.1, 0.8, 0.4];
833        assert_eq!(argmax(&values).unwrap(), 1);
834
835        // 決定論的タイブレーク(同率最大値の場合は先頭インデックスを採択)
836        let tied = vec![0.5, 0.9, 0.9, 0.2];
837        assert_eq!(argmax(&tied).unwrap(), 1);
838
839        // 先頭が最大値の場合
840        let first_max = vec![10.0, 5.0, 2.0];
841        assert_eq!(argmax(&first_max).unwrap(), 0);
842
843        // 末尾が最大値の場合
844        let last_max = vec![1.0, 2.0, 3.0];
845        assert_eq!(argmax(&last_max).unwrap(), 2);
846
847        // 異常系: 空スライス
848        let empty: Vec<f64> = vec![];
849        assert!(argmax(&empty).is_err());
850
851        // 異常系: NaN または Inf
852        let nan_vals = vec![1.0, f64::NAN, 2.0];
853        assert!(argmax(&nan_vals).is_err());
854        let inf_vals = vec![1.0, f64::INFINITY];
855        assert!(argmax(&inf_vals).is_err());
856    }
857
858    #[test]
859    fn test_score_variance_and_confidence() {
860        // 1. ワンホット分布(単一レベルに集中): 分散 0.0、確信度 1.0
861        let one_hot = vec![0.0, 0.0, 1.0, 0.0];
862        let var_one_hot = score_variance(&one_hot).unwrap();
863        let conf_one_hot = normalized_variance_confidence(&one_hot).unwrap();
864        assert!(var_one_hot < 1e-6);
865        assert!((conf_one_hot - 1.0).abs() < 1e-6);
866
867        // 2. 両極端に分裂した二峰性分布 [0.5, 0.0, 0.0, 0.5] (M=4)
868        // 最大可能分散 V_max = (4-1)^2 / 4 = 2.25
869        // Mean = 0 * 0.5 + 3 * 0.5 = 1.5
870        // Var = (0 - 1.5)^2 * 0.5 + (3 - 1.5)^2 * 0.5 = 2.25
871        // 確信度 C_var = 1.0 - 2.25 / 2.25 = 0.0
872        let bimodal = vec![0.5, 0.0, 0.0, 0.5];
873        let var_bimodal = score_variance(&bimodal).unwrap();
874        let conf_bimodal = normalized_variance_confidence(&bimodal).unwrap();
875        assert!((var_bimodal - 2.25).abs() < 1e-6);
876        assert!(conf_bimodal < 1e-6);
877
878        // 3. 隣接段階に分裂した低分散分布 [0.0, 0.5, 0.5, 0.0] (M=4)
879        // Mean = 1 * 0.5 + 2 * 0.5 = 1.5
880        // Var = (1 - 1.5)^2 * 0.5 + (2 - 1.5)^2 * 0.5 = 0.25
881        // 確信度 C_var = 1.0 - 0.25 / 2.25 = 1.0 - 1/9 ≈ 0.888889
882        let adjacent = vec![0.0, 0.5, 0.5, 0.0];
883        let var_adjacent = score_variance(&adjacent).unwrap();
884        let conf_adjacent = normalized_variance_confidence(&adjacent).unwrap();
885        assert!((var_adjacent - 0.25).abs() < 1e-6);
886        assert!((conf_adjacent - (8.0 / 9.0)).abs() < 1e-6);
887
888        // エントロピー確信度との対比検証:
889        // bimodal と adjacent はカテゴリカルエントロピーが完全に同一だが、
890        // 分散確信度では bimodal(0.0) と adjacent(0.889) で明確に区別される。
891        let ent_bimodal = normalized_entropy_confidence(&bimodal).unwrap();
892        let ent_adjacent = normalized_entropy_confidence(&adjacent).unwrap();
893        assert!((ent_bimodal - ent_adjacent).abs() < 1e-6);
894        assert!(conf_adjacent > conf_bimodal);
895
896        // 異常系: 段階数が 2 未満
897        let single_level = vec![1.0];
898        assert!(score_variance(&single_level).is_err());
899        assert!(normalized_variance_confidence(&single_level).is_err());
900    }
901
902    #[test]
903    fn test_noul_stability_and_confidence() {
904        // 極端なロジット差でもオーバーフロー・アンダーフローせず安定計算できることの検証
905        let prob_huge_pos = noul_probability(10000.0, 0.0, 1.0).unwrap();
906        assert!((prob_huge_pos - 1.0).abs() < 1e-6);
907
908        let prob_huge_neg = noul_probability(-10000.0, 0.0, 1.0).unwrap();
909        assert!(prob_huge_neg < 1e-6);
910
911        // 確信度計算の検証
912        // P = 0.5 -> 確信度 0.0
913        assert!((noul_confidence(0.5).unwrap() - 0.0).abs() < 1e-6);
914        // P = 1.0 -> 確信度 1.0
915        assert!((noul_confidence(1.0).unwrap() - 1.0).abs() < 1e-6);
916        // P = 0.0 -> 確信度 1.0
917        assert!((noul_confidence(0.0).unwrap() - 1.0).abs() < 1e-6);
918        // P = 0.8 -> 確信度 0.6
919        assert!((noul_confidence(0.8).unwrap() - 0.6).abs() < 1e-6);
920
921        // 異常系: 範囲外または非有限値
922        assert!(noul_confidence(-0.1).is_err());
923        assert!(noul_confidence(1.1).is_err());
924        assert!(noul_confidence(f64::NAN).is_err());
925    }
926
927    #[test]
928    fn test_softmax_non_finite_positions() {
929        // 先頭、中間、末尾の各位置に非有限値(NaN, +Inf, -Inf)が注入された場合の捕捉を検証する。
930        let test_cases = vec![
931            vec![f64::NAN, 1.0, 2.0],
932            vec![1.0, f64::NAN, 2.0],
933            vec![1.0, 2.0, f64::NAN],
934            vec![f64::INFINITY, 1.0, 2.0],
935            vec![1.0, f64::INFINITY, 2.0],
936            vec![1.0, 2.0, f64::INFINITY],
937            vec![f64::NEG_INFINITY, 1.0, 2.0],
938            vec![1.0, f64::NEG_INFINITY, 2.0],
939            vec![1.0, 2.0, f64::NEG_INFINITY],
940        ];
941
942        for (i, case) in test_cases.iter().enumerate() {
943            let res = softmax(case, 1.0);
944            assert!(
945                res.is_err(),
946                "ケース {i} で非有限値がすり抜けました: {case:?}。"
947            );
948        }
949
950        // K=1 における非有限値の遮断を検証する。
951        assert!(softmax(&[f64::NAN], 1.0).is_err());
952        assert!(softmax(&[f64::INFINITY], 1.0).is_err());
953        assert!(softmax(&[f64::NEG_INFINITY], 1.0).is_err());
954
955        // 全要素が -Inf の場合、指数計算で分母が 0 になるため安全にエラー捕捉されることを検証する。
956        let all_neg_inf = vec![f64::NEG_INFINITY, f64::NEG_INFINITY];
957        assert!(softmax(&all_neg_inf, 1.0).is_err());
958    }
959
960    #[test]
961    fn test_softmax_mask_value_convergence() {
962        // Python 側のパディングマスク値 -1e4 が混入した場合の挙動を検証する。
963        // 有効ロジットに対してマスクされた位置の確率が厳密に 0.0 となり、
964        // かつ有効要素間の確率比率が保たれることを確認する。
965        let logits = vec![3.0, 1.0, -10000.0];
966        let probs = softmax(&logits, 1.0).unwrap();
967        assert_eq!(probs.len(), 3);
968
969        // マスク位置は 0.0 にアンダーフローして安全に収束する。
970        assert_eq!(probs[2], 0.0);
971
972        // 有効要素の和が 1.0 になる。
973        let valid_sum = probs[0] + probs[1];
974        assert!((valid_sum - 1.0).abs() < 1e-6);
975
976        // 3.0 と 1.0 の二値ソフトマックス理論値と一致する。
977        let expected_ratio = (2.0_f64).exp(); // exp(3 - 1)
978        let actual_ratio = probs[0] / probs[1];
979        assert!((actual_ratio - expected_ratio).abs() < 1e-4);
980    }
981
982    #[test]
983    fn test_argmax_non_finite_positions() {
984        // argmax において先頭、中間、末尾に非有限値が含まれる場合にエラーとなることを検証する。
985        let test_cases = vec![
986            vec![f64::NAN, 0.5, 0.2],
987            vec![0.5, f64::NAN, 0.2],
988            vec![0.5, 0.2, f64::NAN],
989            vec![f64::INFINITY, 0.5, 0.2],
990            vec![0.5, f64::INFINITY, 0.2],
991            vec![0.5, 0.2, f64::INFINITY],
992            vec![f64::NEG_INFINITY, 0.5, 0.2],
993            vec![0.5, f64::NEG_INFINITY, 0.2],
994            vec![0.5, 0.2, f64::NEG_INFINITY],
995        ];
996
997        for (i, case) in test_cases.iter().enumerate() {
998            assert!(
999                argmax(case).is_err(),
1000                "argmax ケース {i} で非有限値がすり抜けました: {case:?}。"
1001            );
1002        }
1003    }
1004
1005    #[test]
1006    fn test_noul_non_finite_inputs() {
1007        // noul_probability の真偽ロジットに非有限値が渡された場合の拒絶を検証する。
1008        assert!(noul_probability(f64::NAN, 0.0, 1.0).is_err());
1009        assert!(noul_probability(0.0, f64::NAN, 1.0).is_err());
1010        assert!(noul_probability(f64::INFINITY, 0.0, 1.0).is_err());
1011        assert!(noul_probability(0.0, f64::INFINITY, 1.0).is_err());
1012        assert!(noul_probability(f64::NEG_INFINITY, 0.0, 1.0).is_err());
1013        assert!(noul_probability(0.0, f64::NEG_INFINITY, 1.0).is_err());
1014    }
1015
1016    #[test]
1017    fn test_normalized_entropy() {
1018        // 1. K=1 の特異点: 不確実性ゼロのため 0.0 を返却する。
1019        assert_eq!(normalized_entropy(&[1.0]).unwrap(), 0.0);
1020
1021        // 2. 完全な一様分布: 理論値 H(p) = ln(K) により正規化エントロピーは厳密に 1.0 となる。
1022        let uniform2 = vec![0.5, 0.5];
1023        assert!((normalized_entropy(&uniform2).unwrap() - 1.0).abs() < 1e-6);
1024
1025        let uniform4 = vec![0.25, 0.25, 0.25, 0.25];
1026        assert!((normalized_entropy(&uniform4).unwrap() - 1.0).abs() < 1e-6);
1027
1028        // 3. ワンホット(完全確信)分布: H(p) = 0.0 により正規化エントロピーは 0.0 となる。
1029        let peaked = vec![1.0, 0.0, 0.0, 0.0];
1030        assert!(normalized_entropy(&peaked).unwrap() < 1e-6);
1031
1032        // 4. 一般分布 (K=3)
1033        // p = [0.7, 0.2, 0.1]
1034        // H(p) = -(0.7 * ln 0.7 + 0.2 * ln 0.2 + 0.1 * ln 0.1) ≈ 0.80181855
1035        // H_norm = 0.80181855 / ln(3) ≈ 0.80181855 / 1.09861229 ≈ 0.7298466
1036        let p3 = vec![0.7, 0.2, 0.1];
1037        let h_norm3 = normalized_entropy(&p3).unwrap();
1038        assert!((h_norm3 - 0.7298466).abs() < 1e-5);
1039
1040        // 5. 異常系
1041        assert!(normalized_entropy(&[]).is_err());
1042        assert!(normalized_entropy(&[-0.1, 1.1]).is_err());
1043        assert!(normalized_entropy(&[0.5, f64::NAN]).is_err());
1044        assert!(normalized_entropy(&[f64::INFINITY, 0.5]).is_err());
1045        assert!(normalized_entropy(&[f64::NAN]).is_err());
1046    }
1047
1048    #[test]
1049    fn test_top_margin() {
1050        // 1. K=1 の特異点: 競合候補が存在しないため 1.0 を返却する。
1051        assert_eq!(top_margin(&[1.0]).unwrap(), 1.0);
1052
1053        // 2. 明確な差がある場合
1054        let p_clear = vec![0.7, 0.2, 0.1];
1055        assert!((top_margin(&p_clear).unwrap() - 0.5).abs() < 1e-6);
1056
1057        // 3. 同率1位(タイ)の場合: マージンは 0.0 となる。
1058        let p_tied = vec![0.4, 0.4, 0.2];
1059        assert!(top_margin(&p_tied).unwrap() < 1e-6);
1060
1061        let p_all_equal = vec![0.25, 0.25, 0.25, 0.25];
1062        assert!(top_margin(&p_all_equal).unwrap() < 1e-6);
1063
1064        // 4. ワンホット極限: マージンは 1.0 となる。
1065        let p_onehot = vec![1.0, 0.0, 0.0];
1066        assert!((top_margin(&p_onehot).unwrap() - 1.0).abs() < 1e-6);
1067
1068        // 5. 最大値が末尾にある場合
1069        let p_last = vec![0.1, 0.2, 0.7];
1070        assert!((top_margin(&p_last).unwrap() - 0.5).abs() < 1e-6);
1071
1072        // 6. 異常系
1073        assert!(top_margin(&[]).is_err());
1074        assert!(top_margin(&[-0.5, 1.5]).is_err());
1075        assert!(top_margin(&[0.5, f64::NAN]).is_err());
1076        assert!(top_margin(&[f64::INFINITY, 0.0]).is_err());
1077        assert!(top_margin(&[f64::NAN]).is_err());
1078    }
1079
1080    #[test]
1081    fn test_composite_confidence() {
1082        // 1. K=1 の特異点: 完全に決定しているため 1.0 を返却する。
1083        assert_eq!(composite_confidence(&[1.0]).unwrap(), 1.0);
1084
1085        // 2. ワンホット極限: H_norm = 0.0, Margin = 1.0 -> S_conf = 1.0
1086        let p_onehot = vec![1.0, 0.0, 0.0];
1087        assert!((composite_confidence(&p_onehot).unwrap() - 1.0).abs() < 1e-6);
1088
1089        // 3. 完全一様分布極限: H_norm = 1.0, Margin = 0.0 -> S_conf = 0.0
1090        let p_uniform = vec![0.25, 0.25, 0.25, 0.25];
1091        assert!(composite_confidence(&p_uniform).unwrap() < 1e-6);
1092
1093        // 4. Margin Collapse (上位2候補激突):
1094        // p = [0.49, 0.49, 0.02]
1095        // 分布全体としてはそこそこ尖っているが、Margin = 0.0 のため S_conf は 0.0 となる。
1096        let p_collapse = vec![0.49, 0.49, 0.02];
1097        assert!(composite_confidence(&p_collapse).unwrap() < 1e-6);
1098
1099        // 5. 高確信判定ケース:
1100        // p = [0.85, 0.10, 0.05]
1101        // H(p) = -(0.85*ln 0.85 + 0.1*ln 0.1 + 0.05*ln 0.05) ≈ 0.518
1102        // H_norm = 0.518 / ln(3) ≈ 0.518 / 1.0986 ≈ 0.4715
1103        // Margin = 0.85 - 0.10 = 0.75
1104        // S_conf = (1.0 - 0.4715) * 0.75 ≈ 0.396
1105        let p_high = vec![0.85, 0.10, 0.05];
1106        let s_conf = composite_confidence(&p_high).unwrap();
1107        assert!(s_conf > 0.35 && s_conf < 0.45);
1108
1109        // 6. 圧倒的確信ケース:
1110        // p = [0.98, 0.01, 0.01]
1111        // Margin = 0.97
1112        // 1 - H_norm ≈ 0.89
1113        // S_conf ≈ 0.86
1114        let p_extreme = vec![0.98, 0.01, 0.01];
1115        let s_extreme = composite_confidence(&p_extreme).unwrap();
1116        assert!(s_extreme > 0.80);
1117
1118        // 7. 異常系
1119        assert!(composite_confidence(&[]).is_err());
1120        assert!(composite_confidence(&[-0.1, 1.1]).is_err());
1121        assert!(composite_confidence(&[0.5, f64::NAN]).is_err());
1122    }
1123
1124    #[test]
1125    fn test_bimodality_coefficient() {
1126        // 1. ワンホット極限: 分散ゼロのため 0.0 を返却する。
1127        let p_onehot = vec![1.0, 0.0, 0.0, 0.0];
1128        assert_eq!(bimodality_coefficient(&p_onehot).unwrap(), 0.0);
1129
1130        // 2. 完全両端対立 (二峰性): [0.5, 0.0, 0.0, 0.5]
1131        // 平均 1.5, mu_2 = 2.25, mu_3 = 0.0, mu_4 = 5.0625, kurtosis = 1.0 -> BC = 1.0
1132        let p_bimodal = vec![0.5, 0.0, 0.0, 0.5];
1133        let bc_bimodal = bimodality_coefficient(&p_bimodal).unwrap();
1134        assert!((bc_bimodal - 1.0).abs() < 1e-6);
1135
1136        // 3. 5段階の両端対立: [0.5, 0.0, 0.0, 0.0, 0.5]
1137        let p_bimodal_5 = vec![0.5, 0.0, 0.0, 0.0, 0.5];
1138        let bc_bimodal_5 = bimodality_coefficient(&p_bimodal_5).unwrap();
1139        assert!((bc_bimodal_5 - 1.0).abs() < 1e-6);
1140
1141        // 4. 一様分布: 離散一様分布では M に応じて 0.555〜0.65 付近となる (連続極限で 5/9 ≈ 0.555)。
1142        let p_uniform_4 = vec![0.25, 0.25, 0.25, 0.25];
1143        let bc_uniform_4 = bimodality_coefficient(&p_uniform_4).unwrap();
1144        assert!(bc_uniform_4 > 0.55 && bc_uniform_4 < 0.65);
1145
1146        let p_uniform_10 = vec![0.1; 10];
1147        let bc_uniform_10 = bimodality_coefficient(&p_uniform_10).unwrap();
1148        assert!(bc_uniform_10 > 0.55 && bc_uniform_10 < 0.60);
1149
1150        // 5. 単峰山型分布: [0.05, 0.2, 0.5, 0.2, 0.05] -> BC は 0.555 未満
1151        let p_unimodal = vec![0.05, 0.2, 0.5, 0.2, 0.05];
1152        let bc_unimodal = bimodality_coefficient(&p_unimodal).unwrap();
1153        assert!(bc_unimodal < 0.555);
1154
1155        // 6. 隣接割れ (低分散): [0.0, 0.5, 0.5, 0.0]
1156        // 平均 1.5, 分散 0.25, kurtosis = 1.0 -> BC = 1.0 だが分散比率は極めて小さい
1157        let p_adjacent = vec![0.0, 0.5, 0.5, 0.0];
1158        let var_ratio = score_variance_ratio(&p_adjacent).unwrap();
1159        assert!(var_ratio < 0.20);
1160
1161        // 7. 異常系
1162        assert!(bimodality_coefficient(&[]).is_err());
1163        assert!(bimodality_coefficient(&[1.0]).is_err());
1164        assert!(bimodality_coefficient(&[-0.1, 1.1]).is_err());
1165    }
1166
1167    #[test]
1168    fn test_score_variance_ratio() {
1169        // 1. 完全両端対立: 理論最大分散と一致するため 1.0
1170        let p_bimodal = vec![0.5, 0.0, 0.0, 0.5];
1171        let ratio = score_variance_ratio(&p_bimodal).unwrap();
1172        assert!((ratio - 1.0).abs() < 1e-6);
1173
1174        // 2. ワンホット: 分散 0.0
1175        let p_onehot = vec![0.0, 1.0, 0.0, 0.0];
1176        let ratio_onehot = score_variance_ratio(&p_onehot).unwrap();
1177        assert!(ratio_onehot < 1e-6);
1178
1179        // 3. 異常系
1180        assert!(score_variance_ratio(&[]).is_err());
1181        assert!(score_variance_ratio(&[1.0]).is_err());
1182    }
1183
1184    #[test]
1185    fn test_free_energy_basic() {
1186        let logits = vec![2.0, 1.0, 0.0];
1187        let t = 1.0;
1188        let fe = free_energy(&logits, t).unwrap();
1189        // 理論値: -ln(exp(2) + exp(1) + exp(0)) = -ln(7.389056 + 2.718282 + 1.0) = -ln(11.107338) ≈ -2.4076
1190        let expected = -((2.0f64).exp() + (1.0f64).exp() + (0.0f64).exp()).ln();
1191        assert!((fe - expected).abs() < 1e-9);
1192
1193        // K=1 の場合は -z_0
1194        let fe_single = free_energy(&[3.5], 1.0).unwrap();
1195        assert!((fe_single - (-3.5)).abs() < 1e-9);
1196    }
1197
1198    #[test]
1199    fn test_normalized_free_energy_uniform() {
1200        // 全ロジットが同一 c の場合、E_norm は厳密に -c に一致する。
1201        for &k in &[2, 3, 5, 10, 50] {
1202            let logits_zero = vec![0.0; k];
1203            let nfe_zero = normalized_free_energy(&logits_zero, 1.0).unwrap();
1204            assert!(
1205                nfe_zero.abs() < 1e-9,
1206                "k={k} でロジットが 0 のとき E_norm は 0.0 であるべき"
1207            );
1208
1209            let c = 2.5;
1210            let logits_const = vec![c; k];
1211            let nfe_const = normalized_free_energy(&logits_const, 1.2).unwrap();
1212            assert!(
1213                (nfe_const - (-c)).abs() < 1e-9,
1214                "k={k} でロジットが c のとき E_norm は -c であるべき"
1215            );
1216        }
1217    }
1218
1219    #[test]
1220    fn test_free_energy_extreme_logits() {
1221        // 大きなロジットでも Max-trick によりオーバーフローせずに計算可能。
1222        let logits = vec![1000.0, 999.0, 998.0];
1223        let fe = free_energy(&logits, 1.0).unwrap();
1224        assert!(fe.is_finite());
1225        // E = -1000 - ln(1 + exp(-1) + exp(-2)) ≈ -1000 - 0.4076 = -1000.4076
1226        assert!(fe < -999.0);
1227
1228        let nfe = normalized_free_energy(&logits, 1.0).unwrap();
1229        assert!(nfe.is_finite());
1230        // nfe = fe + ln(3) ≈ -1000.4076 + 1.0986 = -999.309
1231        assert!(nfe < -998.0);
1232    }
1233
1234    #[test]
1235    fn test_free_energy_ood_vs_id() {
1236        let t = 1.0;
1237        // In-Distribution: 1つの候補が強く発火 (低エネルギー = 負の大きな値)
1238        let id_logits = vec![5.0, 0.2, -0.5];
1239        let id_energy = normalized_free_energy(&id_logits, t).unwrap();
1240        assert!(id_energy < -3.5);
1241
1242        // Out-of-Distribution: 全ての候補が低く沈んでいる (高エネルギー = 正または0近傍の大きな値)
1243        let ood_logits = vec![-4.0, -4.5, -5.0];
1244        let ood_energy = normalized_free_energy(&ood_logits, t).unwrap();
1245        assert!(ood_energy > 3.0);
1246
1247        // OOD のエネルギーは ID より遥かに高い
1248        assert!(ood_energy > id_energy + 6.0);
1249    }
1250
1251    #[test]
1252    fn test_free_energy_errors_and_edge_cases() {
1253        // 空配列
1254        assert!(free_energy(&[], 1.0).is_err());
1255        assert!(normalized_free_energy(&[], 1.0).is_err());
1256
1257        // 温度が 0 以下または非有限
1258        assert!(free_energy(&[1.0, 2.0], 0.0).is_err());
1259        assert!(free_energy(&[1.0, 2.0], -1.0).is_err());
1260        assert!(free_energy(&[1.0, 2.0], f64::NAN).is_err());
1261
1262        // ロジットに非有限値
1263        assert!(free_energy(&[1.0, f64::NAN], 1.0).is_err());
1264        assert!(free_energy(&[1.0, f64::INFINITY], 1.0).is_err());
1265    }
1266}