1use crate::types::*;
6use rustkernel_core::{domain::Domain, kernel::KernelMetadata, traits::GpuKernel};
7use std::collections::{HashMap, HashSet, VecDeque};
8
9#[derive(Debug, Clone)]
18pub struct HypergraphConstruction {
19 metadata: KernelMetadata,
20}
21
22impl Default for HypergraphConstruction {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl HypergraphConstruction {
29 #[must_use]
31 pub fn new() -> Self {
32 Self {
33 metadata: KernelMetadata::batch("audit/hypergraph", Domain::FinancialAudit)
34 .with_description("Multi-way relationship hypergraph")
35 .with_throughput(10_000)
36 .with_latency_us(500.0),
37 }
38 }
39
40 pub fn construct(records: &[AuditRecord], config: &HypergraphConfig) -> HypergraphResult {
42 let nodes = Self::build_nodes(records, config);
44
45 let edges = Self::build_edges(records, &nodes, config);
47
48 let node_edges = Self::build_node_edge_mapping(&edges);
50
51 let hypergraph = Hypergraph {
52 nodes,
53 edges,
54 node_edges,
55 };
56
57 let node_centrality = Self::calculate_centrality(&hypergraph);
59
60 let edge_weights = Self::calculate_edge_weights(&hypergraph);
62
63 let patterns = if config.detect_patterns {
65 Self::detect_patterns(&hypergraph, &node_centrality)
66 } else {
67 Vec::new()
68 };
69
70 let stats = Self::calculate_stats(&hypergraph);
72
73 HypergraphResult {
74 hypergraph,
75 node_centrality,
76 edge_weights,
77 patterns,
78 stats,
79 }
80 }
81
82 fn build_nodes(
84 records: &[AuditRecord],
85 config: &HypergraphConfig,
86 ) -> HashMap<String, HypergraphNode> {
87 let mut nodes = HashMap::new();
88
89 for record in records {
90 if config.include_entity_nodes {
92 let entity_id = format!("entity:{}", record.entity_id);
93 nodes
94 .entry(entity_id.clone())
95 .or_insert_with(|| HypergraphNode {
96 id: entity_id,
97 node_type: NodeType::Entity,
98 attributes: HashMap::new(),
99 });
100 }
101
102 if config.include_account_nodes {
104 if let Some(account) = &record.account {
105 let account_id = format!("account:{}", account);
106 nodes
107 .entry(account_id.clone())
108 .or_insert_with(|| HypergraphNode {
109 id: account_id,
110 node_type: NodeType::Account,
111 attributes: HashMap::new(),
112 });
113 }
114 }
115
116 if config.include_transaction_nodes {
118 let tx_id = format!("tx:{}", record.id);
119 nodes
120 .entry(tx_id.clone())
121 .or_insert_with(|| HypergraphNode {
122 id: tx_id,
123 node_type: NodeType::Transaction,
124 attributes: HashMap::new(),
125 });
126 }
127
128 if config.include_category_nodes {
130 let cat_id = format!("category:{}", record.category);
131 nodes
132 .entry(cat_id.clone())
133 .or_insert_with(|| HypergraphNode {
134 id: cat_id,
135 node_type: NodeType::Category,
136 attributes: HashMap::new(),
137 });
138 }
139
140 if config.include_time_nodes {
142 let day = record.timestamp / 86400;
143 let time_id = format!("day:{}", day);
144 nodes
145 .entry(time_id.clone())
146 .or_insert_with(|| HypergraphNode {
147 id: time_id,
148 node_type: NodeType::TimePeriod,
149 attributes: HashMap::new(),
150 });
151 }
152 }
153
154 nodes
155 }
156
157 fn build_edges(
159 records: &[AuditRecord],
160 nodes: &HashMap<String, HypergraphNode>,
161 config: &HypergraphConfig,
162 ) -> Vec<Hyperedge> {
163 let mut edges = Vec::new();
164 let mut edge_id = 0;
165
166 for record in records {
167 let mut connected_nodes = Vec::new();
168
169 if config.include_entity_nodes {
171 let entity_id = format!("entity:{}", record.entity_id);
172 if nodes.contains_key(&entity_id) {
173 connected_nodes.push(entity_id);
174 }
175 }
176
177 if config.include_account_nodes {
178 if let Some(account) = &record.account {
179 let account_id = format!("account:{}", account);
180 if nodes.contains_key(&account_id) {
181 connected_nodes.push(account_id);
182 }
183 }
184 }
185
186 if config.include_transaction_nodes {
187 let tx_id = format!("tx:{}", record.id);
188 if nodes.contains_key(&tx_id) {
189 connected_nodes.push(tx_id);
190 }
191 }
192
193 if config.include_category_nodes {
194 let cat_id = format!("category:{}", record.category);
195 if nodes.contains_key(&cat_id) {
196 connected_nodes.push(cat_id);
197 }
198 }
199
200 if config.include_time_nodes {
201 let day = record.timestamp / 86400;
202 let time_id = format!("day:{}", day);
203 if nodes.contains_key(&time_id) {
204 connected_nodes.push(time_id);
205 }
206 }
207
208 if connected_nodes.len() >= config.min_edge_size {
210 edge_id += 1;
211 edges.push(Hyperedge {
212 id: format!("edge:{}", edge_id),
213 edge_type: Self::determine_edge_type(record),
214 nodes: connected_nodes,
215 weight: record.amount.unwrap_or(1.0),
216 timestamp: Some(record.timestamp),
217 attributes: HashMap::new(),
218 });
219 }
220 }
221
222 edges
223 }
224
225 fn determine_edge_type(record: &AuditRecord) -> HyperedgeType {
227 match record.record_type {
228 AuditRecordType::Payment | AuditRecordType::Transfer => HyperedgeType::Transaction,
229 AuditRecordType::Invoice | AuditRecordType::Receipt => HyperedgeType::DocumentRef,
230 AuditRecordType::JournalEntry | AuditRecordType::Adjustment => {
231 HyperedgeType::AccountRel
232 }
233 AuditRecordType::Expense | AuditRecordType::Revenue => {
234 HyperedgeType::CategoryMembership
235 }
236 }
237 }
238
239 fn build_node_edge_mapping(edges: &[Hyperedge]) -> HashMap<String, Vec<String>> {
241 let mut mapping: HashMap<String, Vec<String>> = HashMap::new();
242
243 for edge in edges {
244 for node_id in &edge.nodes {
245 mapping
246 .entry(node_id.clone())
247 .or_default()
248 .push(edge.id.clone());
249 }
250 }
251
252 mapping
253 }
254
255 fn calculate_centrality(hypergraph: &Hypergraph) -> HashMap<String, f64> {
257 let mut centrality = HashMap::new();
258
259 let total_edges = hypergraph.edges.len() as f64;
261
262 for node_id in hypergraph.nodes.keys() {
263 let edge_count = hypergraph
264 .node_edges
265 .get(node_id)
266 .map(|e| e.len())
267 .unwrap_or(0) as f64;
268
269 let degree_centrality = if total_edges > 0.0 {
270 edge_count / total_edges
271 } else {
272 0.0
273 };
274
275 centrality.insert(node_id.clone(), degree_centrality);
276 }
277
278 centrality
279 }
280
281 fn calculate_edge_weights(hypergraph: &Hypergraph) -> HashMap<String, f64> {
283 let max_weight = hypergraph
284 .edges
285 .iter()
286 .map(|e| e.weight)
287 .fold(0.0, f64::max);
288
289 hypergraph
290 .edges
291 .iter()
292 .map(|e| {
293 let normalized = if max_weight > 0.0 {
294 e.weight / max_weight
295 } else {
296 0.0
297 };
298 (e.id.clone(), normalized)
299 })
300 .collect()
301 }
302
303 fn detect_patterns(
305 hypergraph: &Hypergraph,
306 centrality: &HashMap<String, f64>,
307 ) -> Vec<HypergraphPattern> {
308 let mut patterns = Vec::new();
309 let mut pattern_id = 0;
310
311 let avg_centrality: f64 = centrality.values().sum::<f64>() / centrality.len().max(1) as f64;
313 let std_centrality = Self::std_dev(¢rality.values().cloned().collect::<Vec<_>>());
314
315 for (node_id, cent) in centrality {
316 if *cent > avg_centrality + 2.0 * std_centrality {
317 pattern_id += 1;
318 patterns.push(HypergraphPattern {
319 id: format!("pattern:{}", pattern_id),
320 pattern_type: PatternType::HighCentralityHub,
321 nodes: vec![node_id.clone()],
322 edges: hypergraph
323 .node_edges
324 .get(node_id)
325 .cloned()
326 .unwrap_or_default(),
327 confidence: (*cent - avg_centrality) / std_centrality / 3.0,
328 description: format!(
329 "High centrality hub detected: {} (centrality: {:.3})",
330 node_id, cent
331 ),
332 });
333 }
334 }
335
336 let components = Self::find_connected_components(hypergraph);
338 if components.len() > 1 {
339 for (i, component) in components.iter().enumerate() {
340 if component.len() < hypergraph.nodes.len() / 10 {
341 pattern_id += 1;
342 patterns.push(HypergraphPattern {
343 id: format!("pattern:{}", pattern_id),
344 pattern_type: PatternType::IsolatedComponent,
345 nodes: component.clone(),
346 edges: Vec::new(),
347 confidence: 0.8,
348 description: format!(
349 "Isolated component {} with {} nodes",
350 i + 1,
351 component.len()
352 ),
353 });
354 }
355 }
356 }
357
358 let dense = Self::find_dense_subgraphs(hypergraph);
360 for (nodes, density) in dense {
361 if density > 0.5 {
362 pattern_id += 1;
363 let related_edges: Vec<String> = nodes
364 .iter()
365 .flat_map(|n| hypergraph.node_edges.get(n).cloned().unwrap_or_default())
366 .collect::<HashSet<_>>()
367 .into_iter()
368 .collect();
369
370 patterns.push(HypergraphPattern {
371 id: format!("pattern:{}", pattern_id),
372 pattern_type: PatternType::DenseSubgraph,
373 nodes,
374 edges: related_edges,
375 confidence: density,
376 description: format!("Dense subgraph with density {:.2}", density),
377 });
378 }
379 }
380
381 patterns
382 }
383
384 fn find_connected_components(hypergraph: &Hypergraph) -> Vec<Vec<String>> {
386 let mut components = Vec::new();
387 let mut visited: HashSet<&String> = HashSet::new();
388
389 for node_id in hypergraph.nodes.keys() {
390 if visited.contains(node_id) {
391 continue;
392 }
393
394 let mut component = Vec::new();
395 let mut queue = VecDeque::new();
396 queue.push_back(node_id);
397
398 while let Some(current) = queue.pop_front() {
399 if visited.contains(current) {
400 continue;
401 }
402 visited.insert(current);
403 component.push(current.clone());
404
405 if let Some(edges) = hypergraph.node_edges.get(current) {
407 for edge_id in edges {
408 if let Some(edge) = hypergraph.edges.iter().find(|e| &e.id == edge_id) {
409 for neighbor in &edge.nodes {
410 if !visited.contains(neighbor) {
411 queue.push_back(neighbor);
412 }
413 }
414 }
415 }
416 }
417 }
418
419 if !component.is_empty() {
420 components.push(component);
421 }
422 }
423
424 components
425 }
426
427 fn find_dense_subgraphs(hypergraph: &Hypergraph) -> Vec<(Vec<String>, f64)> {
429 let mut dense_subgraphs = Vec::new();
430
431 let entity_nodes: Vec<&String> = hypergraph
433 .nodes
434 .iter()
435 .filter(|(_, n)| n.node_type == NodeType::Entity)
436 .map(|(id, _)| id)
437 .collect();
438
439 if entity_nodes.len() >= 3 {
440 let mut shared_counts: HashMap<(&String, &String), usize> = HashMap::new();
442
443 for node in &entity_nodes {
444 if let Some(edges) = hypergraph.node_edges.get(*node) {
445 let edge_set: HashSet<&String> = edges.iter().collect();
446
447 for other in &entity_nodes {
448 if node >= other {
449 continue;
450 }
451 if let Some(other_edges) = hypergraph.node_edges.get(*other) {
452 let other_set: HashSet<&String> = other_edges.iter().collect();
453 let shared = edge_set.intersection(&other_set).count();
454 if shared > 0 {
455 shared_counts.insert((node, other), shared);
456 }
457 }
458 }
459 }
460 }
461
462 let avg_shared = if !shared_counts.is_empty() {
464 shared_counts.values().sum::<usize>() as f64 / shared_counts.len() as f64
465 } else {
466 0.0
467 };
468
469 if avg_shared > 1.0 {
470 let high_shared: Vec<_> = shared_counts
471 .iter()
472 .filter(|&(_, &count)| count as f64 > avg_shared * 1.5)
473 .collect();
474
475 if !high_shared.is_empty() {
476 let mut nodes_in_dense: HashSet<String> = HashSet::new();
477 for ((a, b), _) in high_shared {
478 nodes_in_dense.insert((*a).clone());
479 nodes_in_dense.insert((*b).clone());
480 }
481
482 let nodes: Vec<String> = nodes_in_dense.into_iter().collect();
483 let max_edges = nodes.len() * (nodes.len() - 1) / 2;
484 let actual_edges = shared_counts
485 .iter()
486 .filter(|((a, b), _)| nodes.contains(*a) && nodes.contains(*b))
487 .count();
488 let density = if max_edges > 0 {
489 actual_edges as f64 / max_edges as f64
490 } else {
491 0.0
492 };
493
494 dense_subgraphs.push((nodes, density));
495 }
496 }
497 }
498
499 dense_subgraphs
500 }
501
502 fn calculate_stats(hypergraph: &Hypergraph) -> HypergraphStats {
504 let node_count = hypergraph.nodes.len();
505 let edge_count = hypergraph.edges.len();
506
507 let avg_edge_size = if edge_count > 0 {
508 hypergraph
509 .edges
510 .iter()
511 .map(|e| e.nodes.len() as f64)
512 .sum::<f64>()
513 / edge_count as f64
514 } else {
515 0.0
516 };
517
518 let avg_node_degree = if node_count > 0 {
519 hypergraph
520 .node_edges
521 .values()
522 .map(|e| e.len() as f64)
523 .sum::<f64>()
524 / node_count as f64
525 } else {
526 0.0
527 };
528
529 let components = Self::find_connected_components(hypergraph);
530 let component_count = components.len();
531
532 let max_possible_edges = if node_count > 1 {
534 (2_usize.pow(node_count as u32) - node_count - 1) as f64
535 } else {
536 1.0
537 };
538 let density = (edge_count as f64 / max_possible_edges).min(1.0);
539
540 HypergraphStats {
541 node_count,
542 edge_count,
543 avg_edge_size,
544 avg_node_degree,
545 component_count,
546 density,
547 }
548 }
549
550 fn std_dev(values: &[f64]) -> f64 {
552 if values.is_empty() {
553 return 0.0;
554 }
555 let mean = values.iter().sum::<f64>() / values.len() as f64;
556 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / values.len() as f64;
557 variance.sqrt()
558 }
559
560 pub fn get_nodes_by_type(
562 result: &HypergraphResult,
563 node_type: NodeType,
564 ) -> Vec<&HypergraphNode> {
565 result
566 .hypergraph
567 .nodes
568 .values()
569 .filter(|n| n.node_type == node_type)
570 .collect()
571 }
572
573 pub fn get_node_edges<'a>(result: &'a HypergraphResult, node_id: &str) -> Vec<&'a Hyperedge> {
575 result
576 .hypergraph
577 .node_edges
578 .get(node_id)
579 .map(|edge_ids| {
580 result
581 .hypergraph
582 .edges
583 .iter()
584 .filter(|e| edge_ids.contains(&e.id))
585 .collect()
586 })
587 .unwrap_or_default()
588 }
589}
590
591impl GpuKernel for HypergraphConstruction {
592 fn metadata(&self) -> &KernelMetadata {
593 &self.metadata
594 }
595}
596
597#[derive(Debug, Clone)]
603pub struct HypergraphConfig {
604 pub include_entity_nodes: bool,
606 pub include_account_nodes: bool,
608 pub include_transaction_nodes: bool,
610 pub include_category_nodes: bool,
612 pub include_time_nodes: bool,
614 pub min_edge_size: usize,
616 pub detect_patterns: bool,
618}
619
620impl Default for HypergraphConfig {
621 fn default() -> Self {
622 Self {
623 include_entity_nodes: true,
624 include_account_nodes: true,
625 include_transaction_nodes: false,
626 include_category_nodes: true,
627 include_time_nodes: false,
628 min_edge_size: 2,
629 detect_patterns: true,
630 }
631 }
632}
633
634#[cfg(test)]
639mod tests {
640 use super::*;
641
642 fn create_test_record(
643 id: &str,
644 entity_id: &str,
645 record_type: AuditRecordType,
646 amount: f64,
647 timestamp: u64,
648 category: &str,
649 ) -> AuditRecord {
650 AuditRecord {
651 id: id.to_string(),
652 record_type,
653 entity_id: entity_id.to_string(),
654 timestamp,
655 amount: Some(amount),
656 currency: Some("USD".to_string()),
657 account: Some(format!("ACC-{}", entity_id)),
658 counter_party: Some("CP001".to_string()),
659 category: category.to_string(),
660 attributes: HashMap::new(),
661 }
662 }
663
664 fn create_test_records() -> Vec<AuditRecord> {
665 vec![
666 create_test_record(
667 "R001",
668 "E001",
669 AuditRecordType::Payment,
670 1000.0,
671 1000000,
672 "Operating",
673 ),
674 create_test_record(
675 "R002",
676 "E001",
677 AuditRecordType::Invoice,
678 1500.0,
679 1000100,
680 "Operating",
681 ),
682 create_test_record(
683 "R003",
684 "E002",
685 AuditRecordType::Payment,
686 500.0,
687 1000200,
688 "Operating",
689 ),
690 create_test_record(
691 "R004",
692 "E002",
693 AuditRecordType::Revenue,
694 10000.0,
695 1000300,
696 "Sales",
697 ),
698 create_test_record(
699 "R005",
700 "E003",
701 AuditRecordType::Expense,
702 3000.0,
703 1000400,
704 "Operating",
705 ),
706 ]
707 }
708
709 #[test]
710 fn test_construct_hypergraph() {
711 let records = create_test_records();
712 let config = HypergraphConfig::default();
713
714 let result = HypergraphConstruction::construct(&records, &config);
715
716 assert!(!result.hypergraph.nodes.is_empty());
717 assert!(!result.hypergraph.edges.is_empty());
718 }
719
720 #[test]
721 fn test_node_types() {
722 let records = create_test_records();
723 let config = HypergraphConfig::default();
724
725 let result = HypergraphConstruction::construct(&records, &config);
726
727 let entity_nodes = HypergraphConstruction::get_nodes_by_type(&result, NodeType::Entity);
728 assert_eq!(entity_nodes.len(), 3); let category_nodes = HypergraphConstruction::get_nodes_by_type(&result, NodeType::Category);
731 assert_eq!(category_nodes.len(), 2); }
733
734 #[test]
735 fn test_hyperedges() {
736 let records = create_test_records();
737 let config = HypergraphConfig::default();
738
739 let result = HypergraphConstruction::construct(&records, &config);
740
741 assert_eq!(result.hypergraph.edges.len(), 5);
743
744 for edge in &result.hypergraph.edges {
746 assert!(edge.nodes.len() >= config.min_edge_size);
747 }
748 }
749
750 #[test]
751 fn test_node_centrality() {
752 let records = create_test_records();
753 let config = HypergraphConfig::default();
754
755 let result = HypergraphConstruction::construct(&records, &config);
756
757 let operating_cent = result.node_centrality.get("category:Operating").unwrap();
759 let sales_cent = result.node_centrality.get("category:Sales").unwrap();
760 assert!(operating_cent > sales_cent);
761 }
762
763 #[test]
764 fn test_statistics() {
765 let records = create_test_records();
766 let config = HypergraphConfig::default();
767
768 let result = HypergraphConstruction::construct(&records, &config);
769
770 assert!(result.stats.node_count > 0);
771 assert!(result.stats.edge_count > 0);
772 assert!(result.stats.avg_edge_size > 0.0);
773 }
774
775 #[test]
776 fn test_empty_records() {
777 let records: Vec<AuditRecord> = vec![];
778 let config = HypergraphConfig::default();
779
780 let result = HypergraphConstruction::construct(&records, &config);
781
782 assert!(result.hypergraph.nodes.is_empty());
783 assert!(result.hypergraph.edges.is_empty());
784 }
785
786 #[test]
787 fn test_selective_nodes() {
788 let records = create_test_records();
789 let config = HypergraphConfig {
790 include_entity_nodes: true,
791 include_account_nodes: false,
792 include_transaction_nodes: false,
793 include_category_nodes: false,
794 include_time_nodes: false,
795 min_edge_size: 1,
796 detect_patterns: false,
797 };
798
799 let result = HypergraphConstruction::construct(&records, &config);
800
801 for node in result.hypergraph.nodes.values() {
803 assert_eq!(node.node_type, NodeType::Entity);
804 }
805 }
806
807 #[test]
808 fn test_get_node_edges() {
809 let records = create_test_records();
810 let config = HypergraphConfig::default();
811
812 let result = HypergraphConstruction::construct(&records, &config);
813
814 let e001_edges = HypergraphConstruction::get_node_edges(&result, "entity:E001");
815 assert_eq!(e001_edges.len(), 2); }
817
818 #[test]
819 fn test_connected_components() {
820 let records = create_test_records();
821 let config = HypergraphConfig::default();
822
823 let result = HypergraphConstruction::construct(&records, &config);
824
825 assert_eq!(result.stats.component_count, 1);
827 }
828
829 #[test]
830 fn test_edge_weights() {
831 let records = create_test_records();
832 let config = HypergraphConfig::default();
833
834 let result = HypergraphConstruction::construct(&records, &config);
835
836 for weight in result.edge_weights.values() {
838 assert!(*weight >= 0.0 && *weight <= 1.0);
839 }
840
841 assert!(
843 result
844 .edge_weights
845 .values()
846 .any(|w| (*w - 1.0).abs() < 0.01)
847 );
848 }
849
850 #[test]
851 fn test_min_edge_size() {
852 let records = create_test_records();
853 let config = HypergraphConfig {
854 min_edge_size: 5, ..Default::default()
856 };
857
858 let result = HypergraphConstruction::construct(&records, &config);
859
860 for edge in &result.hypergraph.edges {
862 assert!(edge.nodes.len() >= 5);
863 }
864 }
865}