1use crate::GpuError;
13use amari_network::{Community, GeometricNetwork};
14use bytemuck::{Pod, Zeroable};
15use futures::channel::oneshot;
16use thiserror::Error;
17use wgpu::util::DeviceExt;
18
19#[derive(Error, Debug)]
20pub enum GpuNetworkError {
21 #[error("GPU error: {0}")]
22 Gpu(#[from] GpuError),
23
24 #[error("Network error: {0}")]
25 Network(#[from] amari_network::NetworkError),
26
27 #[error("Invalid network size: {0}")]
28 InvalidSize(usize),
29
30 #[error("Unsupported GPU embedding: {0}")]
31 UnsupportedEmbedding(String),
32
33 #[error("Invalid position: {0}")]
34 InvalidPosition(String),
35
36 #[error("Buffer error: {0}")]
37 BufferError(String),
38}
39
40pub type GpuNetworkResult<T> = Result<T, GpuNetworkError>;
41
42pub struct GpuGeometricNetwork {
44 device: wgpu::Device,
45 queue: wgpu::Queue,
46 distance_pipeline: wgpu::ComputePipeline,
47 #[allow(dead_code)]
48 centrality_pipeline: wgpu::ComputePipeline,
49 #[allow(dead_code)]
50 clustering_pipeline: wgpu::ComputePipeline,
51}
52
53#[repr(C)]
55#[derive(Copy, Clone, Pod, Zeroable)]
56struct GpuNodePosition {
57 x: f32,
58 y: f32,
59 z: f32,
60 padding: f32, }
62
63#[repr(C)]
65#[derive(Copy, Clone, Pod, Zeroable)]
66struct GpuEdgeData {
67 source: u32,
68 target: u32,
69 weight: f32,
70 padding: f32,
71}
72
73impl GpuGeometricNetwork {
74 pub async fn new() -> GpuNetworkResult<Self> {
76 let instance = wgpu::Instance::default();
77
78 let adapter = instance
79 .request_adapter(&wgpu::RequestAdapterOptions {
80 power_preference: wgpu::PowerPreference::HighPerformance,
81 compatible_surface: None,
82 force_fallback_adapter: false,
83 })
84 .await
85 .ok_or_else(|| GpuError::InitializationError("No GPU adapter found".to_string()))?;
86
87 let (device, queue) = adapter
88 .request_device(
89 &wgpu::DeviceDescriptor {
90 label: Some("Amari Network GPU Device"),
91 required_features: wgpu::Features::empty(),
92 required_limits: wgpu::Limits::default(),
93 },
94 None,
95 )
96 .await
97 .map_err(|e| GpuError::InitializationError(e.to_string()))?;
98
99 let distance_pipeline = Self::create_distance_pipeline(&device)?;
100 let centrality_pipeline = Self::create_centrality_pipeline(&device)?;
101 let clustering_pipeline = Self::create_clustering_pipeline(&device)?;
102
103 Ok(Self {
104 device,
105 queue,
106 distance_pipeline,
107 centrality_pipeline,
108 clustering_pipeline,
109 })
110 }
111
112 pub async fn compute_all_pairwise_distances<const P: usize, const Q: usize, const R: usize>(
118 &self,
119 network: &GeometricNetwork<P, Q, R>,
120 ) -> GpuNetworkResult<Vec<Vec<f64>>> {
121 let num_nodes = network.num_nodes();
122 if num_nodes == 0 {
123 return Ok(Vec::new());
124 }
125 self.validate_gpu_distance_network(network)?;
126
127 let gpu_positions: Vec<GpuNodePosition> = (0..num_nodes)
129 .map(|i| {
130 let pos = network.get_node(i).unwrap();
131 GpuNodePosition {
132 x: pos.vector_component(0) as f32,
133 y: pos.vector_component(1) as f32,
134 z: pos.vector_component(2) as f32,
135 padding: 0.0,
136 }
137 })
138 .collect();
139
140 let positions_buffer = self
142 .device
143 .create_buffer_init(&wgpu::util::BufferInitDescriptor {
144 label: Some("Node Positions"),
145 contents: bytemuck::cast_slice(&gpu_positions),
146 usage: wgpu::BufferUsages::STORAGE,
147 });
148
149 let output_size = num_nodes * num_nodes * 4; let output_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
151 label: Some("Distance Output"),
152 size: output_size as u64,
153 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
154 mapped_at_creation: false,
155 });
156
157 let staging_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
158 label: Some("Distance Staging"),
159 size: output_size as u64,
160 usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
161 mapped_at_creation: false,
162 });
163
164 let bind_group_layout = self.distance_pipeline.get_bind_group_layout(0);
166 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
167 label: Some("Distance Compute Bind Group"),
168 layout: &bind_group_layout,
169 entries: &[
170 wgpu::BindGroupEntry {
171 binding: 0,
172 resource: positions_buffer.as_entire_binding(),
173 },
174 wgpu::BindGroupEntry {
175 binding: 1,
176 resource: output_buffer.as_entire_binding(),
177 },
178 ],
179 });
180
181 let mut encoder = self
183 .device
184 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
185 label: Some("Distance Compute Encoder"),
186 });
187
188 {
189 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
190 label: Some("Distance Compute Pass"),
191 timestamp_writes: None,
192 });
193 compute_pass.set_pipeline(&self.distance_pipeline);
194 compute_pass.set_bind_group(0, &bind_group, &[]);
195 let workgroup_count = num_nodes.div_ceil(8);
196 compute_pass.dispatch_workgroups(workgroup_count as u32, workgroup_count as u32, 1);
197 }
198
199 encoder.copy_buffer_to_buffer(&output_buffer, 0, &staging_buffer, 0, output_size as u64);
200
201 self.queue.submit(std::iter::once(encoder.finish()));
202
203 let buffer_slice = staging_buffer.slice(..);
205 let (sender, receiver) = oneshot::channel();
206 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
207 let _ = sender.send(result);
208 });
209
210 self.device.poll(wgpu::Maintain::Wait);
211
212 receiver
213 .await
214 .map_err(|_| {
215 GpuNetworkError::BufferError("Failed to receive buffer mapping".to_string())
216 })?
217 .map_err(|e| GpuNetworkError::BufferError(format!("Buffer mapping failed: {:?}", e)))?;
218
219 let data = buffer_slice.get_mapped_range();
220 let result_f32: &[f32] = bytemuck::cast_slice(&data);
221
222 let mut distances = vec![vec![0.0; num_nodes]; num_nodes];
224 for i in 0..num_nodes {
225 for j in 0..num_nodes {
226 distances[i][j] = result_f32[i * num_nodes + j] as f64;
227 }
228 }
229
230 drop(data);
231 staging_buffer.unmap();
232
233 Ok(distances)
234 }
235
236 pub async fn compute_geometric_centrality<const P: usize, const Q: usize, const R: usize>(
238 &self,
239 network: &GeometricNetwork<P, Q, R>,
240 ) -> GpuNetworkResult<Vec<f64>> {
241 let distances = self.compute_all_pairwise_distances(network).await?;
243
244 let num_nodes = network.num_nodes();
247 let mut centrality = vec![0.0; num_nodes];
248
249 for i in 0..num_nodes {
250 let total_distance: f64 = distances[i].iter().sum();
251 centrality[i] = if total_distance > 0.0 {
252 (num_nodes as f64 - 1.0) / total_distance
253 } else {
254 0.0
255 };
256 }
257
258 Ok(centrality)
259 }
260
261 pub async fn geometric_clustering<const P: usize, const Q: usize, const R: usize>(
266 &self,
267 network: &GeometricNetwork<P, Q, R>,
268 k: usize,
269 max_iterations: usize,
270 ) -> GpuNetworkResult<Vec<Community<P, Q, R>>> {
271 let num_nodes = network.num_nodes();
272 if k > num_nodes || k == 0 {
273 return Err(GpuNetworkError::InvalidSize(k));
274 }
275 if max_iterations == 0 {
276 return Err(GpuNetworkError::InvalidSize(max_iterations));
277 }
278
279 let distances = self.compute_all_pairwise_distances(network).await?;
281
282 let mut centroids = Vec::with_capacity(k);
284 for i in 0..k {
285 let centroid_idx = (i * num_nodes) / k;
286 centroids.push(centroid_idx);
287 }
288
289 let mut assignments = vec![0; num_nodes];
290
291 for _iteration in 0..max_iterations {
292 let mut changed = false;
293
294 for node in 0..num_nodes {
296 let mut best_cluster = 0;
297 let mut best_distance = f64::INFINITY;
298
299 for (cluster, ¢roid) in centroids.iter().enumerate().take(k) {
300 let distance = distances[node][centroid];
301
302 if distance < best_distance {
303 best_distance = distance;
304 best_cluster = cluster;
305 }
306 }
307
308 if assignments[node] != best_cluster {
309 assignments[node] = best_cluster;
310 changed = true;
311 }
312 }
313
314 if !changed {
315 break;
316 }
317
318 for (cluster, centroid) in centroids.iter_mut().enumerate().take(k) {
320 let cluster_nodes: Vec<usize> = assignments
321 .iter()
322 .enumerate()
323 .filter(|(_, &c)| c == cluster)
324 .map(|(node, _)| node)
325 .collect();
326
327 if !cluster_nodes.is_empty() {
328 let mut best_medoid = cluster_nodes[0];
329 let mut best_total_distance = f64::INFINITY;
330
331 for &candidate in &cluster_nodes {
332 let total_distance: f64 = cluster_nodes
333 .iter()
334 .map(|&other| distances[candidate][other])
335 .sum();
336
337 if total_distance < best_total_distance {
338 best_total_distance = total_distance;
339 best_medoid = candidate;
340 }
341 }
342
343 *centroid = best_medoid;
344 }
345 }
346 }
347
348 let mut communities = Vec::with_capacity(k);
350 for (cluster, ¢roid) in centroids.iter().enumerate().take(k) {
351 let nodes: Vec<usize> = assignments
352 .iter()
353 .enumerate()
354 .filter(|(_, &c)| c == cluster)
355 .map(|(node, _)| node)
356 .collect();
357
358 if !nodes.is_empty() {
359 let centroid_pos = network.get_node(centroid).unwrap().clone();
360 let cohesion_score = Self::compute_cluster_cohesion(&nodes, &distances);
361 communities.push(Community {
362 nodes,
363 geometric_centroid: centroid_pos,
364 cohesion_score,
365 });
366 }
367 }
368
369 Ok(communities)
370 }
371
372 pub fn should_use_gpu(num_nodes: usize) -> bool {
374 num_nodes >= 100
376 }
377
378 pub fn supports_gpu_distance<const P: usize, const Q: usize, const R: usize>() -> bool {
380 Q == 0 && R == 0 && P <= 3
381 }
382
383 fn validate_gpu_distance_network<const P: usize, const Q: usize, const R: usize>(
386 &self,
387 network: &GeometricNetwork<P, Q, R>,
388 ) -> GpuNetworkResult<()> {
389 if !Self::supports_gpu_distance::<P, Q, R>() {
390 return Err(GpuNetworkError::UnsupportedEmbedding(format!(
391 "GPU network distance supports vector-only Cl(P,0,0) with P <= 3; got Cl({P},{Q},{R})"
392 )));
393 }
394
395 for node_idx in 0..network.num_nodes() {
396 let node = network.get_node(node_idx).ok_or_else(|| {
397 GpuNetworkError::InvalidPosition(format!("node {node_idx} is missing"))
398 })?;
399 for (coeff_idx, &coeff) in node.as_slice().iter().enumerate() {
400 if !coeff.is_finite() {
401 return Err(GpuNetworkError::InvalidPosition(format!(
402 "node {node_idx} coefficient {coeff_idx} is not finite"
403 )));
404 }
405 let is_vector_component = coeff_idx.is_power_of_two()
406 && coeff_idx > 0
407 && coeff_idx.trailing_zeros() < P as u32;
408 if !is_vector_component && coeff.abs() > 1e-12 {
409 return Err(GpuNetworkError::UnsupportedEmbedding(format!(
410 "node {node_idx} has non-vector coefficient at blade {coeff_idx}"
411 )));
412 }
413 }
414 }
415
416 Ok(())
417 }
418
419 fn compute_cluster_cohesion(nodes: &[usize], distances: &[Vec<f64>]) -> f64 {
420 if nodes.len() <= 1 {
421 return 1.0;
422 }
423
424 let mut total = 0.0;
425 let mut count = 0usize;
426 for (idx, &a) in nodes.iter().enumerate() {
427 for &b in nodes.iter().skip(idx + 1) {
428 total += distances[a][b];
429 count += 1;
430 }
431 }
432
433 if count == 0 {
434 1.0
435 } else {
436 1.0 / (1.0 + total / count as f64)
437 }
438 }
439
440 fn create_distance_pipeline(device: &wgpu::Device) -> Result<wgpu::ComputePipeline, GpuError> {
441 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
442 label: Some("Distance Compute Shader"),
443 source: wgpu::ShaderSource::Wgsl(DISTANCE_COMPUTE_SHADER.into()),
444 });
445
446 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
447 label: Some("Distance Compute Pipeline"),
448 layout: None,
449 module: &shader,
450 entry_point: "main",
451 });
452
453 Ok(pipeline)
454 }
455
456 fn create_centrality_pipeline(
457 device: &wgpu::Device,
458 ) -> Result<wgpu::ComputePipeline, GpuError> {
459 Self::create_distance_pipeline(device)
461 }
462
463 fn create_clustering_pipeline(
464 device: &wgpu::Device,
465 ) -> Result<wgpu::ComputePipeline, GpuError> {
466 Self::create_distance_pipeline(device)
468 }
469}
470
471const DISTANCE_COMPUTE_SHADER: &str = r#"
473struct NodePosition {
474 x: f32,
475 y: f32,
476 z: f32,
477 padding: f32,
478}
479
480@group(0) @binding(0)
481var<storage, read> positions: array<NodePosition>;
482
483@group(0) @binding(1)
484var<storage, read_write> distances: array<f32>;
485
486@compute @workgroup_size(8, 8)
487fn main(@builtin(global_invocation_id) global_id: vec3<u32>) {
488 let i = global_id.x;
489 let j = global_id.y;
490 let num_nodes = arrayLength(&positions);
491
492 if (i >= num_nodes || j >= num_nodes) {
493 return;
494 }
495
496 let idx = i * num_nodes + j;
497
498 if (i == j) {
499 distances[idx] = 0.0;
500 return;
501 }
502
503 let pos_i = positions[i];
504 let pos_j = positions[j];
505
506 let dx = pos_i.x - pos_j.x;
507 let dy = pos_i.y - pos_j.y;
508 let dz = pos_i.z - pos_j.z;
509
510 let distance = sqrt(dx * dx + dy * dy + dz * dz);
511 distances[idx] = distance;
512}
513"#;
514
515pub struct AdaptiveNetworkCompute {
517 gpu: Option<GpuGeometricNetwork>,
518}
519
520impl AdaptiveNetworkCompute {
521 fn compute_pairwise_geometric_distances_cpu<const P: usize, const Q: usize, const R: usize>(
522 network: &GeometricNetwork<P, Q, R>,
523 ) -> GpuNetworkResult<Vec<Vec<f64>>> {
524 let num_nodes = network.num_nodes();
525 let mut distances = vec![vec![0.0; num_nodes]; num_nodes];
526
527 for (i, row) in distances.iter_mut().enumerate() {
528 for (j, distance) in row.iter_mut().enumerate() {
529 *distance = network.geometric_distance(i, j)?;
530 }
531 }
532
533 Ok(distances)
534 }
535
536 pub async fn new() -> Self {
538 let gpu = {
540 let panic_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
541 pollster::block_on(async { GpuGeometricNetwork::new().await.ok() })
542 }));
543
544 panic_result.unwrap_or_default()
546 };
547
548 Self { gpu }
549 }
550
551 pub async fn compute_all_pairwise_distances<const P: usize, const Q: usize, const R: usize>(
553 &self,
554 network: &GeometricNetwork<P, Q, R>,
555 ) -> GpuNetworkResult<Vec<Vec<f64>>> {
556 let num_nodes = network.num_nodes();
557
558 if let Some(gpu) = &self.gpu {
559 if GpuGeometricNetwork::should_use_gpu(num_nodes)
560 && GpuGeometricNetwork::supports_gpu_distance::<P, Q, R>()
561 {
562 if let Ok(distances) = gpu.compute_all_pairwise_distances(network).await {
563 return Ok(distances);
564 }
565 }
566 }
567
568 Self::compute_pairwise_geometric_distances_cpu(network)
571 }
572
573 pub async fn compute_geometric_centrality<const P: usize, const Q: usize, const R: usize>(
575 &self,
576 network: &GeometricNetwork<P, Q, R>,
577 ) -> GpuNetworkResult<Vec<f64>> {
578 let num_nodes = network.num_nodes();
579
580 if let Some(gpu) = &self.gpu {
581 if GpuGeometricNetwork::should_use_gpu(num_nodes)
582 && GpuGeometricNetwork::supports_gpu_distance::<P, Q, R>()
583 {
584 if let Ok(centrality) = gpu.compute_geometric_centrality(network).await {
585 return Ok(centrality);
586 }
587 }
588 }
589
590 network
592 .compute_geometric_centrality()
593 .map_err(GpuNetworkError::Network)
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600
601 #[test]
602 fn test_should_use_gpu() {
603 assert!(!GpuGeometricNetwork::should_use_gpu(10));
604 assert!(GpuGeometricNetwork::should_use_gpu(1000));
605 }
606
607 #[tokio::test]
608 async fn test_adaptive_network_creation() {
609 let adaptive = AdaptiveNetworkCompute::new().await;
611
612 match &adaptive.gpu {
614 Some(_) => {
615 println!("✅ GPU network acceleration available");
616 }
617 None => {
618 println!("✅ GPU not available, using CPU fallback for network operations");
619 }
620 }
621
622 }
625}