Skip to main content

torsh_graph/
geometric.rs

1//! Geometric Graph Neural Networks
2//!
3//! This module provides geometric deep learning capabilities for graph-structured
4//! data with spatial coordinates. It includes geometric graph construction methods,
5//! spatial convolutions, and geometric transformations inspired by scirs2-spatial.
6//!
7//! # Features:
8//! - Geometric graph construction (k-NN, radius, Delaunay)
9//! - Point cloud to graph conversion
10//! - Spatial graph convolutions with distance-based weighting
11//! - Geometric transformations (rotation, translation, scaling)
12//! - 3D mesh processing
13//! - Geometric pooling operations
14/// Crate-local result alias: the error type defaults to [`TorshError`],
15/// so both `Result<T>` and `Result<T, OtherError>` stay valid.
16type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
17
18use crate::parameter::Parameter;
19use crate::{GraphData, GraphLayer};
20use scirs2_core::random::thread_rng;
21use std::cmp::Ordering;
22use std::collections::HashMap;
23use torsh_tensor::{
24    creation::{from_vec, randn, zeros},
25    Tensor,
26};
27
28/// Point in 3D space
29#[derive(Debug, Clone, Copy)]
30pub struct Point3D {
31    pub x: f32,
32    pub y: f32,
33    pub z: f32,
34}
35
36impl Point3D {
37    pub fn new(x: f32, y: f32, z: f32) -> Self {
38        Self { x, y, z }
39    }
40
41    pub fn distance(&self, other: &Point3D) -> f32 {
42        ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2))
43            .sqrt()
44    }
45
46    pub fn dot(&self, other: &Point3D) -> f32 {
47        self.x * other.x + self.y * other.y + self.z * other.z
48    }
49
50    pub fn norm(&self) -> f32 {
51        (self.x.powi(2) + self.y.powi(2) + self.z.powi(2)).sqrt()
52    }
53}
54
55/// Geometric graph construction methods
56pub struct GeometricGraphBuilder;
57
58impl GeometricGraphBuilder {
59    /// Build k-nearest neighbors graph from point cloud
60    pub fn knn_graph(points: &[Point3D], k: usize, features: Option<Tensor>) -> Result<GraphData> {
61        let num_points = points.len();
62        let mut edges = Vec::new();
63        let mut edge_weights = Vec::new();
64
65        for i in 0..num_points {
66            // Find k nearest neighbors
67            let mut distances: Vec<(usize, f32)> = (0..num_points)
68                .filter(|&j| j != i)
69                .map(|j| (j, points[i].distance(&points[j])))
70                .collect();
71
72            distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
73
74            for (j, dist) in distances.iter().take(k) {
75                edges.push(i as f32);
76                edges.push(*j as f32);
77                edge_weights.push(*dist);
78            }
79        }
80
81        let num_edges = edges.len() / 2;
82        let edge_index = from_vec(edges, &[2, num_edges], torsh_core::device::DeviceType::Cpu)?;
83
84        // Use provided features or create default features
85        let x = match features {
86            Some(features) => features,
87            None => {
88                let coords: Vec<f32> = points.iter().flat_map(|p| vec![p.x, p.y, p.z]).collect();
89                from_vec(
90                    coords,
91                    &[num_points, 3],
92                    torsh_core::device::DeviceType::Cpu,
93                )?
94            }
95        };
96
97        let mut graph = GraphData::new(x, edge_index);
98
99        // Store edge weights as edge attributes
100        let edge_attr = from_vec(
101            edge_weights,
102            &[num_edges, 1],
103            torsh_core::device::DeviceType::Cpu,
104        )?;
105        graph.edge_attr = Some(edge_attr);
106
107        Ok(graph)
108    }
109
110    /// Build radius graph (connect all points within radius)
111    pub fn radius_graph(
112        points: &[Point3D],
113        radius: f32,
114        features: Option<Tensor>,
115    ) -> Result<GraphData> {
116        let num_points = points.len();
117        let mut edges = Vec::new();
118        let mut edge_weights = Vec::new();
119
120        for i in 0..num_points {
121            for j in (i + 1)..num_points {
122                let dist = points[i].distance(&points[j]);
123
124                if dist <= radius {
125                    edges.push(i as f32);
126                    edges.push(j as f32);
127                    edges.push(j as f32);
128                    edges.push(i as f32);
129                    edge_weights.push(dist);
130                    edge_weights.push(dist);
131                }
132            }
133        }
134
135        let num_edges = edges.len() / 2;
136        let edge_index = if num_edges > 0 {
137            from_vec(edges, &[2, num_edges], torsh_core::device::DeviceType::Cpu)?
138        } else {
139            from_vec(vec![], &[2, 0], torsh_core::device::DeviceType::Cpu)?
140        };
141
142        let x = match features {
143            Some(features) => features,
144            None => {
145                let coords: Vec<f32> = points.iter().flat_map(|p| vec![p.x, p.y, p.z]).collect();
146                from_vec(
147                    coords,
148                    &[num_points, 3],
149                    torsh_core::device::DeviceType::Cpu,
150                )?
151            }
152        };
153
154        let mut graph = GraphData::new(x, edge_index);
155
156        if num_edges > 0 {
157            let edge_attr = from_vec(
158                edge_weights,
159                &[num_edges, 1],
160                torsh_core::device::DeviceType::Cpu,
161            )?;
162            graph.edge_attr = Some(edge_attr);
163        }
164
165        Ok(graph)
166    }
167
168    /// Build Delaunay triangulation graph (2D simplified version)
169    pub fn delaunay_graph_2d(points: &[(f32, f32)], features: Option<Tensor>) -> Result<GraphData> {
170        let num_points = points.len();
171
172        // Simplified Delaunay: connect points that are close
173        // Full Delaunay would require more complex algorithms
174        let mut edges = Vec::new();
175        let mut visited_pairs: std::collections::HashSet<(usize, usize)> =
176            std::collections::HashSet::new();
177
178        for i in 0..num_points {
179            // Find k nearest neighbors for simplified triangulation
180            let k = 5;
181            let mut distances: Vec<(usize, f32)> = (0..num_points)
182                .filter(|&j| j != i)
183                .map(|j| {
184                    let dx = points[i].0 - points[j].0;
185                    let dy = points[i].1 - points[j].1;
186                    (j, (dx * dx + dy * dy).sqrt())
187                })
188                .collect();
189
190            distances.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
191
192            for (j, _) in distances.iter().take(k) {
193                let pair = if i < *j { (i, *j) } else { (*j, i) };
194
195                if !visited_pairs.contains(&pair) {
196                    visited_pairs.insert(pair);
197                    edges.push(i as f32);
198                    edges.push(*j as f32);
199                    edges.push(*j as f32);
200                    edges.push(i as f32);
201                }
202            }
203        }
204
205        let num_edges = edges.len() / 2;
206        let edge_index = from_vec(edges, &[2, num_edges], torsh_core::device::DeviceType::Cpu)?;
207
208        let x = match features {
209            Some(features) => features,
210            None => {
211                let coords: Vec<f32> = points.iter().flat_map(|(x, y)| vec![*x, *y]).collect();
212                from_vec(
213                    coords,
214                    &[num_points, 2],
215                    torsh_core::device::DeviceType::Cpu,
216                )?
217            }
218        };
219
220        Ok(GraphData::new(x, edge_index))
221    }
222}
223
224/// Geometric convolution layer with distance-based attention
225#[derive(Debug)]
226pub struct GeometricConv {
227    in_features: usize,
228    out_features: usize,
229    hidden_dim: usize,
230
231    // MLP for message generation
232    message_mlp: Vec<Parameter>,
233
234    // Distance encoding
235    distance_encoder: Parameter,
236
237    // Output projection
238    output_weight: Parameter,
239
240    bias: Option<Parameter>,
241}
242
243impl GeometricConv {
244    /// Create a new geometric convolution layer
245    pub fn new(
246        in_features: usize,
247        out_features: usize,
248        hidden_dim: usize,
249        use_bias: bool,
250    ) -> Result<Self> {
251        // MLP layers for message generation
252        let message_layer1 = Parameter::new(randn(&[in_features * 2 + 1, hidden_dim])?);
253        let message_layer2 = Parameter::new(randn(&[hidden_dim, hidden_dim])?);
254
255        let distance_encoder = Parameter::new(randn(&[1, hidden_dim])?);
256        let output_weight = Parameter::new(randn(&[hidden_dim, out_features])?);
257
258        let bias = if use_bias {
259            Some(Parameter::new(zeros(&[out_features])?))
260        } else {
261            None
262        };
263
264        Ok(Self {
265            in_features,
266            out_features,
267            hidden_dim,
268            message_mlp: vec![message_layer1, message_layer2],
269            distance_encoder,
270            output_weight,
271            bias,
272        })
273    }
274
275    /// Forward pass through geometric convolution
276    pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
277        let num_nodes = graph.num_nodes;
278        let num_edges = graph.num_edges;
279
280        // Get edge distances if available
281        let edge_distances = if let Some(ref edge_attr) = graph.edge_attr {
282            edge_attr.to_vec()?
283        } else {
284            vec![1.0; num_edges]
285        };
286
287        // Aggregate messages
288        let edge_data = graph.edge_index.to_vec()?;
289        let mut aggregated = vec![0.0; num_nodes * self.hidden_dim];
290
291        let node_features = graph.x.to_vec()?;
292
293        for edge_idx in 0..num_edges {
294            let src = edge_data[edge_idx * 2] as usize;
295            let dst = edge_data[edge_idx * 2 + 1] as usize;
296
297            if src >= num_nodes || dst >= num_nodes {
298                continue;
299            }
300
301            // Get source and destination features
302            let src_features = &node_features[src * self.in_features..(src + 1) * self.in_features];
303            let dst_features = &node_features[dst * self.in_features..(dst + 1) * self.in_features];
304
305            // Distance encoding
306            let dist = edge_distances[edge_idx.min(edge_distances.len() - 1)];
307
308            // Concatenate features and distance
309            let mut message_input = Vec::new();
310            message_input.extend_from_slice(src_features);
311            message_input.extend_from_slice(dst_features);
312            message_input.push(dist);
313
314            // Compute message through MLP (simplified)
315            let message = self.compute_message(&message_input)?;
316
317            // Aggregate to destination node
318            for (i, &val) in message.iter().enumerate() {
319                aggregated[dst * self.hidden_dim + i] += val;
320            }
321        }
322
323        // Apply output projection
324        let mut output_features = vec![0.0; num_nodes * self.out_features];
325
326        for node in 0..num_nodes {
327            let agg_features = &aggregated[node * self.hidden_dim..(node + 1) * self.hidden_dim];
328            let output_proj = self.output_weight.clone_data().to_vec()?;
329
330            for out_idx in 0..self.out_features {
331                let mut sum = 0.0;
332                for hid_idx in 0..self.hidden_dim {
333                    sum +=
334                        agg_features[hid_idx] * output_proj[hid_idx * self.out_features + out_idx];
335                }
336
337                if let Some(ref bias) = self.bias {
338                    let bias_data = bias.clone_data().to_vec()?;
339                    if out_idx < bias_data.len() {
340                        sum += bias_data[out_idx];
341                    }
342                }
343
344                output_features[node * self.out_features + out_idx] = sum;
345            }
346        }
347
348        let output = from_vec(
349            output_features,
350            &[num_nodes, self.out_features],
351            torsh_core::device::DeviceType::Cpu,
352        )?;
353
354        let mut output_graph = graph.clone();
355        output_graph.x = output;
356        Ok(output_graph)
357    }
358
359    /// Compute message from concatenated features and distance
360    fn compute_message(&self, input: &[f32]) -> Result<Vec<f32>> {
361        // Layer 1
362        let layer1_weights = self.message_mlp[0].clone_data().to_vec()?;
363        let input_dim = self.in_features * 2 + 1;
364        let mut hidden = vec![0.0; self.hidden_dim];
365
366        for h in 0..self.hidden_dim {
367            let mut sum = 0.0;
368            for i in 0..input_dim.min(input.len()) {
369                sum += input[i] * layer1_weights[i * self.hidden_dim + h];
370            }
371            hidden[h] = sum.max(0.0); // ReLU
372        }
373
374        // Layer 2
375        let layer2_weights = self.message_mlp[1].clone_data().to_vec()?;
376        let mut output = vec![0.0; self.hidden_dim];
377
378        for h in 0..self.hidden_dim {
379            let mut sum = 0.0;
380            for i in 0..self.hidden_dim {
381                sum += hidden[i] * layer2_weights[i * self.hidden_dim + h];
382            }
383            output[h] = sum.max(0.0); // ReLU
384        }
385
386        Ok(output)
387    }
388}
389
390impl GraphLayer for GeometricConv {
391    fn forward(&self, graph: &GraphData) -> Result<GraphData> {
392        self.forward(graph)
393    }
394
395    fn parameters(&self) -> Vec<Tensor> {
396        let mut params = Vec::new();
397
398        for layer in &self.message_mlp {
399            params.push(layer.clone_data());
400        }
401
402        params.push(self.distance_encoder.clone_data());
403        params.push(self.output_weight.clone_data());
404
405        if let Some(ref bias) = self.bias {
406            params.push(bias.clone_data());
407        }
408
409        params
410    }
411}
412
413/// Geometric transformations for point clouds and graphs
414pub struct GeometricTransformer;
415
416impl GeometricTransformer {
417    /// Apply rotation to point cloud
418    pub fn rotate_3d(points: &mut [Point3D], axis: &Point3D, angle: f32) {
419        let cos_theta = angle.cos();
420        let sin_theta = angle.sin();
421
422        // Normalize axis
423        let norm = axis.norm();
424        if norm == 0.0 {
425            return;
426        }
427
428        let ux = axis.x / norm;
429        let uy = axis.y / norm;
430        let uz = axis.z / norm;
431
432        // Rotation matrix (Rodrigues' rotation formula)
433        for point in points.iter_mut() {
434            let x = point.x;
435            let y = point.y;
436            let z = point.z;
437
438            // Dot product with axis
439            let dot = ux * x + uy * y + uz * z;
440
441            // Cross product with axis
442            let cross_x = uy * z - uz * y;
443            let cross_y = uz * x - ux * z;
444            let cross_z = ux * y - uy * x;
445
446            // Apply rotation
447            point.x = x * cos_theta + cross_x * sin_theta + ux * dot * (1.0 - cos_theta);
448            point.y = y * cos_theta + cross_y * sin_theta + uy * dot * (1.0 - cos_theta);
449            point.z = z * cos_theta + cross_z * sin_theta + uz * dot * (1.0 - cos_theta);
450        }
451    }
452
453    /// Apply translation to point cloud
454    pub fn translate_3d(points: &mut [Point3D], offset: &Point3D) {
455        for point in points.iter_mut() {
456            point.x += offset.x;
457            point.y += offset.y;
458            point.z += offset.z;
459        }
460    }
461
462    /// Apply scaling to point cloud
463    pub fn scale_3d(points: &mut [Point3D], scale: f32) {
464        for point in points.iter_mut() {
465            point.x *= scale;
466            point.y *= scale;
467            point.z *= scale;
468        }
469    }
470
471    /// Normalize point cloud to unit sphere
472    pub fn normalize_to_unit_sphere(points: &mut [Point3D]) {
473        if points.is_empty() {
474            return;
475        }
476
477        // Find center
478        let mut center = Point3D::new(0.0, 0.0, 0.0);
479        for point in points.iter() {
480            center.x += point.x;
481            center.y += point.y;
482            center.z += point.z;
483        }
484        center.x /= points.len() as f32;
485        center.y /= points.len() as f32;
486        center.z /= points.len() as f32;
487
488        // Translate to origin
489        Self::translate_3d(points, &Point3D::new(-center.x, -center.y, -center.z));
490
491        // Find max distance
492        let max_dist = points
493            .iter()
494            .map(|p| p.norm())
495            .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal))
496            .unwrap_or(1.0);
497
498        // Scale to unit sphere
499        if max_dist > 0.0 {
500            Self::scale_3d(points, 1.0 / max_dist);
501        }
502    }
503}
504
505/// Geometric pooling operations
506pub struct GeometricPooling;
507
508impl GeometricPooling {
509    /// Voxel-based pooling (divide space into voxels and pool within each)
510    pub fn voxel_pool(
511        points: &[Point3D],
512        features: &Tensor,
513        voxel_size: f32,
514    ) -> Result<(Vec<Point3D>, Tensor)> {
515        let feature_data = features.to_vec()?;
516        let feature_dim = features.shape().dims()[1];
517
518        // Compute voxel indices
519        let mut voxel_map: HashMap<(i32, i32, i32), Vec<usize>> = HashMap::new();
520
521        for (i, point) in points.iter().enumerate() {
522            let vx = (point.x / voxel_size).floor() as i32;
523            let vy = (point.y / voxel_size).floor() as i32;
524            let vz = (point.z / voxel_size).floor() as i32;
525
526            voxel_map
527                .entry((vx, vy, vz))
528                .or_insert_with(Vec::new)
529                .push(i);
530        }
531
532        // Pool points and features within each voxel
533        let mut pooled_points = Vec::new();
534        let mut pooled_features = Vec::new();
535
536        for (_voxel, indices) in voxel_map {
537            if indices.is_empty() {
538                continue;
539            }
540
541            // Average position
542            let mut avg_point = Point3D::new(0.0, 0.0, 0.0);
543            for &idx in &indices {
544                avg_point.x += points[idx].x;
545                avg_point.y += points[idx].y;
546                avg_point.z += points[idx].z;
547            }
548            avg_point.x /= indices.len() as f32;
549            avg_point.y /= indices.len() as f32;
550            avg_point.z /= indices.len() as f32;
551
552            pooled_points.push(avg_point);
553
554            // Average features
555            let mut avg_features = vec![0.0; feature_dim];
556            for &idx in &indices {
557                for d in 0..feature_dim {
558                    avg_features[d] += feature_data[idx * feature_dim + d];
559                }
560            }
561            for val in &mut avg_features {
562                *val /= indices.len() as f32;
563            }
564
565            pooled_features.extend(avg_features);
566        }
567
568        let pooled_tensor = from_vec(
569            pooled_features,
570            &[pooled_points.len(), feature_dim],
571            torsh_core::device::DeviceType::Cpu,
572        )?;
573
574        Ok((pooled_points, pooled_tensor))
575    }
576
577    /// Farthest point sampling
578    pub fn farthest_point_sampling(
579        points: &[Point3D],
580        features: &Tensor,
581        num_samples: usize,
582    ) -> Result<(Vec<Point3D>, Tensor)> {
583        let num_points = points.len();
584        let feature_dim = features.shape().dims()[1];
585        let feature_data = features.to_vec()?;
586
587        if num_samples >= num_points {
588            return Ok((points.to_vec(), features.clone()));
589        }
590
591        let mut selected = Vec::new();
592        let mut distances = vec![f32::MAX; num_points];
593
594        // Start with random point
595        let mut rng = thread_rng();
596        let first_idx = rng.gen_range(0..num_points);
597        selected.push(first_idx);
598
599        // Update distances
600        for i in 0..num_points {
601            distances[i] = points[i].distance(&points[first_idx]);
602        }
603
604        // Iteratively select farthest point
605        for _ in 1..num_samples {
606            let farthest_idx = distances
607                .iter()
608                .enumerate()
609                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(Ordering::Equal))
610                .map(|(idx, _)| idx)
611                .unwrap_or(0);
612
613            selected.push(farthest_idx);
614
615            // Update distances
616            for i in 0..num_points {
617                let dist = points[i].distance(&points[farthest_idx]);
618                distances[i] = distances[i].min(dist);
619            }
620        }
621
622        // Extract selected points and features
623        let sampled_points: Vec<_> = selected.iter().map(|&idx| points[idx]).collect();
624        let sampled_features: Vec<_> = selected
625            .iter()
626            .flat_map(|&idx| {
627                let start = idx * feature_dim;
628                let end = start + feature_dim;
629                &feature_data[start..end]
630            })
631            .copied()
632            .collect();
633
634        let sampled_tensor = from_vec(
635            sampled_features,
636            &[num_samples, feature_dim],
637            torsh_core::device::DeviceType::Cpu,
638        )?;
639
640        Ok((sampled_points, sampled_tensor))
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use super::*;
647
648    #[test]
649    fn test_point3d_distance() {
650        let p1 = Point3D::new(0.0, 0.0, 0.0);
651        let p2 = Point3D::new(3.0, 4.0, 0.0);
652
653        assert!((p1.distance(&p2) - 5.0).abs() < 1e-5);
654    }
655
656    #[test]
657    fn test_knn_graph() {
658        let points = vec![
659            Point3D::new(0.0, 0.0, 0.0),
660            Point3D::new(1.0, 0.0, 0.0),
661            Point3D::new(0.0, 1.0, 0.0),
662            Point3D::new(1.0, 1.0, 0.0),
663        ];
664
665        let graph =
666            GeometricGraphBuilder::knn_graph(&points, 2, None).expect("operation should succeed");
667
668        assert_eq!(graph.num_nodes, 4);
669        assert_eq!(graph.x.shape().dims()[1], 3); // 3D coordinates
670        assert!(graph.edge_attr.is_some());
671    }
672
673    #[test]
674    fn test_radius_graph() {
675        let points = vec![
676            Point3D::new(0.0, 0.0, 0.0),
677            Point3D::new(0.5, 0.0, 0.0),
678            Point3D::new(2.0, 0.0, 0.0),
679        ];
680
681        let graph = GeometricGraphBuilder::radius_graph(&points, 1.0, None)
682            .expect("operation should succeed");
683
684        assert_eq!(graph.num_nodes, 3);
685        assert!(graph.num_edges >= 2); // At least points 0 and 1 connected
686    }
687
688    #[test]
689    fn test_geometric_conv() {
690        let points = vec![
691            Point3D::new(0.0, 0.0, 0.0),
692            Point3D::new(1.0, 0.0, 0.0),
693            Point3D::new(0.0, 1.0, 0.0),
694        ];
695
696        let graph =
697            GeometricGraphBuilder::knn_graph(&points, 2, None).expect("operation should succeed");
698        let conv = GeometricConv::new(3, 6, 8, true).expect("operation should succeed");
699
700        let output = conv.forward(&graph).expect("operation should succeed");
701
702        assert_eq!(output.num_nodes, 3);
703        assert_eq!(output.x.shape().dims()[1], 6);
704    }
705
706    #[test]
707    fn test_geometric_rotation() {
708        let mut points = vec![Point3D::new(1.0, 0.0, 0.0)];
709
710        let axis = Point3D::new(0.0, 0.0, 1.0);
711        let angle = std::f32::consts::PI / 2.0;
712
713        GeometricTransformer::rotate_3d(&mut points, &axis, angle);
714
715        // After 90 degree rotation around Z-axis, (1,0,0) -> (0,1,0)
716        assert!((points[0].x - 0.0).abs() < 1e-5);
717        assert!((points[0].y - 1.0).abs() < 1e-5);
718    }
719
720    #[test]
721    fn test_normalize_to_unit_sphere() {
722        let mut points = vec![
723            Point3D::new(2.0, 0.0, 0.0),
724            Point3D::new(0.0, 2.0, 0.0),
725            Point3D::new(0.0, 0.0, 2.0),
726        ];
727
728        GeometricTransformer::normalize_to_unit_sphere(&mut points);
729
730        // All points should be within unit sphere
731        for point in &points {
732            assert!(point.norm() <= 1.0 + 1e-5);
733        }
734    }
735
736    #[test]
737    fn test_voxel_pooling() {
738        let points = vec![
739            Point3D::new(0.1, 0.1, 0.1),
740            Point3D::new(0.2, 0.2, 0.2),
741            Point3D::new(1.1, 1.1, 1.1),
742        ];
743
744        let features = randn(&[3, 4]).unwrap();
745
746        let (pooled_points, pooled_features) =
747            GeometricPooling::voxel_pool(&points, &features, 1.0)
748                .expect("operation should succeed");
749
750        assert!(pooled_points.len() <= 3);
751        assert_eq!(pooled_features.shape().dims()[1], 4);
752    }
753
754    #[test]
755    fn test_farthest_point_sampling() {
756        let points = vec![
757            Point3D::new(0.0, 0.0, 0.0),
758            Point3D::new(1.0, 0.0, 0.0),
759            Point3D::new(0.0, 1.0, 0.0),
760            Point3D::new(0.0, 0.0, 1.0),
761        ];
762
763        let features = randn(&[4, 3]).unwrap();
764
765        let (sampled_points, sampled_features) =
766            GeometricPooling::farthest_point_sampling(&points, &features, 2)
767                .expect("operation should succeed");
768
769        assert_eq!(sampled_points.len(), 2);
770        assert_eq!(sampled_features.shape().dims(), &[2, 3]);
771    }
772}