Skip to main content

ringkernel_accnet/models/
network.rs

1//! Accounting network graph representation.
2//!
3//! The network is a directed graph where:
4//! - Nodes = Accounts
5//! - Edges = Transaction flows (monetary movements)
6//!
7//! This provides the foundation for graph analytics like centrality,
8//! cycle detection, and anomaly identification.
9
10use super::{
11    AccountFlags, AccountMetadata, AccountNode, AccountType, AggregatedFlow, FlowDirection,
12    GraphEdge, HybridTimestamp, TransactionFlow,
13};
14use rkyv::{Archive, Deserialize, Serialize};
15use std::collections::HashMap;
16use uuid::Uuid;
17
18/// Statistics about the accounting network.
19#[derive(Debug, Clone, Default)]
20pub struct NetworkStatistics {
21    /// Number of account nodes
22    pub node_count: usize,
23    /// Number of flow edges
24    pub edge_count: usize,
25    /// Number of unique edges (ignoring direction)
26    pub unique_edge_count: usize,
27    /// Graph density (edges / possible edges)
28    pub density: f64,
29    /// Average node degree (in + out)
30    pub avg_degree: f64,
31    /// Maximum node degree
32    pub max_degree: u32,
33    /// Number of connected components
34    pub component_count: usize,
35    /// Total monetary flow
36    pub total_flow_amount: f64,
37    /// Average flow confidence
38    pub avg_confidence: f64,
39    /// Number of suspense accounts detected
40    pub suspense_account_count: usize,
41    /// Number of GAAP violations
42    pub gaap_violation_count: usize,
43    /// Number of fraud patterns detected
44    pub fraud_pattern_count: usize,
45    /// Timestamp of last update
46    pub last_updated: HybridTimestamp,
47}
48
49/// The complete accounting network graph.
50#[derive(Debug, Clone)]
51pub struct AccountingNetwork {
52    /// Network identifier
53    pub id: Uuid,
54    /// Entity (company) this network belongs to
55    pub entity_id: Uuid,
56    /// Fiscal year
57    pub fiscal_year: u16,
58    /// Fiscal period (1-12 for monthly, 1-4 for quarterly)
59    pub fiscal_period: u8,
60
61    /// Account nodes (indexed by account index 0-255)
62    pub accounts: Vec<AccountNode>,
63    /// Account metadata (names, codes, etc.)
64    pub account_metadata: HashMap<u16, AccountMetadata>,
65
66    /// Transaction flows (edges)
67    pub flows: Vec<TransactionFlow>,
68    /// Aggregated flows by (source, target) pair
69    pub aggregated_flows: HashMap<(u16, u16), AggregatedFlow>,
70
71    /// Adjacency list: for each account, list of (target, flow_index)
72    pub adjacency_out: Vec<Vec<(u16, usize)>>,
73    /// Reverse adjacency: for each account, list of (source, flow_index)
74    pub adjacency_in: Vec<Vec<(u16, usize)>>,
75
76    /// Network statistics
77    pub statistics: NetworkStatistics,
78
79    /// Period start timestamp.
80    pub period_start: HybridTimestamp,
81    /// Period end timestamp.
82    pub period_end: HybridTimestamp,
83}
84
85impl AccountingNetwork {
86    /// Create a new empty network.
87    pub fn new(entity_id: Uuid, fiscal_year: u16, fiscal_period: u8) -> Self {
88        Self {
89            id: Uuid::new_v4(),
90            entity_id,
91            fiscal_year,
92            fiscal_period,
93            accounts: Vec::new(),
94            account_metadata: HashMap::new(),
95            flows: Vec::new(),
96            aggregated_flows: HashMap::new(),
97            adjacency_out: Vec::new(),
98            adjacency_in: Vec::new(),
99            statistics: NetworkStatistics::default(),
100            period_start: HybridTimestamp::zero(),
101            period_end: HybridTimestamp::zero(),
102        }
103    }
104
105    /// Add an account to the network.
106    pub fn add_account(&mut self, mut account: AccountNode, metadata: AccountMetadata) -> u16 {
107        let index = self.accounts.len() as u16;
108        account.index = index;
109        self.accounts.push(account);
110        self.account_metadata.insert(index, metadata);
111        self.adjacency_out.push(Vec::new());
112        self.adjacency_in.push(Vec::new());
113        self.statistics.node_count = self.accounts.len();
114        index
115    }
116
117    /// Add a flow to the network.
118    pub fn add_flow(&mut self, flow: TransactionFlow) {
119        let flow_index = self.flows.len();
120        let source = flow.source_account_index;
121        let target = flow.target_account_index;
122
123        // Update adjacency lists
124        if (source as usize) < self.adjacency_out.len() {
125            self.adjacency_out[source as usize].push((target, flow_index));
126        }
127        if (target as usize) < self.adjacency_in.len() {
128            self.adjacency_in[target as usize].push((source, flow_index));
129        }
130
131        // Update aggregated flows
132        let key = (source, target);
133        self.aggregated_flows
134            .entry(key)
135            .or_insert_with(|| AggregatedFlow::new(source, target))
136            .add(&flow);
137
138        // Update account degrees and balances (using saturating add to prevent overflow)
139        let flow_amount = flow.amount;
140        if (source as usize) < self.accounts.len() {
141            let acc = &mut self.accounts[source as usize];
142            acc.out_degree = acc.out_degree.saturating_add(1);
143            acc.transaction_count = acc.transaction_count.saturating_add(1);
144            // Source account: debit (outflow)
145            acc.total_debits = acc.total_debits + flow_amount;
146            acc.closing_balance = acc.closing_balance - flow_amount;
147        }
148        if (target as usize) < self.accounts.len() {
149            let acc = &mut self.accounts[target as usize];
150            acc.in_degree = acc.in_degree.saturating_add(1);
151            acc.transaction_count = acc.transaction_count.saturating_add(1);
152            // Target account: credit (inflow)
153            acc.total_credits = acc.total_credits + flow_amount;
154            acc.closing_balance = acc.closing_balance + flow_amount;
155        }
156
157        // Update timestamps
158        if self.period_start.physical == 0 || flow.timestamp < self.period_start {
159            self.period_start = flow.timestamp;
160        }
161        if flow.timestamp > self.period_end {
162            self.period_end = flow.timestamp;
163        }
164
165        self.flows.push(flow);
166        self.statistics.edge_count = self.flows.len();
167        self.statistics.unique_edge_count = self.aggregated_flows.len();
168    }
169
170    /// Incorporate multiple flows efficiently.
171    pub fn incorporate_flows(&mut self, flows: &[TransactionFlow]) {
172        for flow in flows {
173            self.add_flow(flow.clone());
174        }
175        self.update_statistics();
176    }
177
178    /// Update network statistics.
179    pub fn update_statistics(&mut self) {
180        let n = self.accounts.len();
181        let e = self.aggregated_flows.len();
182
183        // Density: e / (n * (n-1)) for directed graph
184        self.statistics.density = if n > 1 {
185            e as f64 / (n * (n - 1)) as f64
186        } else {
187            0.0
188        };
189
190        // Average degree
191        let total_degree: u32 = self
192            .accounts
193            .iter()
194            .map(|a| a.in_degree as u32 + a.out_degree as u32)
195            .sum();
196        self.statistics.avg_degree = if n > 0 {
197            total_degree as f64 / n as f64
198        } else {
199            0.0
200        };
201
202        // Max degree
203        self.statistics.max_degree = self
204            .accounts
205            .iter()
206            .map(|a| a.in_degree as u32 + a.out_degree as u32)
207            .max()
208            .unwrap_or(0);
209
210        // Total flow and average confidence
211        let mut total_amount = 0.0;
212        let mut total_confidence = 0.0;
213        for flow in &self.flows {
214            total_amount += flow.amount.to_f64().abs();
215            total_confidence += flow.confidence as f64;
216        }
217        self.statistics.total_flow_amount = total_amount;
218        self.statistics.avg_confidence = if !self.flows.is_empty() {
219            total_confidence / self.flows.len() as f64
220        } else {
221            0.0
222        };
223
224        // Count flagged accounts
225        self.statistics.suspense_account_count = self
226            .accounts
227            .iter()
228            .filter(|a| a.flags.has(AccountFlags::IS_SUSPENSE_ACCOUNT))
229            .count();
230        self.statistics.gaap_violation_count = self
231            .accounts
232            .iter()
233            .filter(|a| a.flags.has(AccountFlags::HAS_GAAP_VIOLATION))
234            .count();
235        self.statistics.fraud_pattern_count = self
236            .accounts
237            .iter()
238            .filter(|a| a.flags.has(AccountFlags::HAS_FRAUD_PATTERN))
239            .count();
240
241        self.statistics.last_updated = HybridTimestamp::now();
242    }
243
244    /// Get neighbors of an account.
245    pub fn neighbors(&self, account_index: u16, direction: FlowDirection) -> Vec<u16> {
246        let mut result = Vec::new();
247        let idx = account_index as usize;
248
249        match direction {
250            FlowDirection::Outflow => {
251                if idx < self.adjacency_out.len() {
252                    for &(target, _) in &self.adjacency_out[idx] {
253                        if !result.contains(&target) {
254                            result.push(target);
255                        }
256                    }
257                }
258            }
259            FlowDirection::Inflow => {
260                if idx < self.adjacency_in.len() {
261                    for &(source, _) in &self.adjacency_in[idx] {
262                        if !result.contains(&source) {
263                            result.push(source);
264                        }
265                    }
266                }
267            }
268            FlowDirection::Both => {
269                result.extend(self.neighbors(account_index, FlowDirection::Outflow));
270                for neighbor in self.neighbors(account_index, FlowDirection::Inflow) {
271                    if !result.contains(&neighbor) {
272                        result.push(neighbor);
273                    }
274                }
275            }
276        }
277
278        result
279    }
280
281    /// Get all edges as GraphEdge structs (for algorithms).
282    pub fn edges(&self) -> Vec<GraphEdge> {
283        self.aggregated_flows
284            .iter()
285            .map(|(&(from, to), agg)| GraphEdge {
286                from,
287                to,
288                weight: agg.total_amount,
289            })
290            .collect()
291    }
292
293    /// Get account by index.
294    pub fn get_account(&self, index: u16) -> Option<&AccountNode> {
295        self.accounts.get(index as usize)
296    }
297
298    /// Get account metadata by index.
299    pub fn get_metadata(&self, index: u16) -> Option<&AccountMetadata> {
300        self.account_metadata.get(&index)
301    }
302
303    /// Find account by code.
304    pub fn find_by_code(&self, code: &str) -> Option<u16> {
305        for (idx, meta) in &self.account_metadata {
306            if meta.code == code {
307                return Some(*idx);
308            }
309        }
310        None
311    }
312
313    /// Find account by name (partial match).
314    pub fn find_by_name(&self, name: &str) -> Vec<u16> {
315        let lower_name = name.to_lowercase();
316        self.account_metadata
317            .iter()
318            .filter(|(_, meta)| meta.name.to_lowercase().contains(&lower_name))
319            .map(|(&idx, _)| idx)
320            .collect()
321    }
322
323    /// Get accounts by type.
324    pub fn accounts_by_type(&self, account_type: AccountType) -> Vec<u16> {
325        self.accounts
326            .iter()
327            .filter(|a| a.account_type == account_type)
328            .map(|a| a.index)
329            .collect()
330    }
331
332    /// Calculate PageRank for all nodes.
333    /// Uses power iteration method.
334    pub fn calculate_pagerank(&mut self, damping: f64, iterations: usize) {
335        let n = self.accounts.len();
336        if n == 0 {
337            return;
338        }
339
340        let mut pagerank = vec![1.0 / n as f64; n];
341        let mut new_pagerank = vec![0.0; n];
342
343        for _ in 0..iterations {
344            new_pagerank.fill((1.0 - damping) / n as f64);
345
346            for (i, account) in self.accounts.iter().enumerate() {
347                let out_degree = account.out_degree as usize;
348                if out_degree > 0 {
349                    let contribution = damping * pagerank[i] / out_degree as f64;
350                    for &(target, _) in &self.adjacency_out[i] {
351                        new_pagerank[target as usize] += contribution;
352                    }
353                } else {
354                    // Dangling node: distribute evenly
355                    let contribution = damping * pagerank[i] / n as f64;
356                    for pr in new_pagerank.iter_mut() {
357                        *pr += contribution;
358                    }
359                }
360            }
361
362            std::mem::swap(&mut pagerank, &mut new_pagerank);
363        }
364
365        // Store results
366        for (i, account) in self.accounts.iter_mut().enumerate() {
367            account.pagerank = pagerank[i] as f32;
368        }
369    }
370
371    /// Create a snapshot of current state for visualization.
372    pub fn snapshot(&self) -> NetworkSnapshot {
373        NetworkSnapshot {
374            timestamp: HybridTimestamp::now(),
375            node_count: self.accounts.len(),
376            edge_count: self.flows.len(),
377            unique_edges: self.aggregated_flows.len(),
378            total_flow: self.statistics.total_flow_amount,
379            avg_confidence: self.statistics.avg_confidence,
380            suspense_count: self.statistics.suspense_account_count,
381            violation_count: self.statistics.gaap_violation_count,
382            fraud_count: self.statistics.fraud_pattern_count,
383        }
384    }
385
386    /// Compute PageRank scores without modifying the network.
387    /// Returns a vector of PageRank scores (one per account).
388    pub fn compute_pagerank(&self, iterations: usize, damping: f64) -> Vec<f64> {
389        let n = self.accounts.len();
390        if n == 0 {
391            return Vec::new();
392        }
393
394        let mut pagerank = vec![1.0 / n as f64; n];
395        let mut new_pagerank = vec![0.0; n];
396
397        for _ in 0..iterations {
398            new_pagerank.fill((1.0 - damping) / n as f64);
399
400            for (i, account) in self.accounts.iter().enumerate() {
401                let out_degree = account.out_degree as usize;
402                if out_degree > 0 {
403                    let contribution = damping * pagerank[i] / out_degree as f64;
404                    for &(target, _) in &self.adjacency_out[i] {
405                        new_pagerank[target as usize] += contribution;
406                    }
407                } else {
408                    // Dangling node: distribute evenly
409                    let contribution = damping * pagerank[i] / n as f64;
410                    for pr in new_pagerank.iter_mut() {
411                        *pr += contribution;
412                    }
413                }
414            }
415
416            std::mem::swap(&mut pagerank, &mut new_pagerank);
417        }
418
419        pagerank
420    }
421}
422
423/// Lightweight snapshot of network state for UI updates.
424#[derive(Debug, Clone)]
425pub struct NetworkSnapshot {
426    /// Timestamp of the snapshot.
427    pub timestamp: HybridTimestamp,
428    /// Number of account nodes.
429    pub node_count: usize,
430    /// Number of transaction edges.
431    pub edge_count: usize,
432    /// Number of unique account pairs.
433    pub unique_edges: usize,
434    /// Total monetary flow in the network.
435    pub total_flow: f64,
436    /// Average confidence across all flows.
437    pub avg_confidence: f64,
438    /// Number of suspense accounts detected.
439    pub suspense_count: usize,
440    /// Number of GAAP violations.
441    pub violation_count: usize,
442    /// Number of fraud patterns.
443    pub fraud_count: usize,
444}
445
446/// GPU-compatible network header structure.
447/// Used for kernel dispatch.
448#[derive(Debug, Clone, Copy, Archive, Serialize, Deserialize)]
449#[repr(C, align(128))]
450pub struct GpuNetworkHeader {
451    /// Network ID (first 8 bytes of UUID)
452    pub network_id: u64,
453    /// Entity ID (first 8 bytes of UUID)
454    pub entity_id: u64,
455    /// Fiscal year
456    pub fiscal_year: u16,
457    /// Fiscal period
458    pub fiscal_period: u8,
459    /// Number of accounts (max 256 for GPU)
460    pub account_count: u8,
461    /// Number of flows
462    pub flow_count: u32,
463    /// Padding
464    pub _pad1: [u8; 4],
465    /// Period start timestamp
466    pub period_start: HybridTimestamp,
467    /// Period end timestamp
468    pub period_end: HybridTimestamp,
469    /// Network density
470    pub density: f32,
471    /// Average degree
472    pub avg_degree: f32,
473    /// Reserved
474    pub _reserved: [u8; 72],
475}
476
477impl From<&AccountingNetwork> for GpuNetworkHeader {
478    fn from(network: &AccountingNetwork) -> Self {
479        Self {
480            network_id: network.id.as_u128() as u64,
481            entity_id: network.entity_id.as_u128() as u64,
482            fiscal_year: network.fiscal_year,
483            fiscal_period: network.fiscal_period,
484            account_count: network.accounts.len().min(256) as u8,
485            flow_count: network.flows.len() as u32,
486            _pad1: [0; 4],
487            period_start: network.period_start,
488            period_end: network.period_end,
489            density: network.statistics.density as f32,
490            avg_degree: network.statistics.avg_degree as f32,
491            _reserved: [0; 72],
492        }
493    }
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499    use crate::models::Decimal128;
500
501    fn create_test_network() -> AccountingNetwork {
502        let mut network = AccountingNetwork::new(Uuid::new_v4(), 2024, 1);
503
504        // Add accounts
505        let cash = AccountNode::new(Uuid::new_v4(), AccountType::Asset, 0);
506        let ar = AccountNode::new(Uuid::new_v4(), AccountType::Asset, 1);
507        let revenue = AccountNode::new(Uuid::new_v4(), AccountType::Revenue, 2);
508
509        network.add_account(cash, AccountMetadata::new("1100", "Cash"));
510        network.add_account(ar, AccountMetadata::new("1200", "Accounts Receivable"));
511        network.add_account(revenue, AccountMetadata::new("4000", "Sales Revenue"));
512
513        // Add flows: Revenue -> A/R -> Cash
514        let flow1 = TransactionFlow::new(
515            2, // Revenue (credit side is source in double-entry)
516            1, // A/R
517            Decimal128::from_f64(1000.0),
518            Uuid::new_v4(),
519            HybridTimestamp::now(),
520        );
521        let flow2 = TransactionFlow::new(
522            1, // A/R
523            0, // Cash
524            Decimal128::from_f64(1000.0),
525            Uuid::new_v4(),
526            HybridTimestamp::now(),
527        );
528
529        network.add_flow(flow1);
530        network.add_flow(flow2);
531
532        network
533    }
534
535    #[test]
536    fn test_network_creation() {
537        let network = create_test_network();
538        assert_eq!(network.accounts.len(), 3);
539        assert_eq!(network.flows.len(), 2);
540        assert_eq!(network.aggregated_flows.len(), 2);
541    }
542
543    #[test]
544    fn test_neighbors() {
545        let network = create_test_network();
546
547        // A/R (index 1) should have:
548        // - Inflow from Revenue (2)
549        // - Outflow to Cash (0)
550        let in_neighbors = network.neighbors(1, FlowDirection::Inflow);
551        let out_neighbors = network.neighbors(1, FlowDirection::Outflow);
552
553        assert!(in_neighbors.contains(&2));
554        assert!(out_neighbors.contains(&0));
555    }
556
557    #[test]
558    fn test_pagerank() {
559        let mut network = create_test_network();
560        network.calculate_pagerank(0.85, 20);
561
562        // Cash (end of chain) should have highest PageRank
563        let cash_pr = network.accounts[0].pagerank;
564        let revenue_pr = network.accounts[2].pagerank;
565        assert!(cash_pr > revenue_pr);
566    }
567
568    #[test]
569    fn test_gpu_header_size() {
570        let size = std::mem::size_of::<GpuNetworkHeader>();
571        assert!(
572            size >= 128,
573            "GpuNetworkHeader should be at least 128 bytes, got {}",
574            size
575        );
576        assert!(
577            size.is_multiple_of(128),
578            "GpuNetworkHeader should be 128-byte aligned, got {}",
579            size
580        );
581    }
582}