sphereql_embed/
configured_projection.rs1use sphereql_core::SphericalPoint;
15
16use crate::config::ProjectionKind;
17use crate::kernel_pca::KernelPcaProjection;
18use crate::laplacian::LaplacianEigenmapProjection;
19use crate::projection::{PcaProjection, Projection};
20use crate::types::{Embedding, ProjectedPoint};
21
22#[derive(Clone)]
28pub enum ConfiguredProjection {
29 Pca(PcaProjection),
30 KernelPca(KernelPcaProjection),
31 Laplacian(LaplacianEigenmapProjection),
32}
33
34impl std::fmt::Debug for ConfiguredProjection {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 Self::Pca(_) => write!(f, "ConfiguredProjection::Pca"),
38 Self::KernelPca(_) => write!(f, "ConfiguredProjection::KernelPca"),
39 Self::Laplacian(_) => write!(f, "ConfiguredProjection::Laplacian"),
40 }
41 }
42}
43
44impl Projection for ConfiguredProjection {
45 fn project(&self, embedding: &Embedding) -> SphericalPoint {
46 match self {
47 Self::Pca(p) => p.project(embedding),
48 Self::KernelPca(p) => p.project(embedding),
49 Self::Laplacian(p) => p.project(embedding),
50 }
51 }
52
53 fn project_rich(&self, embedding: &Embedding) -> ProjectedPoint {
54 match self {
55 Self::Pca(p) => p.project_rich(embedding),
56 Self::KernelPca(p) => p.project_rich(embedding),
57 Self::Laplacian(p) => p.project_rich(embedding),
58 }
59 }
60
61 fn dimensionality(&self) -> usize {
62 match self {
63 Self::Pca(p) => p.dimensionality(),
64 Self::KernelPca(p) => p.dimensionality(),
65 Self::Laplacian(p) => p.dimensionality(),
66 }
67 }
68}
69
70impl ConfiguredProjection {
71 pub fn kind(&self) -> ProjectionKind {
73 match self {
74 Self::Pca(_) => ProjectionKind::Pca,
75 Self::KernelPca(_) => ProjectionKind::KernelPca,
76 Self::Laplacian(_) => ProjectionKind::LaplacianEigenmap,
77 }
78 }
79
80 pub fn explained_variance_ratio(&self) -> f64 {
86 match self {
87 Self::Pca(p) => p.explained_variance_ratio(),
88 Self::KernelPca(p) => p.explained_variance_ratio(),
89 Self::Laplacian(p) => p.explained_variance_ratio(),
90 }
91 }
92
93 pub fn as_pca(&self) -> Option<&PcaProjection> {
95 match self {
96 Self::Pca(p) => Some(p),
97 _ => None,
98 }
99 }
100
101 pub fn as_kernel_pca(&self) -> Option<&KernelPcaProjection> {
103 match self {
104 Self::KernelPca(p) => Some(p),
105 _ => None,
106 }
107 }
108
109 pub fn as_laplacian(&self) -> Option<&LaplacianEigenmapProjection> {
112 match self {
113 Self::Laplacian(p) => Some(p),
114 _ => None,
115 }
116 }
117}
118
119impl From<PcaProjection> for ConfiguredProjection {
120 fn from(p: PcaProjection) -> Self {
121 Self::Pca(p)
122 }
123}
124
125impl From<KernelPcaProjection> for ConfiguredProjection {
126 fn from(p: KernelPcaProjection) -> Self {
127 Self::KernelPca(p)
128 }
129}
130
131impl From<LaplacianEigenmapProjection> for ConfiguredProjection {
132 fn from(p: LaplacianEigenmapProjection) -> Self {
133 Self::Laplacian(p)
134 }
135}
136
137#[cfg(test)]
140mod tests {
141 use super::*;
142 use crate::types::RadialStrategy;
143
144 fn emb(vals: &[f64]) -> Embedding {
145 Embedding::new(vals.to_vec())
146 }
147
148 fn toy_corpus() -> Vec<Embedding> {
149 (0..8)
150 .map(|i| {
151 let t = i as f64;
152 emb(&[1.0 + t * 0.01, 0.5 - t * 0.01, 0.2, 0.05, 0.03])
153 })
154 .collect()
155 }
156
157 #[test]
158 fn pca_variant_dispatches() {
159 let corpus = toy_corpus();
160 let pca = PcaProjection::fit(&corpus, RadialStrategy::Fixed(1.0)).unwrap();
161 let cp: ConfiguredProjection = pca.into();
162 assert_eq!(cp.kind(), ProjectionKind::Pca);
163 assert_eq!(cp.dimensionality(), 5);
164 let sp = cp.project(&corpus[0]);
165 assert!((sp.r - 1.0).abs() < 1e-9);
166 assert!(cp.as_pca().is_some());
167 assert!(cp.as_kernel_pca().is_none());
168 assert!(cp.as_laplacian().is_none());
169 }
170
171 #[test]
172 fn kernel_pca_variant_dispatches() {
173 let corpus = toy_corpus();
174 let kpca = KernelPcaProjection::fit(&corpus, RadialStrategy::Fixed(1.0)).unwrap();
175 let cp: ConfiguredProjection = kpca.into();
176 assert_eq!(cp.kind(), ProjectionKind::KernelPca);
177 assert_eq!(cp.dimensionality(), 5);
178 assert!(cp.as_kernel_pca().is_some());
179 assert!(cp.as_pca().is_none());
180 }
181
182 #[test]
183 fn laplacian_variant_dispatches() {
184 let corpus = toy_corpus();
187 let lap = LaplacianEigenmapProjection::fit(&corpus, RadialStrategy::Fixed(1.0)).unwrap();
188 let cp: ConfiguredProjection = lap.into();
189 assert_eq!(cp.kind(), ProjectionKind::LaplacianEigenmap);
190 assert_eq!(cp.dimensionality(), 5);
191 assert!(cp.as_laplacian().is_some());
192 assert!(cp.as_pca().is_none());
193 }
194
195 #[test]
196 fn explained_variance_ratio_in_range_for_every_variant() {
197 let corpus = toy_corpus();
198 let pca: ConfiguredProjection = PcaProjection::fit(&corpus, RadialStrategy::Fixed(1.0))
199 .unwrap()
200 .into();
201 let kpca: ConfiguredProjection =
202 KernelPcaProjection::fit(&corpus, RadialStrategy::Fixed(1.0))
203 .unwrap()
204 .into();
205 let lap: ConfiguredProjection =
206 LaplacianEigenmapProjection::fit(&corpus, RadialStrategy::Fixed(1.0))
207 .unwrap()
208 .into();
209 for cp in &[pca, kpca, lap] {
210 let r = cp.explained_variance_ratio();
211 assert!((0.0..=1.0).contains(&r), "{:?}: {r}", cp);
212 }
213 }
214
215 #[test]
216 fn debug_formats_kind_not_inner() {
217 let corpus = toy_corpus();
218 let pca: ConfiguredProjection = PcaProjection::fit(&corpus, RadialStrategy::Fixed(1.0))
219 .unwrap()
220 .into();
221 assert_eq!(format!("{:?}", pca), "ConfiguredProjection::Pca");
222 }
223}