amari_network/
lib.rs

1//! # Geometric Network Analysis
2//!
3//! This crate provides graph/network analysis tools where nodes are embedded
4//! in Clifford algebra (geometric algebra) space. This enables:
5//!
6//! - Geometric distance metrics between nodes
7//! - Community detection via geometric clustering
8//! - Information diffusion using geometric products
9//! - Fast path-finding with tropical algebra
10//!
11//! ## Mathematical Foundation
12//!
13//! Nodes are represented as multivectors in Cl(P,Q,R), the Clifford algebra
14//! with signature (P,Q,R). The geometric distance between nodes uses the
15//! natural norm in this space: `||a - b||` where `a` and `b` are node positions.
16//!
17//! ## Basic Usage
18//!
19//! ```rust
20//! use amari_network::GeometricNetwork;
21//! use amari_core::Vector;
22//!
23//! // Create a network in 3D Euclidean space (signature 3,0,0)
24//! let mut network = GeometricNetwork::<3, 0, 0>::new();
25//!
26//! // Add nodes at specific geometric positions
27//! let node1 = network.add_node(Vector::from_components(1.0, 0.0, 0.0).mv);
28//! let node2 = network.add_node(Vector::from_components(0.0, 1.0, 0.0).mv);
29//!
30//! // Connect nodes with weighted edges
31//! network.add_edge(node1, node2, 1.0).unwrap();
32//!
33//! // Compute geometric properties
34//! let distance = network.geometric_distance(node1, node2);
35//! let centrality = network.compute_geometric_centrality();
36//! ```
37//!
38//! ## Features
39//!
40//! - **Geometric Embedding**: Nodes as multivectors in Clifford algebra space
41//! - **Tropical Optimization**: Fast path-finding using tropical (max-plus) algebra
42//! - **Community Detection**: Spectral and geometric clustering methods
43//! - **Diffusion Modeling**: Information propagation via geometric products
44//! - **Centrality Measures**: Geometric, betweenness, and eigenvector centrality
45//!
46//! ## Application Areas
47//!
48//! - Social network analysis with semantic embeddings
49//! - Citation networks with geometric document representations
50//! - Graph neural networks with geometric features
51//! - Epistemic network analysis
52//! - Belief/information propagation modeling
53
54use amari_core::Multivector;
55use std::collections::HashMap;
56
57pub mod error;
58pub mod tropical;
59
60// Formal verification modules (optional)
61#[cfg(feature = "formal-verification")]
62pub mod verified;
63#[cfg(feature = "formal-verification")]
64pub mod verified_contracts;
65
66pub use error::{NetworkError, NetworkResult};
67
68// Re-export formal verification types when feature is enabled
69#[cfg(feature = "formal-verification")]
70pub use verified::{NetworkProperty, NetworkSignature, VerifiedGeometricNetwork};
71#[cfg(feature = "formal-verification")]
72pub use verified_contracts::{
73    GeometricProperties, GraphTheoreticProperties, TropicalProperties,
74    VerifiedContractGeometricNetwork,
75};
76
77/// A network where nodes are embedded in geometric algebra space
78///
79/// Each node is represented as a multivector in Cl(P,Q,R), enabling
80/// geometric operations like distance computation and geometric products
81/// for information diffusion.
82#[derive(Clone, Debug)]
83pub struct GeometricNetwork<const P: usize, const Q: usize, const R: usize> {
84    /// Node positions as multivectors in Cl(P,Q,R)
85    nodes: Vec<Multivector<P, Q, R>>,
86    /// Weighted directed edges
87    edges: Vec<GeometricEdge>,
88    /// Optional metadata for each node
89    node_metadata: HashMap<usize, NodeMetadata>,
90}
91
92/// Edge with geometric properties
93///
94/// Represents a weighted directed edge between two nodes in the network.
95/// The weight can represent strength of connection, similarity, or distance.
96#[derive(Clone, Debug, PartialEq)]
97pub struct GeometricEdge {
98    /// Source node index
99    pub source: usize,
100    /// Target node index
101    pub target: usize,
102    /// Edge weight (strength, similarity, etc.)
103    pub weight: f64,
104}
105
106/// Metadata associated with a network node
107///
108/// Allows attaching labels and properties to nodes for analysis
109/// and visualization purposes.
110#[derive(Clone, Debug, PartialEq)]
111#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
112pub struct NodeMetadata {
113    /// Optional human-readable label
114    pub label: Option<String>,
115    /// Custom numerical properties
116    pub properties: HashMap<String, f64>,
117}
118
119/// Result of community detection analysis
120///
121/// Represents a detected community with its member nodes,
122/// geometric centroid, and cohesion score.
123#[derive(Clone, Debug)]
124pub struct Community<const P: usize, const Q: usize, const R: usize> {
125    /// Node indices belonging to this community
126    pub nodes: Vec<usize>,
127    /// Geometric centroid of community nodes
128    pub geometric_centroid: Multivector<P, Q, R>,
129    /// Cohesion score (higher = more cohesive)
130    pub cohesion_score: f64,
131}
132
133/// Result of information propagation analysis
134///
135/// Tracks how information spreads through the network over time,
136/// measuring coverage, influence, and convergence properties.
137#[derive(Clone, Debug)]
138pub struct PropagationAnalysis {
139    /// Number of nodes reached at each timestep
140    pub coverage: Vec<usize>,
141    /// Influence score for each node (total information transmitted)
142    pub influence_scores: Vec<f64>,
143    /// Number of steps until convergence
144    pub convergence_time: usize,
145}
146
147impl NodeMetadata {
148    /// Create new empty metadata
149    pub fn new() -> Self {
150        Self {
151            label: None,
152            properties: HashMap::new(),
153        }
154    }
155
156    /// Create metadata with label
157    pub fn with_label(label: impl Into<String>) -> Self {
158        Self {
159            label: Some(label.into()),
160            properties: HashMap::new(),
161        }
162    }
163
164    /// Add a numerical property
165    pub fn with_property(mut self, key: impl Into<String>, value: f64) -> Self {
166        self.properties.insert(key.into(), value);
167        self
168    }
169}
170
171impl Default for NodeMetadata {
172    fn default() -> Self {
173        Self::new()
174    }
175}
176
177impl<const P: usize, const Q: usize, const R: usize> GeometricNetwork<P, Q, R> {
178    /// Create a new empty geometric network
179    pub fn new() -> Self {
180        Self {
181            nodes: Vec::new(),
182            edges: Vec::new(),
183            node_metadata: HashMap::new(),
184        }
185    }
186
187    /// Create a network with pre-allocated capacity
188    pub fn with_capacity(num_nodes: usize, num_edges: usize) -> Self {
189        Self {
190            nodes: Vec::with_capacity(num_nodes),
191            edges: Vec::with_capacity(num_edges),
192            node_metadata: HashMap::with_capacity(num_nodes),
193        }
194    }
195
196    /// Add a node at the specified geometric position
197    ///
198    /// Returns the index of the newly added node.
199    pub fn add_node(&mut self, position: Multivector<P, Q, R>) -> usize {
200        let index = self.nodes.len();
201        self.nodes.push(position);
202        index
203    }
204
205    /// Add a node with associated metadata
206    ///
207    /// Returns the index of the newly added node.
208    pub fn add_node_with_metadata(
209        &mut self,
210        position: Multivector<P, Q, R>,
211        metadata: NodeMetadata,
212    ) -> usize {
213        let index = self.add_node(position);
214        self.node_metadata.insert(index, metadata);
215        index
216    }
217
218    /// Add a directed edge between two nodes
219    pub fn add_edge(&mut self, source: usize, target: usize, weight: f64) -> NetworkResult<()> {
220        if source >= self.nodes.len() {
221            return Err(NetworkError::NodeIndexOutOfBounds(source));
222        }
223        if target >= self.nodes.len() {
224            return Err(NetworkError::NodeIndexOutOfBounds(target));
225        }
226
227        self.edges.push(GeometricEdge {
228            source,
229            target,
230            weight,
231        });
232        Ok(())
233    }
234
235    /// Add an undirected edge (creates two directed edges)
236    pub fn add_undirected_edge(&mut self, a: usize, b: usize, weight: f64) -> NetworkResult<()> {
237        self.add_edge(a, b, weight)?;
238        self.add_edge(b, a, weight)?;
239        Ok(())
240    }
241
242    /// Get the number of nodes in the network
243    pub fn num_nodes(&self) -> usize {
244        self.nodes.len()
245    }
246
247    /// Get the number of edges in the network
248    pub fn num_edges(&self) -> usize {
249        self.edges.len()
250    }
251
252    /// Get a reference to a node's position
253    pub fn get_node(&self, index: usize) -> Option<&Multivector<P, Q, R>> {
254        self.nodes.get(index)
255    }
256
257    /// Get metadata for a node
258    pub fn get_metadata(&self, index: usize) -> Option<&NodeMetadata> {
259        self.node_metadata.get(&index)
260    }
261
262    /// Get all neighbors of a node
263    pub fn neighbors(&self, node: usize) -> Vec<usize> {
264        self.edges
265            .iter()
266            .filter(|edge| edge.source == node)
267            .map(|edge| edge.target)
268            .collect()
269    }
270
271    /// Get the degree (number of outgoing edges) of a node
272    pub fn degree(&self, node: usize) -> usize {
273        self.edges.iter().filter(|edge| edge.source == node).count()
274    }
275
276    /// Get all edges in the network
277    pub fn edges(&self) -> &[GeometricEdge] {
278        &self.edges
279    }
280
281    /// Compute geometric distance between two nodes
282    ///
283    /// Uses the natural norm in Clifford algebra space: ||a - b||
284    /// where a and b are the multivector positions of the nodes.
285    pub fn geometric_distance(&self, node1: usize, node2: usize) -> NetworkResult<f64> {
286        if node1 >= self.nodes.len() {
287            return Err(NetworkError::NodeIndexOutOfBounds(node1));
288        }
289        if node2 >= self.nodes.len() {
290            return Err(NetworkError::NodeIndexOutOfBounds(node2));
291        }
292
293        let diff = self.nodes[node1].clone() - self.nodes[node2].clone();
294        Ok(diff.norm())
295    }
296
297    /// Compute geometric centrality for all nodes
298    ///
299    /// Geometric centrality is based on the inverse of the sum of geometric
300    /// distances to all other nodes. Nodes closer to the geometric center
301    /// of the network have higher centrality.
302    pub fn compute_geometric_centrality(&self) -> NetworkResult<Vec<f64>> {
303        if self.nodes.is_empty() {
304            return Ok(Vec::new());
305        }
306
307        let mut centrality = vec![0.0; self.nodes.len()];
308
309        for (i, centrality_value) in centrality.iter_mut().enumerate().take(self.nodes.len()) {
310            let mut total_distance = 0.0;
311            let mut reachable_count = 0;
312
313            for j in 0..self.nodes.len() {
314                if i != j {
315                    let distance = self.geometric_distance(i, j)?;
316                    if distance > 0.0 {
317                        total_distance += distance;
318                        reachable_count += 1;
319                    }
320                }
321            }
322
323            // Centrality is inverse of average distance (higher = more central)
324            *centrality_value = if total_distance > 0.0 && reachable_count > 0 {
325                reachable_count as f64 / total_distance
326            } else {
327                0.0
328            };
329        }
330
331        Ok(centrality)
332    }
333
334    /// Compute betweenness centrality for all nodes
335    ///
336    /// Measures how often each node lies on shortest paths between
337    /// other pairs of nodes using graph-theoretic shortest paths.
338    pub fn compute_betweenness_centrality(&self) -> NetworkResult<Vec<f64>> {
339        if self.nodes.is_empty() {
340            return Ok(Vec::new());
341        }
342
343        let mut betweenness = vec![0.0; self.nodes.len()];
344        let distances = self.compute_all_pairs_shortest_paths()?;
345
346        for s in 0..self.nodes.len() {
347            for t in 0..self.nodes.len() {
348                if s == t {
349                    continue;
350                }
351
352                if distances[s][t].is_infinite() {
353                    continue; // No path from s to t
354                }
355
356                // Count nodes on shortest paths from s to t
357                for v in 0..self.nodes.len() {
358                    if v == s || v == t {
359                        continue;
360                    }
361
362                    if !distances[s][v].is_infinite() && !distances[v][t].is_infinite() {
363                        let path_through_v = distances[s][v] + distances[v][t];
364
365                        // Check if path through v is a shortest path (within tolerance)
366                        if (path_through_v - distances[s][t]).abs() < 1e-10 {
367                            betweenness[v] += 1.0;
368                        }
369                    }
370                }
371            }
372        }
373
374        // Normalize by number of node pairs
375        let normalization = ((self.nodes.len() - 1) * (self.nodes.len() - 2)) as f64;
376        if normalization > 0.0 {
377            for value in &mut betweenness {
378                *value /= normalization;
379            }
380        }
381
382        Ok(betweenness)
383    }
384
385    /// Compute all-pairs shortest paths using Floyd-Warshall algorithm
386    ///
387    /// Returns a matrix where element [i][j] is the shortest path distance
388    /// from node i to node j using edge weights.
389    pub fn compute_all_pairs_shortest_paths(&self) -> NetworkResult<Vec<Vec<f64>>> {
390        let n = self.nodes.len();
391        let mut distances = vec![vec![f64::INFINITY; n]; n];
392
393        // Initialize distances
394        for (i, distance_row) in distances.iter_mut().enumerate().take(n) {
395            distance_row[i] = 0.0; // Distance to self is 0
396        }
397
398        // Set edge distances
399        for edge in &self.edges {
400            distances[edge.source][edge.target] = edge.weight;
401        }
402
403        // Floyd-Warshall algorithm
404        for k in 0..n {
405            for i in 0..n {
406                for j in 0..n {
407                    if distances[i][k] != f64::INFINITY && distances[k][j] != f64::INFINITY {
408                        let new_distance = distances[i][k] + distances[k][j];
409                        if new_distance < distances[i][j] {
410                            distances[i][j] = new_distance;
411                        }
412                    }
413                }
414            }
415        }
416
417        Ok(distances)
418    }
419
420    /// Find shortest path between two nodes using Dijkstra's algorithm
421    ///
422    /// Returns the path as a vector of node indices and the total distance.
423    /// Uses edge weights for distance computation.
424    pub fn shortest_path(
425        &self,
426        source: usize,
427        target: usize,
428    ) -> NetworkResult<Option<(Vec<usize>, f64)>> {
429        if source >= self.nodes.len() {
430            return Err(NetworkError::NodeIndexOutOfBounds(source));
431        }
432        if target >= self.nodes.len() {
433            return Err(NetworkError::NodeIndexOutOfBounds(target));
434        }
435
436        if source == target {
437            return Ok(Some((vec![source], 0.0)));
438        }
439
440        let n = self.nodes.len();
441        let mut distances = vec![f64::INFINITY; n];
442        let mut previous = vec![None; n];
443        let mut visited = vec![false; n];
444
445        distances[source] = 0.0;
446
447        // Build adjacency list for efficiency
448        let mut adj_list: Vec<Vec<(usize, f64)>> = vec![Vec::new(); n];
449        for edge in &self.edges {
450            adj_list[edge.source].push((edge.target, edge.weight));
451        }
452
453        for _ in 0..n {
454            // Find unvisited node with minimum distance
455            let mut current = None;
456            let mut min_distance = f64::INFINITY;
457
458            for v in 0..n {
459                if !visited[v] && distances[v] < min_distance {
460                    min_distance = distances[v];
461                    current = Some(v);
462                }
463            }
464
465            let current = match current {
466                Some(v) => v,
467                None => break, // All remaining nodes are unreachable
468            };
469
470            if current == target {
471                break; // Found shortest path to target
472            }
473
474            visited[current] = true;
475
476            // Update distances to neighbors
477            for &(neighbor, weight) in &adj_list[current] {
478                if !visited[neighbor] {
479                    let new_distance = distances[current] + weight;
480                    if new_distance < distances[neighbor] {
481                        distances[neighbor] = new_distance;
482                        previous[neighbor] = Some(current);
483                    }
484                }
485            }
486        }
487
488        // Check if target is reachable
489        if distances[target].is_infinite() {
490            return Ok(None);
491        }
492
493        // Reconstruct path
494        let mut path = Vec::new();
495        let mut current = target;
496
497        while let Some(prev) = previous[current] {
498            path.push(current);
499            current = prev;
500        }
501        path.push(source);
502        path.reverse();
503
504        Ok(Some((path, distances[target])))
505    }
506
507    /// Find shortest path using geometric distances
508    ///
509    /// Uses geometric distances between multivector positions rather than edge weights.
510    /// This provides a direct path in the geometric algebra space.
511    pub fn shortest_geometric_path(
512        &self,
513        source: usize,
514        target: usize,
515    ) -> NetworkResult<Option<(Vec<usize>, f64)>> {
516        if source >= self.nodes.len() {
517            return Err(NetworkError::NodeIndexOutOfBounds(source));
518        }
519        if target >= self.nodes.len() {
520            return Err(NetworkError::NodeIndexOutOfBounds(target));
521        }
522
523        if source == target {
524            return Ok(Some((vec![source], 0.0)));
525        }
526
527        let n = self.nodes.len();
528        let mut distances = vec![f64::INFINITY; n];
529        let mut previous = vec![None; n];
530        let mut visited = vec![false; n];
531
532        distances[source] = 0.0;
533
534        for _ in 0..n {
535            // Find unvisited node with minimum distance
536            let mut current = None;
537            let mut min_distance = f64::INFINITY;
538
539            for v in 0..n {
540                if !visited[v] && distances[v] < min_distance {
541                    min_distance = distances[v];
542                    current = Some(v);
543                }
544            }
545
546            let current = match current {
547                Some(v) => v,
548                None => break,
549            };
550
551            if current == target {
552                break;
553            }
554
555            visited[current] = true;
556
557            // Check all nodes that are connected by edges
558            for edge in &self.edges {
559                if edge.source == current {
560                    let neighbor = edge.target;
561                    if !visited[neighbor] {
562                        let geometric_distance = self.geometric_distance(current, neighbor)?;
563                        let new_distance = distances[current] + geometric_distance;
564
565                        if new_distance < distances[neighbor] {
566                            distances[neighbor] = new_distance;
567                            previous[neighbor] = Some(current);
568                        }
569                    }
570                }
571            }
572        }
573
574        if distances[target].is_infinite() {
575            return Ok(None);
576        }
577
578        // Reconstruct path
579        let mut path = Vec::new();
580        let mut current = target;
581
582        while let Some(prev) = previous[current] {
583            path.push(current);
584            current = prev;
585        }
586        path.push(source);
587        path.reverse();
588
589        Ok(Some((path, distances[target])))
590    }
591
592    /// Convert to tropical network for efficient path operations
593    ///
594    /// Creates a TropicalNetwork representation using edge weights,
595    /// enabling fast shortest path computations via tropical algebra.
596    pub fn to_tropical_network(&self) -> NetworkResult<crate::tropical::TropicalNetwork> {
597        let n = self.nodes.len();
598        let mut weights = vec![vec![f64::INFINITY; n]; n];
599
600        // Set diagonal to 0 (distance to self)
601        for (i, weight_row) in weights.iter_mut().enumerate().take(n) {
602            weight_row[i] = 0.0;
603        }
604
605        // Set edge weights
606        for edge in &self.edges {
607            weights[edge.source][edge.target] = edge.weight;
608        }
609
610        crate::tropical::TropicalNetwork::from_weights(&weights)
611    }
612
613    /// Detect communities using geometric clustering
614    ///
615    /// Groups nodes based on their geometric proximity in the multivector space.
616    /// Uses k-means style clustering with geometric distance metric.
617    pub fn find_communities(
618        &self,
619        num_communities: usize,
620    ) -> NetworkResult<Vec<Community<P, Q, R>>> {
621        if self.nodes.is_empty() {
622            return Ok(Vec::new());
623        }
624
625        if num_communities == 0 {
626            return Err(NetworkError::invalid_param(
627                "num_communities",
628                0,
629                "greater than 0",
630            ));
631        }
632
633        if num_communities >= self.nodes.len() {
634            // Each node is its own community
635            let communities = self
636                .nodes
637                .iter()
638                .enumerate()
639                .map(|(i, node)| Community {
640                    nodes: vec![i],
641                    geometric_centroid: node.clone(),
642                    cohesion_score: 1.0,
643                })
644                .collect();
645            return Ok(communities);
646        }
647
648        // Initialize centroids using k-means++ style selection
649        let mut centroids: Vec<Multivector<P, Q, R>> = Vec::with_capacity(num_communities);
650        centroids.push(self.nodes[0].clone());
651
652        for _ in 1..num_communities {
653            let mut max_distance = 0.0;
654            let mut farthest_node = 0;
655
656            for (i, node) in self.nodes.iter().enumerate() {
657                let mut min_distance_to_centroid = f64::INFINITY;
658
659                for centroid in &centroids {
660                    let diff = node.clone() - centroid.clone();
661                    let distance = diff.norm();
662                    if distance < min_distance_to_centroid {
663                        min_distance_to_centroid = distance;
664                    }
665                }
666
667                if min_distance_to_centroid > max_distance {
668                    max_distance = min_distance_to_centroid;
669                    farthest_node = i;
670                }
671            }
672
673            centroids.push(self.nodes[farthest_node].clone());
674        }
675
676        // Iterative clustering
677        let mut assignments = vec![0; self.nodes.len()];
678        let max_iterations = 100;
679
680        for _ in 0..max_iterations {
681            let mut changed = false;
682
683            // Assign nodes to nearest centroid
684            for (node_idx, node) in self.nodes.iter().enumerate() {
685                let mut best_cluster = 0;
686                let mut min_distance = f64::INFINITY;
687
688                for (cluster_idx, centroid) in centroids.iter().enumerate() {
689                    let diff = node.clone() - centroid.clone();
690                    let distance = diff.norm();
691
692                    if distance < min_distance {
693                        min_distance = distance;
694                        best_cluster = cluster_idx;
695                    }
696                }
697
698                if assignments[node_idx] != best_cluster {
699                    assignments[node_idx] = best_cluster;
700                    changed = true;
701                }
702            }
703
704            if !changed {
705                break;
706            }
707
708            // Update centroids
709            for (cluster_idx, centroid) in centroids.iter_mut().enumerate().take(num_communities) {
710                let cluster_nodes: Vec<&Multivector<P, Q, R>> = self
711                    .nodes
712                    .iter()
713                    .enumerate()
714                    .filter(|(i, _)| assignments[*i] == cluster_idx)
715                    .map(|(_, node)| node)
716                    .collect();
717
718                if !cluster_nodes.is_empty() {
719                    // Compute centroid as average
720                    let mut sum = cluster_nodes[0].clone();
721                    for node in cluster_nodes.iter().skip(1) {
722                        sum = sum + (*node).clone();
723                    }
724                    *centroid = sum * (1.0 / cluster_nodes.len() as f64);
725                }
726            }
727        }
728
729        // Build communities and compute cohesion scores
730        let mut communities = Vec::new();
731
732        for (cluster_idx, centroid) in centroids.iter().enumerate().take(num_communities) {
733            let cluster_nodes: Vec<usize> = assignments
734                .iter()
735                .enumerate()
736                .filter(|(_, &assignment)| assignment == cluster_idx)
737                .map(|(i, _)| i)
738                .collect();
739
740            if cluster_nodes.is_empty() {
741                continue;
742            }
743
744            // Compute cohesion as inverse of average intra-cluster distance
745            let mut total_distance = 0.0;
746            let mut pair_count = 0;
747
748            for &i in &cluster_nodes {
749                for &j in &cluster_nodes {
750                    if i < j {
751                        let distance = self.geometric_distance(i, j)?;
752                        total_distance += distance;
753                        pair_count += 1;
754                    }
755                }
756            }
757
758            let cohesion_score = if pair_count > 0 && total_distance > 0.0 {
759                pair_count as f64 / total_distance
760            } else {
761                1.0
762            };
763
764            communities.push(Community {
765                nodes: cluster_nodes,
766                geometric_centroid: centroid.clone(),
767                cohesion_score,
768            });
769        }
770
771        Ok(communities)
772    }
773
774    /// Simulate information diffusion through the network
775    ///
776    /// Models how information spreads using geometric products between node positions.
777    /// Initial information is placed at source nodes and propagates based on edge weights
778    /// and geometric similarity.
779    pub fn simulate_diffusion(
780        &self,
781        initial_sources: &[usize],
782        max_steps: usize,
783        diffusion_rate: f64,
784    ) -> NetworkResult<PropagationAnalysis> {
785        if initial_sources.is_empty() {
786            return Err(NetworkError::insufficient_data(
787                "No initial sources provided",
788            ));
789        }
790
791        for &source in initial_sources {
792            if source >= self.nodes.len() {
793                return Err(NetworkError::NodeIndexOutOfBounds(source));
794            }
795        }
796
797        if diffusion_rate <= 0.0 || diffusion_rate > 1.0 {
798            return Err(NetworkError::invalid_param(
799                "diffusion_rate",
800                diffusion_rate,
801                "between 0 and 1",
802            ));
803        }
804
805        let n = self.nodes.len();
806        let mut information_levels = vec![0.0; n];
807        let mut coverage = Vec::new();
808        let mut influence_scores = vec![0.0; n];
809
810        // Initialize information at source nodes
811        for &source in initial_sources {
812            information_levels[source] = 1.0;
813        }
814
815        let convergence_threshold = 1e-6;
816        let mut converged_step = max_steps;
817
818        for step in 0..max_steps {
819            // Count nodes with significant information
820            let current_coverage = information_levels
821                .iter()
822                .filter(|&&level| level > convergence_threshold)
823                .count();
824            coverage.push(current_coverage);
825
826            let previous_levels = information_levels.clone();
827            let mut new_levels = information_levels.clone();
828
829            // Diffuse information along edges
830            for edge in &self.edges {
831                let source_level = information_levels[edge.source];
832                if source_level > convergence_threshold {
833                    // Compute geometric similarity for diffusion strength
834                    let similarity = self.compute_geometric_similarity(edge.source, edge.target)?;
835
836                    // Information transfer based on edge weight, diffusion rate, and geometric similarity
837                    let transfer_amount = source_level * diffusion_rate * similarity * edge.weight;
838
839                    new_levels[edge.target] += transfer_amount;
840                    influence_scores[edge.source] += transfer_amount;
841                }
842            }
843
844            // Apply decay to prevent infinite accumulation
845            for level in &mut new_levels {
846                *level *= 0.95; // 5% decay per step
847            }
848
849            information_levels = new_levels;
850
851            // Check for convergence
852            let total_change: f64 = information_levels
853                .iter()
854                .zip(previous_levels.iter())
855                .map(|(new, old)| (new - old).abs())
856                .sum();
857
858            if total_change < convergence_threshold {
859                converged_step = step + 1;
860                break;
861            }
862        }
863
864        Ok(PropagationAnalysis {
865            coverage,
866            influence_scores,
867            convergence_time: converged_step,
868        })
869    }
870
871    /// Compute geometric similarity between two nodes
872    ///
873    /// Uses the geometric product in Clifford algebra to measure similarity.
874    /// Higher values indicate more similar geometric positions.
875    fn compute_geometric_similarity(&self, node1: usize, node2: usize) -> NetworkResult<f64> {
876        let pos1 = &self.nodes[node1];
877        let pos2 = &self.nodes[node2];
878
879        // Compute geometric product and extract scalar part as similarity measure
880        let product = pos1.geometric_product(pos2);
881        let scalar_part = product.scalar_part();
882
883        // Normalize by the norms to get a similarity measure
884        let norm1 = pos1.norm();
885        let norm2 = pos2.norm();
886
887        if norm1 > 0.0 && norm2 > 0.0 {
888            Ok((scalar_part / (norm1 * norm2)).abs())
889        } else {
890            Ok(0.0)
891        }
892    }
893
894    /// Perform spectral clustering using the graph Laplacian
895    ///
896    /// Uses eigenvalue decomposition of the normalized Laplacian matrix
897    /// to identify community structure in the network.
898    pub fn spectral_clustering(&self, num_clusters: usize) -> NetworkResult<Vec<Vec<usize>>> {
899        use nalgebra::{DMatrix, SymmetricEigen};
900
901        if self.nodes.is_empty() {
902            return Ok(Vec::new());
903        }
904
905        if num_clusters == 0 {
906            return Err(NetworkError::invalid_param(
907                "num_clusters",
908                0,
909                "greater than 0",
910            ));
911        }
912
913        let n = self.nodes.len();
914        if num_clusters >= n {
915            // Each node is its own cluster
916            return Ok((0..n).map(|i| vec![i]).collect());
917        }
918
919        // Build adjacency matrix
920        let mut adjacency = DMatrix::zeros(n, n);
921        for edge in &self.edges {
922            adjacency[(edge.source, edge.target)] = edge.weight;
923            // For spectral clustering, typically use symmetric adjacency
924            adjacency[(edge.target, edge.source)] = edge.weight;
925        }
926
927        // Compute degree matrix
928        let mut degree = DMatrix::zeros(n, n);
929        for i in 0..n {
930            let node_degree: f64 = adjacency.row(i).sum();
931            if node_degree > 0.0 {
932                degree[(i, i)] = node_degree;
933            }
934        }
935
936        // Compute normalized Laplacian: L = I - D^(-1/2) * A * D^(-1/2)
937        let mut normalized_laplacian = DMatrix::identity(n, n);
938
939        for i in 0..n {
940            for j in 0..n {
941                if i != j && degree[(i, i)] > 0.0 && degree[(j, j)] > 0.0 {
942                    let value = adjacency[(i, j)] / (degree[(i, i)].sqrt() * degree[(j, j)].sqrt());
943                    normalized_laplacian[(i, j)] = -value;
944                }
945            }
946        }
947
948        // Compute eigenvalues and eigenvectors
949        let eigen = SymmetricEigen::new(normalized_laplacian);
950        let eigenvalues = eigen.eigenvalues;
951        let eigenvectors = eigen.eigenvectors;
952
953        // Use smallest eigenvalues' eigenvectors for clustering
954        let mut clusters = vec![Vec::new(); num_clusters];
955
956        // Simple assignment based on eigenvector values
957        for i in 0..n {
958            let mut best_cluster = 0;
959            let mut max_value = eigenvectors[(i, 0)];
960
961            for k in 1..num_clusters.min(eigenvalues.len()) {
962                if eigenvectors[(i, k)].abs() > max_value.abs() {
963                    max_value = eigenvectors[(i, k)];
964                    best_cluster = k;
965                }
966            }
967
968            clusters[best_cluster].push(i);
969        }
970
971        // Remove empty clusters
972        clusters.retain(|cluster| !cluster.is_empty());
973
974        Ok(clusters)
975    }
976}
977
978impl<const P: usize, const Q: usize, const R: usize> Default for GeometricNetwork<P, Q, R> {
979    fn default() -> Self {
980        Self::new()
981    }
982}