1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
//! #581 — binary `decision_function` shape/sign for
//! `QuadraticDiscriminantAnalysis`, mirroring the logistic #454 precedent
//! (`divergence_logistic_decision_shape.rs`).
//!
//! ## Two distinct contracts
//!
//! **Rust library (this crate):** `FittedQDA::decision_function -> Array2<F>`
//! ALWAYS returns the raw `(n_samples, n_classes)` per-class log-posterior — for
//! binary that is `(n, 2)` (the two raw class scores, NOT their difference).
//! This uniform 2-D shape is the same Rust idiom the logistic library uses
//! (`divergence_logistic_decision_shape.rs`): callers pattern on `Array2`
//! regardless of class count.
//!
//! **scikit-learn (the oracle):** `QuadraticDiscriminantAnalysis.decision_function`
//! collapses the BINARY case to shape `(n,)` = `dec[:,1] - dec[:,0]`, the
//! log-likelihood ratio of the positive class
//! (`sklearn/discriminant_analysis.py:1000-1002`:
//! `if len(self.classes_) == 2: return dec_func[:, 1] - dec_func[:, 0]`).
//!
//! Per goal.md, correctness lives in the library crate; SHAPE at the Python↔Rust
//! boundary is a marshalling/ABI concern fixed in `ferrolearn-python`. So the
//! sklearn `(n,)` binary shape is a BINDING ABI contract:
//! `ferrolearn.QuadraticDiscriminantAnalysis.decision_function` must transform
//! the library's binary `(n,2)` matrix into sklearn's `(n,)` = `col1 - col0`.
//! That ravel-and-difference is tracked by #581 and will be pinned as a pytest
//! divergence when the loop reaches `ferrolearn-python` (the binding currently
//! exposes only `new`/`fit`/`predict`, so `decision_function` is not yet on the
//! ABI at all).
//!
//! NOTE vs logistic #454: the logistic library returns binary `(n, 1)` (a single
//! raveled score), so its binding ravel is a pure `(n,1)->(n,)` reshape. QDA's
//! library returns `(n, 2)` (two raw scores), so its binding transform is
//! `dec[:,1] - dec[:,0]` — a value combination, not just a reshape. This test
//! pins BOTH facts: the library shape is `(n, 2)`, and `col1 - col0` reproduces
//! the live sklearn `(n,)` binary `decision_function` exactly.
//!
//! Tracking: #581 (ferrolearn-python binding ABI item; kept open).
use Fit;
use QDA;
use ;
/// Library contract: binary `decision_function` is `Array2` shape `(n, 2)`, and
/// the binding's required `dec[:,1] - dec[:,0]` transform reproduces the live
/// sklearn 1.5.2 binary `decision_function` `(n,)` values exactly.
///
/// Oracle (live sklearn 1.5.2):
/// ```text
/// python3 -c "import numpy as np; from sklearn.discriminant_analysis import \
/// QuadraticDiscriminantAnalysis as Q; \
/// X=np.array([[1.,1.],[1.,3.],[2.,1.5],[2.,2.5],[8.,8.],[8.,9.5],[9.,8.5],[9.,9.5]]); \
/// y=np.array([0,0,0,0,1,1,1,1]); m=Q().fit(X,y); \
/// print(m.decision_function(X).shape); print(m.decision_function(X).tolist())"
/// # shape (8,)
/// # [-116.41537777272299, -97.95383931118454, -93.15383931118453,
/// # -83.46153161887683, 84.29616068881546, 96.44616068881548,
/// # 109.33462222727701, 117.73462222727701]
/// ```