1use scirs2_core::ndarray_ext::{Array1, Array2, ArrayView2};
8use sklears_core::error::{Result as SklResult, SklearsError};
9use sklears_core::types::Float;
10
11pub trait GraphBuilder: Clone {
13 #[allow(non_snake_case)] fn build(&self, X: &ArrayView2<Float>) -> SklResult<Array2<f64>>;
16}
17
18pub trait GraphTransform: Clone {
20 fn transform(&self, graph: &Array2<f64>) -> SklResult<Array2<f64>>;
22}
23
24#[derive(Debug, Clone)]
26pub struct KNNGraphBuilder {
27 n_neighbors: usize,
28 weighted: bool,
29 sigma: f64,
30}
31
32impl KNNGraphBuilder {
33 pub fn new(n_neighbors: usize) -> Self {
35 Self {
36 n_neighbors,
37 weighted: true,
38 sigma: 1.0,
39 }
40 }
41
42 pub fn weighted(mut self, weighted: bool) -> Self {
44 self.weighted = weighted;
45 self
46 }
47
48 pub fn sigma(mut self, sigma: f64) -> Self {
50 self.sigma = sigma;
51 self
52 }
53}
54
55impl GraphBuilder for KNNGraphBuilder {
56 #[allow(non_snake_case)] fn build(&self, X: &ArrayView2<Float>) -> SklResult<Array2<f64>> {
58 let n_samples = X.nrows();
59 let mut graph = Array2::<f64>::zeros((n_samples, n_samples));
60
61 for i in 0..n_samples {
62 let mut distances: Vec<(usize, f64)> = Vec::new();
63
64 for j in 0..n_samples {
65 if i != j {
66 let diff = &X.row(i) - &X.row(j);
67 let dist = diff.mapv(|x| x * x).sum().sqrt();
68 distances.push((j, dist));
69 }
70 }
71
72 distances.sort_by(|a, b| a.1.partial_cmp(&b.1).expect("operation should succeed"));
73
74 for &(j, dist) in distances.iter().take(self.n_neighbors) {
75 if self.weighted {
76 let weight = (-dist * dist / (2.0 * self.sigma * self.sigma)).exp();
77 graph[[i, j]] = weight;
78 } else {
79 graph[[i, j]] = 1.0;
80 }
81 }
82 }
83
84 Ok(graph)
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct EpsilonGraphBuilder {
91 epsilon: f64,
92 weighted: bool,
93 sigma: f64,
94}
95
96impl EpsilonGraphBuilder {
97 pub fn new(epsilon: f64) -> Self {
99 Self {
100 epsilon,
101 weighted: true,
102 sigma: 1.0,
103 }
104 }
105
106 pub fn weighted(mut self, weighted: bool) -> Self {
108 self.weighted = weighted;
109 self
110 }
111
112 pub fn sigma(mut self, sigma: f64) -> Self {
114 self.sigma = sigma;
115 self
116 }
117}
118
119impl GraphBuilder for EpsilonGraphBuilder {
120 #[allow(non_snake_case)] fn build(&self, X: &ArrayView2<Float>) -> SklResult<Array2<f64>> {
122 let n_samples = X.nrows();
123 let mut graph = Array2::<f64>::zeros((n_samples, n_samples));
124
125 for i in 0..n_samples {
126 for j in 0..n_samples {
127 if i != j {
128 let diff = &X.row(i) - &X.row(j);
129 let dist = diff.mapv(|x| x * x).sum().sqrt();
130
131 if dist < self.epsilon {
132 if self.weighted {
133 let weight = (-dist * dist / (2.0 * self.sigma * self.sigma)).exp();
134 graph[[i, j]] = weight;
135 } else {
136 graph[[i, j]] = 1.0;
137 }
138 }
139 }
140 }
141 }
142
143 Ok(graph)
144 }
145}
146
147#[derive(Debug, Clone)]
149pub struct SymmetrizeTransform {
150 method: String,
151}
152
153impl SymmetrizeTransform {
154 pub fn new(method: String) -> Self {
156 Self { method }
157 }
158}
159
160impl GraphTransform for SymmetrizeTransform {
161 fn transform(&self, graph: &Array2<f64>) -> SklResult<Array2<f64>> {
162 let n = graph.nrows();
163 let mut symmetric = graph.clone();
164
165 match self.method.as_str() {
166 "max" => {
167 for i in 0..n {
168 for j in (i + 1)..n {
169 let value = graph[[i, j]].max(graph[[j, i]]);
170 symmetric[[i, j]] = value;
171 symmetric[[j, i]] = value;
172 }
173 }
174 }
175 "average" => {
176 for i in 0..n {
177 for j in (i + 1)..n {
178 let value = (graph[[i, j]] + graph[[j, i]]) / 2.0;
179 symmetric[[i, j]] = value;
180 symmetric[[j, i]] = value;
181 }
182 }
183 }
184 _ => {
185 return Err(SklearsError::InvalidInput(format!(
186 "Unknown symmetrization method: {}",
187 self.method
188 )));
189 }
190 }
191
192 Ok(symmetric)
193 }
194}
195
196#[derive(Debug, Clone)]
198pub struct NormalizeTransform {
199 method: String,
200}
201
202impl NormalizeTransform {
203 pub fn new(method: String) -> Self {
205 Self { method }
206 }
207}
208
209impl GraphTransform for NormalizeTransform {
210 fn transform(&self, graph: &Array2<f64>) -> SklResult<Array2<f64>> {
211 let n = graph.nrows();
212 let mut normalized = graph.clone();
213
214 match self.method.as_str() {
215 "row" => {
216 for i in 0..n {
217 let row_sum: f64 = graph.row(i).sum();
218 if row_sum > 0.0 {
219 for j in 0..n {
220 normalized[[i, j]] /= row_sum;
221 }
222 }
223 }
224 }
225 "symmetric" => {
226 let mut degrees = Array1::<f64>::zeros(n);
228 for i in 0..n {
229 degrees[i] = graph.row(i).sum();
230 }
231
232 for i in 0..n {
233 for j in 0..n {
234 if degrees[i] > 0.0 && degrees[j] > 0.0 {
235 normalized[[i, j]] = graph[[i, j]] / (degrees[i] * degrees[j]).sqrt();
236 }
237 }
238 }
239 }
240 _ => {
241 return Err(SklearsError::InvalidInput(format!(
242 "Unknown normalization method: {}",
243 self.method
244 )));
245 }
246 }
247
248 Ok(normalized)
249 }
250}
251
252#[derive(Debug, Clone)]
254pub struct SparsifyTransform {
255 threshold: f64,
256}
257
258impl SparsifyTransform {
259 pub fn new(threshold: f64) -> Self {
261 Self { threshold }
262 }
263}
264
265impl GraphTransform for SparsifyTransform {
266 fn transform(&self, graph: &Array2<f64>) -> SklResult<Array2<f64>> {
267 let mut sparse = graph.clone();
268 let n = graph.nrows();
269
270 for i in 0..n {
271 for j in 0..n {
272 if sparse[[i, j]] < self.threshold {
273 sparse[[i, j]] = 0.0;
274 }
275 }
276 }
277
278 Ok(sparse)
279 }
280}
281
282#[derive(Clone)]
284pub struct GraphPipeline {
285 builder: Box<dyn GraphBuilderTrait>,
286 transforms: Vec<Box<dyn GraphTransformTrait>>,
287}
288
289impl std::fmt::Debug for GraphPipeline {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 f.debug_struct("GraphPipeline")
292 .field("builder", &"Box<dyn GraphBuilderTrait>")
293 .field(
294 "transforms",
295 &format!("{} transforms", self.transforms.len()),
296 )
297 .finish()
298 }
299}
300
301trait GraphBuilderTrait {
303 #[allow(non_snake_case)] fn build_graph(&self, X: &ArrayView2<Float>) -> SklResult<Array2<f64>>;
305 fn clone_box(&self) -> Box<dyn GraphBuilderTrait>;
306}
307
308trait GraphTransformTrait {
309 fn transform_graph(&self, graph: &Array2<f64>) -> SklResult<Array2<f64>>;
310 fn clone_box(&self) -> Box<dyn GraphTransformTrait>;
311}
312
313impl<T: GraphBuilder + 'static> GraphBuilderTrait for T {
314 #[allow(non_snake_case)] fn build_graph(&self, X: &ArrayView2<Float>) -> SklResult<Array2<f64>> {
316 self.build(X)
317 }
318
319 fn clone_box(&self) -> Box<dyn GraphBuilderTrait> {
320 Box::new(self.clone())
321 }
322}
323
324impl<T: GraphTransform + 'static> GraphTransformTrait for T {
325 fn transform_graph(&self, graph: &Array2<f64>) -> SklResult<Array2<f64>> {
326 self.transform(graph)
327 }
328
329 fn clone_box(&self) -> Box<dyn GraphTransformTrait> {
330 Box::new(self.clone())
331 }
332}
333
334impl Clone for Box<dyn GraphBuilderTrait> {
335 fn clone(&self) -> Self {
336 self.clone_box()
337 }
338}
339
340impl Clone for Box<dyn GraphTransformTrait> {
341 fn clone(&self) -> Self {
342 self.clone_box()
343 }
344}
345
346impl GraphPipeline {
347 pub fn new<B: GraphBuilder + 'static>(builder: B) -> Self {
349 Self {
350 builder: Box::new(builder),
351 transforms: Vec::new(),
352 }
353 }
354
355 pub fn add_transform<T: GraphTransform + 'static>(mut self, transform: T) -> Self {
357 self.transforms.push(Box::new(transform));
358 self
359 }
360
361 #[allow(non_snake_case)] pub fn build(&self, X: &ArrayView2<Float>) -> SklResult<Array2<f64>> {
364 let mut graph = self.builder.build_graph(X)?;
365
366 for transform in &self.transforms {
367 graph = transform.transform_graph(&graph)?;
368 }
369
370 Ok(graph)
371 }
372}
373
374#[cfg(test)]
375mod tests {
376 use super::*;
377 use scirs2_core::array;
378
379 #[test]
380 #[allow(non_snake_case)]
381 fn test_knn_graph_builder() {
382 let X = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]];
383 let builder = KNNGraphBuilder::new(2).weighted(true).sigma(1.0);
384
385 let graph = builder.build(&X.view()).expect("operation should succeed");
386
387 assert_eq!(graph.dim(), (4, 4));
388 for i in 0..4 {
390 let row_nonzero = graph.row(i).iter().filter(|&&x| x > 0.0).count();
391 assert_eq!(row_nonzero, 2);
392 }
393 }
394
395 #[test]
396 #[allow(non_snake_case)]
397 fn test_epsilon_graph_builder() {
398 let X = array![[0.0, 0.0], [1.0, 0.0], [10.0, 0.0]];
399 let builder = EpsilonGraphBuilder::new(2.0).weighted(false);
400
401 let graph = builder.build(&X.view()).expect("operation should succeed");
402
403 assert_eq!(graph.dim(), (3, 3));
404 assert_eq!(graph[[0, 1]], 1.0);
406 assert_eq!(graph[[1, 0]], 1.0);
407 assert_eq!(graph[[0, 2]], 0.0);
409 }
410
411 #[test]
412 fn test_symmetrize_transform() {
413 let mut graph = Array2::<f64>::zeros((3, 3));
414 graph[[0, 1]] = 1.0;
415 graph[[1, 0]] = 2.0;
416 graph[[1, 2]] = 3.0;
417 graph[[2, 1]] = 4.0;
418
419 let transform = SymmetrizeTransform::new("max".to_string());
420 let symmetric = transform
421 .transform(&graph)
422 .expect("operation should succeed");
423
424 assert_eq!(symmetric[[0, 1]], 2.0);
425 assert_eq!(symmetric[[1, 0]], 2.0);
426 assert_eq!(symmetric[[1, 2]], 4.0);
427 assert_eq!(symmetric[[2, 1]], 4.0);
428 }
429
430 #[test]
431 fn test_normalize_transform() {
432 let mut graph = Array2::<f64>::zeros((3, 3));
433 graph[[0, 1]] = 2.0;
434 graph[[0, 2]] = 2.0;
435 graph[[1, 0]] = 1.0;
436
437 let transform = NormalizeTransform::new("row".to_string());
438 let normalized = transform
439 .transform(&graph)
440 .expect("operation should succeed");
441
442 let row_sum: f64 = normalized.row(0).sum();
444 assert!((row_sum - 1.0).abs() < 1e-10);
445 }
446
447 #[test]
448 fn test_sparsify_transform() {
449 let mut graph = Array2::<f64>::zeros((3, 3));
450 graph[[0, 1]] = 0.1;
451 graph[[0, 2]] = 0.5;
452 graph[[1, 2]] = 0.3;
453
454 let transform = SparsifyTransform::new(0.2);
455 let sparse = transform
456 .transform(&graph)
457 .expect("operation should succeed");
458
459 assert_eq!(sparse[[0, 1]], 0.0); assert_eq!(sparse[[0, 2]], 0.5); assert_eq!(sparse[[1, 2]], 0.3); }
463
464 #[test]
465 #[allow(non_snake_case)]
466 fn test_graph_pipeline() {
467 let X = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0], [3.0, 3.0]];
468
469 let pipeline = GraphPipeline::new(KNNGraphBuilder::new(2).weighted(true))
470 .add_transform(SymmetrizeTransform::new("average".to_string()))
471 .add_transform(NormalizeTransform::new("row".to_string()));
472
473 let graph = pipeline.build(&X.view()).expect("operation should succeed");
474
475 assert_eq!(graph.dim(), (4, 4));
476
477 for i in 0..4 {
479 let row_sum: f64 = graph.row(i).sum();
480 assert!((row_sum - 1.0).abs() < 1e-6 || row_sum == 0.0);
481 }
482
483 let total_edges: usize = graph.iter().filter(|&&x| x > 0.0).count();
486 assert!(total_edges > 0);
487 }
488
489 #[test]
490 #[allow(non_snake_case)]
491 fn test_symmetric_pipeline() {
492 let X = array![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
493
494 let pipeline = GraphPipeline::new(KNNGraphBuilder::new(1).weighted(true))
495 .add_transform(SymmetrizeTransform::new("max".to_string()));
496
497 let graph = pipeline.build(&X.view()).expect("operation should succeed");
498
499 assert_eq!(graph.dim(), (3, 3));
500
501 for i in 0..3 {
503 for j in 0..3 {
504 assert!((graph[[i, j]] - graph[[j, i]]).abs() < 1e-10);
505 }
506 }
507 }
508}