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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
//! Divergence pins for `KernelPCA` (ferrolearn-decomp) vs scikit-learn 1.5.2.
//!
//! Audited area: `ferrolearn-decomp/src/kernel_pca.rs` against
//! `sklearn/decomposition/_kernel_pca.py` (tag 1.5.2). The numerical core
//! (eigenvalues_, double-centering, eigenvector `1/sqrt(eigval)` scaling,
//! transform values, and the `svd_flip` sign convention) was found to match
//! the live sklearn 1.5.2 dense oracle to ~1e-12 for distinct-eigenvalue
//! inputs and is NOT pinned here (CLEAN).
//!
//! The two divergences pinned below are control-flow / contract divergences
//! where ferrolearn produces a DIFFERENT observable outcome than sklearn:
//!
//! - #2389: `n_components > n_samples`. sklearn clamps `n_components =
//! min(K.shape[0], n_components)` (`_kernel_pca.py:337`) and FITS; ferrolearn
//! returns `Err(InvalidParameter)`.
//! - #2390: a non-PSD centered kernel (e.g. `sigmoid`) with significant
//! negative eigenvalues. sklearn RAISES `ValueError` via
//! `_check_psd_eigenvalues` (`utils/validation.py:1958`, called at
//! `_kernel_pca.py:368`); ferrolearn silently clamps the negatives to 0 and
//! returns `Ok` with a garbage embedding.
//!
//! Expected values come from the live sklearn 1.5.2 oracle (R-CHAR-3); the
//! commands are quoted in each test. NEVER literal-copied from ferrolearn.
use FerroError;
use ;
use ;
use ;
/// Deterministic distinct-eigenvalue fixture (6 samples, 2 features).
/// Divergence: `KernelPCA::fit` with `n_components > n_samples`.
///
/// sklearn `_kernel_pca.py:337`:
/// ```python
/// n_components = min(K.shape[0], self.n_components)
/// ```
/// so `KernelPCA(n_components=10, kernel='linear').fit(X)` on a 6-sample X
/// FITS and yields `eigenvalues_.shape == (6,)` and `transform(X).shape ==
/// (6, 6)` (the extra components are zero eigenvalues). ferrolearn instead
/// returns `Err(InvalidParameter{ name: "n_components", ... })`
/// (`kernel_pca.rs:508-516`).
///
/// Oracle (live sklearn 1.5.2, run from /tmp):
/// ```text
/// python3 -c "
/// import numpy as np; from sklearn.decomposition import KernelPCA
/// X=np.array([[0.1,0.3],[0.9,0.2],[0.4,0.8],[0.7,0.5],[0.2,0.6],[0.55,0.15]])
/// kp=KernelPCA(n_components=10,kernel='linear',eigen_solver='dense').fit(X)
/// print(kp.eigenvalues_.shape, kp.transform(X).shape)"
/// # -> (6,) (6, 6)
/// ```
///
/// Tracking: #2389
/// Divergence: a non-PSD centered kernel with significant negative eigenvalues.
///
/// sklearn `_kernel_pca.py:368` calls
/// `_check_psd_eigenvalues(self.eigenvalues_, enable_warnings=False)`
/// which at `sklearn/utils/validation.py:1958` RAISES:
/// `ValueError("There are significant negative eigenvalues (... of the maximum
/// positive). Either the matrix is not PSD ...")`.
///
/// The `sigmoid` kernel with `gamma=2.0, coef0=1.0` on the fixture produces a
/// centered gram with a negative eigenvalue ~0.092 of the max positive, so
/// sklearn ABORTS the fit. ferrolearn instead clamps negatives to 0
/// (`kernel_pca.rs:566-570`) and returns `Ok` with a garbage embedding.
///
/// Oracle (live sklearn 1.5.2, run from /tmp):
/// ```text
/// python3 -c "
/// import numpy as np; from sklearn.decomposition import KernelPCA
/// X=np.array([[0.1,0.3],[0.9,0.2],[0.4,0.8],[0.7,0.5],[0.2,0.6],[0.55,0.15]])
/// KernelPCA(n_components=5,kernel='sigmoid',gamma=2.0,coef0=1.0,eigen_solver='dense').fit(X)"
/// # -> ValueError: There are significant negative eigenvalues (0.0924979 of
/// # the maximum positive). Either the matrix is not PSD ...
/// ```
///
/// Tracking: #2390