1type 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#[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
55pub struct GeometricGraphBuilder;
57
58impl GeometricGraphBuilder {
59 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 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 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 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 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 pub fn delaunay_graph_2d(points: &[(f32, f32)], features: Option<Tensor>) -> Result<GraphData> {
170 let num_points = points.len();
171
172 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 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#[derive(Debug)]
226pub struct GeometricConv {
227 in_features: usize,
228 out_features: usize,
229 hidden_dim: usize,
230
231 message_mlp: Vec<Parameter>,
233
234 distance_encoder: Parameter,
236
237 output_weight: Parameter,
239
240 bias: Option<Parameter>,
241}
242
243impl GeometricConv {
244 pub fn new(
246 in_features: usize,
247 out_features: usize,
248 hidden_dim: usize,
249 use_bias: bool,
250 ) -> Result<Self> {
251 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 pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
277 let num_nodes = graph.num_nodes;
278 let num_edges = graph.num_edges;
279
280 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 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 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 let dist = edge_distances[edge_idx.min(edge_distances.len() - 1)];
307
308 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 let message = self.compute_message(&message_input)?;
316
317 for (i, &val) in message.iter().enumerate() {
319 aggregated[dst * self.hidden_dim + i] += val;
320 }
321 }
322
323 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 fn compute_message(&self, input: &[f32]) -> Result<Vec<f32>> {
361 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); }
373
374 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); }
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
413pub struct GeometricTransformer;
415
416impl GeometricTransformer {
417 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 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 for point in points.iter_mut() {
434 let x = point.x;
435 let y = point.y;
436 let z = point.z;
437
438 let dot = ux * x + uy * y + uz * z;
440
441 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 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 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 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 pub fn normalize_to_unit_sphere(points: &mut [Point3D]) {
473 if points.is_empty() {
474 return;
475 }
476
477 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 Self::translate_3d(points, &Point3D::new(-center.x, -center.y, -center.z));
490
491 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 if max_dist > 0.0 {
500 Self::scale_3d(points, 1.0 / max_dist);
501 }
502 }
503}
504
505pub struct GeometricPooling;
507
508impl GeometricPooling {
509 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 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 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 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 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 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 let mut rng = thread_rng();
596 let first_idx = rng.gen_range(0..num_points);
597 selected.push(first_idx);
598
599 for i in 0..num_points {
601 distances[i] = points[i].distance(&points[first_idx]);
602 }
603
604 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 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 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); 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); }
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 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 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}