1#![allow(dead_code)]
15type Result<T, E = torsh_core::error::TorshError> = std::result::Result<T, E>;
18
19use crate::parameter::Parameter;
20use crate::{GraphData, GraphLayer};
21use torsh_tensor::{
22 creation::{randn, zeros},
23 Tensor,
24};
25
26use scirs2_core::ndarray::{Array1, Array2, ArrayView1, Axis};
28use std::collections::HashMap;
29use std::sync::Arc;
30
31#[derive(Debug)]
38pub struct MPNNConv {
39 in_features: usize,
40 out_features: usize,
41 edge_features: usize,
42 message_hidden_dim: usize,
43 update_hidden_dim: usize,
44
45 message_layer1: Parameter,
47 message_layer2: Parameter,
48 message_bias1: Option<Parameter>,
49 message_bias2: Option<Parameter>,
50
51 update_layer1: Parameter,
53 update_layer2: Parameter,
54 update_bias1: Option<Parameter>,
55 update_bias2: Option<Parameter>,
56
57 edge_embedding: Option<Parameter>,
59
60 aggregation_type: AggregationType,
61}
62
63#[derive(Debug, Clone, Copy)]
65pub enum AggregationType {
66 Sum,
67 Mean,
68 Max,
69 Attention,
70}
71
72impl MPNNConv {
73 pub fn new(
75 in_features: usize,
76 out_features: usize,
77 edge_features: usize,
78 message_hidden_dim: usize,
79 update_hidden_dim: usize,
80 aggregation_type: AggregationType,
81 bias: bool,
82 ) -> Result<Self> {
83 let message_input_dim = 2 * in_features + edge_features;
85 let message_layer1 = Parameter::new(randn(&[message_input_dim, message_hidden_dim])?);
86 let message_layer2 = Parameter::new(randn(&[message_hidden_dim, out_features])?);
87
88 let message_bias1 = if bias {
89 Some(Parameter::new(zeros(&[message_hidden_dim])?))
90 } else {
91 None
92 };
93
94 let message_bias2 = if bias {
95 Some(Parameter::new(zeros(&[out_features])?))
96 } else {
97 None
98 };
99
100 let update_input_dim = in_features + out_features;
102 let update_layer1 = Parameter::new(randn(&[update_input_dim, update_hidden_dim])?);
103 let update_layer2 = Parameter::new(randn(&[update_hidden_dim, out_features])?);
104
105 let update_bias1 = if bias {
106 Some(Parameter::new(zeros(&[update_hidden_dim])?))
107 } else {
108 None
109 };
110
111 let update_bias2 = if bias {
112 Some(Parameter::new(zeros(&[out_features])?))
113 } else {
114 None
115 };
116
117 let edge_embedding = if edge_features > 0 {
119 Some(Parameter::new(randn(&[edge_features, edge_features])?))
120 } else {
121 None
122 };
123
124 Ok(Self {
125 in_features,
126 out_features,
127 edge_features,
128 message_hidden_dim,
129 update_hidden_dim,
130 message_layer1,
131 message_layer2,
132 message_bias1,
133 message_bias2,
134 update_layer1,
135 update_layer2,
136 update_bias1,
137 update_bias2,
138 edge_embedding,
139 aggregation_type,
140 })
141 }
142
143 pub fn forward(&self, graph: &GraphData) -> Result<GraphData> {
145 let num_nodes = graph.num_nodes;
146 let edge_data = crate::utils::tensor_to_vec2::<f32>(&graph.edge_index)?;
147 let _num_edges = edge_data[0].len();
148
149 let messages = self.compute_messages(graph)?;
151
152 let aggregated = self.aggregate_messages(&messages, &edge_data, num_nodes)?;
154
155 let updated_features = self.update_nodes(&graph.x, &aggregated)?;
157
158 Ok(GraphData {
159 x: updated_features,
160 edge_index: graph.edge_index.clone(),
161 edge_attr: graph.edge_attr.clone(),
162 batch: graph.batch.clone(),
163 num_nodes: graph.num_nodes,
164 num_edges: graph.num_edges,
165 })
166 }
167
168 fn compute_messages(&self, graph: &GraphData) -> Result<Tensor> {
170 let edge_data = crate::utils::tensor_to_vec2::<f32>(&graph.edge_index)?;
171 let num_edges = edge_data[0].len();
172
173 let mut all_messages = Vec::new();
174
175 for edge_idx in 0..num_edges {
176 let src_idx = edge_data[0][edge_idx] as usize;
177 let dst_idx = edge_data[1][edge_idx] as usize;
178
179 let h_i = graph
181 .x
182 .slice_tensor(0, src_idx, src_idx + 1)?
183 .squeeze_tensor(0)?;
184 let h_j = graph
185 .x
186 .slice_tensor(0, dst_idx, dst_idx + 1)?
187 .squeeze_tensor(0)?;
188
189 let edge_feat = if let Some(ref edge_attr) = graph.edge_attr {
191 if self.edge_features > 0 {
192 let e_ij = edge_attr
193 .slice_tensor(0, edge_idx, edge_idx + 1)?
194 .squeeze_tensor(0)?;
195
196 if let Some(ref edge_emb) = self.edge_embedding {
198 let e_ij_2d = e_ij.unsqueeze_tensor(0)?;
200 e_ij_2d.matmul(&edge_emb.clone_data())?.squeeze_tensor(0)?
201 } else {
202 e_ij
203 }
204 } else {
205 zeros(&[self.edge_features])?
206 }
207 } else {
208 zeros(&[self.edge_features])?
209 };
210
211 let message_input = Tensor::cat(&[&h_i, &h_j, &edge_feat], 0)?;
213
214 let message_input_2d = message_input.unsqueeze_tensor(0)?;
217 let mut message = message_input_2d
218 .matmul(&self.message_layer1.clone_data())?
219 .squeeze_tensor(0)?;
220
221 if let Some(ref bias1) = self.message_bias1 {
222 message = message.add(&bias1.clone_data())?;
223 }
224
225 message = message.maximum(&zeros(&message.shape().dims())?)?;
227
228 let message_2d = message.unsqueeze_tensor(0)?;
230 message = message_2d
231 .matmul(&self.message_layer2.clone_data())?
232 .squeeze_tensor(0)?;
233
234 if let Some(ref bias2) = self.message_bias2 {
235 message = message.add(&bias2.clone_data())?;
236 }
237
238 all_messages.push(message);
239 }
240
241 if all_messages.is_empty() {
243 Ok(zeros(&[0, self.out_features])?)
244 } else {
245 let mut message_data = Vec::new();
247 for msg in &all_messages {
248 let msg_vec = msg.to_vec()?;
249 message_data.extend(msg_vec);
250 }
251
252 Ok(torsh_tensor::creation::from_vec(
253 message_data,
254 &[all_messages.len(), self.out_features],
255 torsh_core::device::DeviceType::Cpu,
256 )?)
257 }
258 }
259
260 fn aggregate_messages(
262 &self,
263 messages: &Tensor,
264 edge_data: &[Vec<f32>],
265 num_nodes: usize,
266 ) -> Result<Tensor> {
267 let mut aggregated = zeros(&[num_nodes, self.out_features])?;
268 let num_edges = edge_data[0].len();
269
270 if num_edges == 0 {
271 return Ok(aggregated);
272 }
273
274 match self.aggregation_type {
275 AggregationType::Sum | AggregationType::Mean => {
276 let mut node_counts = vec![0; num_nodes];
277
278 for edge_idx in 0..num_edges {
280 let dst_idx = edge_data[1][edge_idx] as usize;
281 if dst_idx < num_nodes {
282 let message = messages
283 .slice_tensor(0, edge_idx, edge_idx + 1)?
284 .squeeze_tensor(0)?;
285
286 let current = aggregated
287 .slice_tensor(0, dst_idx, dst_idx + 1)?
288 .squeeze_tensor(0)?;
289 let updated = current.add(&message)?;
290
291 aggregated
292 .slice_tensor(0, dst_idx, dst_idx + 1)?
293 .copy_(&updated.unsqueeze_tensor(0)?)?;
294
295 node_counts[dst_idx] += 1;
296 }
297 }
298
299 if matches!(self.aggregation_type, AggregationType::Mean) {
301 for node in 0..num_nodes {
302 if node_counts[node] > 0 {
303 let current = aggregated
304 .slice_tensor(0, node, node + 1)?
305 .squeeze_tensor(0)?;
306 let normalized = current.div_scalar(node_counts[node] as f32)?;
307
308 aggregated
309 .slice_tensor(0, node, node + 1)?
310 .copy_(&normalized.unsqueeze_tensor(0)?)?;
311 }
312 }
313 }
314 }
315
316 AggregationType::Max => {
317 aggregated.fill_(-1e9_f32)?;
319
320 for edge_idx in 0..num_edges {
321 let dst_idx = edge_data[1][edge_idx] as usize;
322 if dst_idx < num_nodes {
323 let message = messages
324 .slice_tensor(0, edge_idx, edge_idx + 1)?
325 .squeeze_tensor(0)?;
326
327 let current = aggregated
328 .slice_tensor(0, dst_idx, dst_idx + 1)?
329 .squeeze_tensor(0)?;
330 let updated = current.maximum(&message)?;
331
332 aggregated
333 .slice_tensor(0, dst_idx, dst_idx + 1)?
334 .copy_(&updated.unsqueeze_tensor(0)?)?;
335 }
336 }
337
338 let aggregated_data = aggregated.to_vec()?;
341 let filtered_data: Vec<f32> = aggregated_data
342 .iter()
343 .map(|&x| if x <= -1e8_f32 { 0.0 } else { x })
344 .collect();
345 aggregated = Tensor::from_data(
346 filtered_data,
347 aggregated.shape().dims().to_vec(),
348 aggregated.device(),
349 )?;
350 }
351
352 AggregationType::Attention => {
353 return self.aggregate_messages(messages, edge_data, num_nodes);
356 }
357 }
358
359 Ok(aggregated)
360 }
361
362 fn update_nodes(
364 &self,
365 current_states: &Tensor,
366 aggregated_messages: &Tensor,
367 ) -> Result<Tensor> {
368 let num_nodes = current_states.shape().dims()[0];
369 let mut updated_states = zeros(&[num_nodes, self.out_features])?;
370
371 for node in 0..num_nodes {
372 let h_i = current_states
374 .slice_tensor(0, node, node + 1)?
375 .squeeze_tensor(0)?;
376
377 let m_i = aggregated_messages
379 .slice_tensor(0, node, node + 1)?
380 .squeeze_tensor(0)?;
381
382 let update_input = Tensor::cat(&[&h_i, &m_i], 0)?;
384
385 let update_input_2d = update_input.unsqueeze_tensor(0)?;
388 let mut updated = update_input_2d
389 .matmul(&self.update_layer1.clone_data())?
390 .squeeze_tensor(0)?;
391
392 if let Some(ref bias1) = self.update_bias1 {
393 updated = updated.add(&bias1.clone_data())?;
394 }
395
396 let mut updated_temp = updated;
398 updated_temp.clamp_(0.0, f32::INFINITY)?;
399 updated = updated_temp;
400
401 let updated_2d = updated.unsqueeze_tensor(0)?;
403 updated = updated_2d
404 .matmul(&self.update_layer2.clone_data())?
405 .squeeze_tensor(0)?;
406
407 if let Some(ref bias2) = self.update_bias2 {
408 updated = updated.add(&bias2.clone_data())?;
409 }
410
411 let updated_data = updated.to_vec()?;
413 for (i, &value) in updated_data.iter().enumerate() {
414 updated_states.set_item(&[node, i], value)?;
415 }
416 }
417
418 Ok(updated_states)
419 }
420}
421
422impl GraphLayer for MPNNConv {
423 fn forward(&self, graph: &GraphData) -> Result<GraphData> {
424 self.forward(graph)
425 }
426
427 fn parameters(&self) -> Vec<Tensor> {
428 let mut params = vec![
429 self.message_layer1.clone_data(),
430 self.message_layer2.clone_data(),
431 self.update_layer1.clone_data(),
432 self.update_layer2.clone_data(),
433 ];
434
435 if let Some(ref bias1) = self.message_bias1 {
436 params.push(bias1.clone_data());
437 }
438
439 if let Some(ref bias2) = self.message_bias2 {
440 params.push(bias2.clone_data());
441 }
442
443 if let Some(ref bias1) = self.update_bias1 {
444 params.push(bias1.clone_data());
445 }
446
447 if let Some(ref bias2) = self.update_bias2 {
448 params.push(bias2.clone_data());
449 }
450
451 if let Some(ref edge_emb) = self.edge_embedding {
452 params.push(edge_emb.clone_data());
453 }
454
455 params
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use super::*;
462 use torsh_core::device::DeviceType;
463 use torsh_tensor::creation::from_vec;
464
465 #[test]
466 fn test_mpnn_creation() {
467 let mpnn = MPNNConv::new(8, 16, 4, 32, 32, AggregationType::Sum, true);
468 let params = mpnn.expect("operation should succeed").parameters();
469
470 assert!(params.len() >= 4); assert!(params.len() <= 9); }
475
476 #[test]
477 fn test_mpnn_forward() {
478 let mpnn = MPNNConv::new(3, 8, 2, 16, 16, AggregationType::Mean, false);
479
480 let x = from_vec(
482 vec![
483 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, ],
487 &[3, 3],
488 DeviceType::Cpu,
489 )
490 .expect("operation should succeed");
491
492 let edge_index = from_vec(vec![0.0, 1.0, 2.0, 1.0, 2.0, 0.0], &[2, 3], DeviceType::Cpu)
493 .expect("from vec should succeed");
494
495 let edge_attr = from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6], &[3, 2], DeviceType::Cpu)
496 .expect("from vec should succeed");
497
498 let graph = GraphData::new(x, edge_index).with_edge_attr(edge_attr);
499
500 let output = mpnn
501 .expect("operation should succeed")
502 .forward(&graph)
503 .expect("operation should succeed");
504 assert_eq!(output.x.shape().dims(), &[3, 8]);
505 assert_eq!(output.num_nodes, 3);
506 }
507
508 #[test]
509 fn test_mpnn_aggregation_types() {
510 let mpnn_sum = MPNNConv::new(2, 4, 0, 8, 8, AggregationType::Sum, false);
511 let mpnn_mean = MPNNConv::new(2, 4, 0, 8, 8, AggregationType::Mean, false);
512 let mpnn_max = MPNNConv::new(2, 4, 0, 8, 8, AggregationType::Max, false);
513
514 let x = from_vec(vec![1.0, 2.0, 3.0, 4.0], &[2, 2], DeviceType::Cpu)
516 .expect("from vec should succeed");
517
518 let edge_index =
519 from_vec(vec![0.0, 1.0], &[2, 1], DeviceType::Cpu).expect("from vec should succeed");
520
521 let graph = GraphData::new(x, edge_index);
522
523 let _output_sum = mpnn_sum.expect("operation should succeed").forward(&graph);
525 let _output_mean = mpnn_mean.expect("operation should succeed").forward(&graph);
526 let _output_max = mpnn_max.expect("operation should succeed").forward(&graph);
527 }
528
529 #[test]
530 fn test_mpnn_empty_graph() {
531 let mpnn = MPNNConv::new(3, 8, 0, 16, 16, AggregationType::Sum, false);
532
533 let x = from_vec(vec![1.0, 2.0, 3.0], &[1, 3], DeviceType::Cpu)
535 .expect("from vec should succeed");
536
537 let edge_index = zeros(&[2, 0]).expect("zeros should succeed");
538 let graph = GraphData::new(x, edge_index);
539
540 let output = mpnn
541 .expect("operation should succeed")
542 .forward(&graph)
543 .expect("operation should succeed");
544 assert_eq!(output.x.shape().dims(), &[1, 8]);
545 assert_eq!(output.num_nodes, 1);
546 }
547}
548
549#[derive(Debug, Clone)]
555pub struct AdvancedSIMDMPNN {
556 in_features: usize,
558 out_features: usize,
559 edge_features: usize,
560
561 simd_chunk_size: usize,
563 memory_efficient: bool,
564 use_attention: bool,
565 num_attention_heads: usize,
566
567 message_weights: Array2<f64>,
569 update_weights: Array2<f64>,
570 attention_weights: Option<Array2<f64>>,
571
572 message_bias: Option<Array1<f64>>,
574 update_bias: Option<Array1<f64>>,
575
576 aggregation_config: AdvancedAggregationConfig,
578
579 performance_cache: PerformanceCache,
581}
582
583#[derive(Debug, Clone)]
585pub struct AdvancedAggregationConfig {
586 primary_aggregation: AggregationType,
588 secondary_aggregation: Option<AggregationType>,
590 hierarchical_levels: usize,
592 attention_temperature: f64,
594 dynamic_routing: bool,
596}
597
598#[derive(Debug, Clone)]
600pub struct PerformanceCache {
601 adjacency_patterns: HashMap<String, Arc<Array2<f64>>>,
603 degree_stats: HashMap<usize, (f64, f64)>, message_cache: HashMap<String, Arc<Array2<f64>>>,
607 simd_speedup_factor: f64,
609}
610
611impl AdvancedSIMDMPNN {
612 pub fn new(
614 in_features: usize,
615 out_features: usize,
616 edge_features: usize,
617 config: AdvancedMPNNConfig,
618 ) -> Self {
619 let message_input_dim = 2 * in_features + edge_features;
620 let hidden_dim = config.hidden_dim;
621
622 let message_weights = Self::initialize_weights_simd(message_input_dim, hidden_dim);
624 let update_weights = Self::initialize_weights_simd(hidden_dim + in_features, out_features);
625
626 let attention_weights = if config.use_attention {
628 Some(Self::initialize_weights_simd(
629 hidden_dim,
630 config.num_attention_heads * hidden_dim,
631 ))
632 } else {
633 None
634 };
635
636 let message_bias = if config.use_bias {
638 Some(Array1::zeros(hidden_dim))
639 } else {
640 None
641 };
642
643 let update_bias = if config.use_bias {
644 Some(Array1::zeros(out_features))
645 } else {
646 None
647 };
648
649 Self {
650 in_features,
651 out_features,
652 edge_features,
653 simd_chunk_size: config.simd_chunk_size,
654 memory_efficient: config.memory_efficient,
655 use_attention: config.use_attention,
656 num_attention_heads: config.num_attention_heads,
657 message_weights,
658 update_weights,
659 attention_weights,
660 message_bias,
661 update_bias,
662 aggregation_config: config.aggregation_config,
663 performance_cache: PerformanceCache::new(),
664 }
665 }
666
667 pub fn forward_simd(&mut self, graph: &GraphData) -> Result<GraphData> {
673 let batch_size = graph.num_nodes;
674
675 if batch_size == 0 {
676 return Ok(graph.clone());
677 }
678
679 let node_features = self.tensor_to_array2(&graph.x)?;
681 let edge_indices = self.extract_edge_indices(&graph.edge_index);
682 let edge_attributes = match graph.edge_attr.as_ref() {
683 Some(attr) => Some(self.tensor_to_array2(attr)?),
684 None => None,
685 };
686
687 let messages = if self.memory_efficient && batch_size > self.simd_chunk_size {
689 self.compute_messages_chunked(&node_features, &edge_indices, &edge_attributes)
690 } else {
691 self.compute_messages_vectorized(&node_features, &edge_indices, &edge_attributes)
692 };
693
694 let aggregated_messages =
696 self.aggregate_messages_simd(&messages, &edge_indices, batch_size);
697
698 let updated_features = self.update_nodes_simd(&node_features, &aggregated_messages);
700
701 let output_tensor = self.array2_to_tensor(&updated_features)?;
703
704 self.update_performance_cache(batch_size, edge_indices.len());
706
707 Ok(GraphData::new(output_tensor, graph.edge_index.clone())
708 .with_edge_attr_opt(graph.edge_attr.clone()))
709 }
710
711 fn initialize_weights_simd(input_dim: usize, output_dim: usize) -> Array2<f64> {
713 let mut weights = Array2::zeros((input_dim, output_dim));
714 let scale = (2.0 / input_dim as f64).sqrt();
715
716 use std::collections::hash_map::DefaultHasher;
718 use std::hash::{Hash, Hasher};
719
720 for i in 0..input_dim {
721 for j in 0..output_dim {
722 let mut hasher = DefaultHasher::new();
723 (i, j).hash(&mut hasher);
724 let hash_val = hasher.finish();
725 let normalized = (hash_val as f64) / (u64::MAX as f64);
726 weights[[i, j]] = (normalized - 0.5) * 2.0 * scale;
727 }
728 }
729
730 weights
731 }
732
733 fn compute_messages_vectorized(
735 &self,
736 node_features: &Array2<f64>,
737 edge_indices: &[(usize, usize)],
738 edge_attributes: &Option<Array2<f64>>,
739 ) -> Array2<f64> {
740 let num_edges = edge_indices.len();
741 let message_dim = self.message_weights.ncols();
742 let mut messages = Array2::zeros((num_edges, message_dim));
743
744 for (edge_idx, &(src, dst)) in edge_indices.iter().enumerate() {
746 if src < node_features.nrows() && dst < node_features.nrows() {
747 let src_features = node_features.row(src);
749 let dst_features = node_features.row(dst);
750
751 let mut message_input =
752 Vec::with_capacity(self.in_features * 2 + self.edge_features);
753
754 message_input.extend(src_features.iter());
756 message_input.extend(dst_features.iter());
757
758 if let Some(ref edge_attr) = edge_attributes {
760 if edge_idx < edge_attr.nrows() {
761 message_input.extend(edge_attr.row(edge_idx).iter());
762 } else {
763 message_input.resize(message_input.len() + self.edge_features, 0.0);
765 }
766 } else {
767 message_input.resize(message_input.len() + self.edge_features, 0.0);
769 }
770
771 let input_array = Array1::from_vec(message_input);
773 let message = self.compute_message_mlp(&input_array);
774
775 for (i, &val) in message.iter().enumerate() {
777 if i < message_dim {
778 messages[[edge_idx, i]] = val;
779 }
780 }
781 }
782 }
783
784 messages
785 }
786
787 fn compute_messages_chunked(
789 &self,
790 node_features: &Array2<f64>,
791 edge_indices: &[(usize, usize)],
792 edge_attributes: &Option<Array2<f64>>,
793 ) -> Array2<f64> {
794 let num_edges = edge_indices.len();
795 let message_dim = self.message_weights.ncols();
796 let mut messages = Array2::zeros((num_edges, message_dim));
797
798 for chunk_start in (0..num_edges).step_by(self.simd_chunk_size) {
800 let chunk_end = (chunk_start + self.simd_chunk_size).min(num_edges);
801 let chunk_indices = &edge_indices[chunk_start..chunk_end];
802
803 for (local_idx, &(src, dst)) in chunk_indices.iter().enumerate() {
805 let edge_idx = chunk_start + local_idx;
806
807 if src < node_features.nrows() && dst < node_features.nrows() {
808 let message = self.compute_single_message(
809 &node_features.row(src),
810 &node_features.row(dst),
811 edge_attributes.as_ref().and_then(|attr| {
812 if edge_idx < attr.nrows() {
813 Some(attr.row(edge_idx))
814 } else {
815 None
816 }
817 }),
818 );
819
820 for (i, &val) in message.iter().enumerate() {
822 if i < message_dim {
823 messages[[edge_idx, i]] = val;
824 }
825 }
826 }
827 }
828 }
829
830 messages
831 }
832
833 fn compute_message_mlp(&self, input: &Array1<f64>) -> Array1<f64> {
835 let mut hidden = Array1::zeros(self.message_weights.ncols());
837
838 for (i, _row) in self.message_weights.axis_iter(Axis(1)).enumerate() {
840 let dot_product = input
841 .iter()
842 .zip(self.message_weights.axis_iter(Axis(0)))
843 .map(|(&x, weight_col)| x * weight_col[i])
844 .sum::<f64>();
845
846 hidden[i] = dot_product;
847 }
848
849 if let Some(ref bias) = self.message_bias {
851 for i in 0..hidden.len() {
852 if i < bias.len() {
853 hidden[i] += bias[i];
854 }
855 }
856 }
857
858 hidden.mapv_inplace(|x| x.max(0.0));
860
861 hidden
863 }
864
865 fn compute_single_message(
867 &self,
868 src_features: &ArrayView1<f64>,
869 dst_features: &ArrayView1<f64>,
870 edge_features: Option<ArrayView1<f64>>,
871 ) -> Array1<f64> {
872 let mut message_input = Vec::with_capacity(self.in_features * 2 + self.edge_features);
873
874 message_input.extend(src_features.iter());
876 message_input.extend(dst_features.iter());
877
878 if let Some(edge_feat) = edge_features {
879 message_input.extend(edge_feat.iter());
880 } else {
881 message_input.resize(message_input.len() + self.edge_features, 0.0);
882 }
883
884 let input_array = Array1::from_vec(message_input);
885 self.compute_message_mlp(&input_array)
886 }
887
888 fn aggregate_messages_simd(
890 &self,
891 messages: &Array2<f64>,
892 edge_indices: &[(usize, usize)],
893 num_nodes: usize,
894 ) -> Array2<f64> {
895 let message_dim = messages.ncols();
896 let mut aggregated = Array2::zeros((num_nodes, message_dim));
897
898 match self.aggregation_config.primary_aggregation {
899 AggregationType::Sum => {
900 self.aggregate_sum_simd(messages, edge_indices, &mut aggregated)
901 }
902 AggregationType::Mean => {
903 self.aggregate_mean_simd(messages, edge_indices, &mut aggregated)
904 }
905 AggregationType::Max => {
906 self.aggregate_max_simd(messages, edge_indices, &mut aggregated)
907 }
908 AggregationType::Attention => {
909 self.aggregate_attention_simd(messages, edge_indices, &mut aggregated)
910 }
911 }
912
913 aggregated
914 }
915
916 fn aggregate_sum_simd(
918 &self,
919 messages: &Array2<f64>,
920 edge_indices: &[(usize, usize)],
921 aggregated: &mut Array2<f64>,
922 ) {
923 for (edge_idx, &(_, dst)) in edge_indices.iter().enumerate() {
924 if dst < aggregated.nrows() && edge_idx < messages.nrows() {
925 let message = messages.row(edge_idx);
926 let mut dst_row = aggregated.row_mut(dst);
927
928 for (i, &msg_val) in message.iter().enumerate() {
930 if i < dst_row.len() {
931 dst_row[i] += msg_val;
932 }
933 }
934 }
935 }
936 }
937
938 fn aggregate_mean_simd(
940 &self,
941 messages: &Array2<f64>,
942 edge_indices: &[(usize, usize)],
943 aggregated: &mut Array2<f64>,
944 ) {
945 self.aggregate_sum_simd(messages, edge_indices, aggregated);
947
948 let mut neighbor_counts = vec![0usize; aggregated.nrows()];
950 for &(_, dst) in edge_indices {
951 if dst < neighbor_counts.len() {
952 neighbor_counts[dst] += 1;
953 }
954 }
955
956 for (node_idx, count) in neighbor_counts.iter().enumerate() {
958 if *count > 0 && node_idx < aggregated.nrows() {
959 let count_f64 = *count as f64;
960 let mut row = aggregated.row_mut(node_idx);
961 row.mapv_inplace(|x| x / count_f64);
962 }
963 }
964 }
965
966 fn aggregate_max_simd(
968 &self,
969 messages: &Array2<f64>,
970 edge_indices: &[(usize, usize)],
971 aggregated: &mut Array2<f64>,
972 ) {
973 aggregated.fill(f64::NEG_INFINITY);
975
976 for (edge_idx, &(_, dst)) in edge_indices.iter().enumerate() {
977 if dst < aggregated.nrows() && edge_idx < messages.nrows() {
978 let message = messages.row(edge_idx);
979 let mut dst_row = aggregated.row_mut(dst);
980
981 for (i, &msg_val) in message.iter().enumerate() {
983 if i < dst_row.len() {
984 dst_row[i] = dst_row[i].max(msg_val);
985 }
986 }
987 }
988 }
989
990 aggregated.mapv_inplace(|x| if x == f64::NEG_INFINITY { 0.0 } else { x });
992 }
993
994 fn aggregate_attention_simd(
996 &self,
997 messages: &Array2<f64>,
998 edge_indices: &[(usize, usize)],
999 aggregated: &mut Array2<f64>,
1000 ) {
1001 if let Some(ref attention_weights) = self.attention_weights {
1002 let attention_scores = self.compute_attention_scores_simd(messages, attention_weights);
1004
1005 for (edge_idx, &(_, dst)) in edge_indices.iter().enumerate() {
1007 if dst < aggregated.nrows() && edge_idx < messages.nrows() {
1008 let message = messages.row(edge_idx);
1009 let attention_weight = attention_scores.get(edge_idx).copied().unwrap_or(0.0);
1010 let mut dst_row = aggregated.row_mut(dst);
1011
1012 for (i, &msg_val) in message.iter().enumerate() {
1014 if i < dst_row.len() {
1015 dst_row[i] += msg_val * attention_weight;
1016 }
1017 }
1018 }
1019 }
1020 } else {
1021 self.aggregate_sum_simd(messages, edge_indices, aggregated);
1023 }
1024 }
1025
1026 fn compute_attention_scores_simd(
1028 &self,
1029 messages: &Array2<f64>,
1030 attention_weights: &Array2<f64>,
1031 ) -> Vec<f64> {
1032 let num_messages = messages.nrows();
1033 let mut scores = Vec::with_capacity(num_messages);
1034
1035 for i in 0..num_messages {
1036 let message = messages.row(i);
1037
1038 let score = message
1040 .iter()
1041 .zip(attention_weights.column(0).iter())
1042 .map(|(&m, &w)| m * w)
1043 .sum::<f64>();
1044
1045 scores.push(score);
1046 }
1047
1048 self.softmax_simd(&mut scores);
1050 scores
1051 }
1052
1053 fn softmax_simd(&self, scores: &mut Vec<f64>) {
1055 if scores.is_empty() {
1056 return;
1057 }
1058
1059 let max_score = scores.iter().copied().fold(f64::NEG_INFINITY, f64::max);
1061
1062 for score in scores.iter_mut() {
1064 *score = (*score - max_score).exp();
1065 }
1066
1067 let sum: f64 = scores.iter().sum();
1069 if sum > 1e-15 {
1070 for score in scores.iter_mut() {
1071 *score /= sum;
1072 }
1073 }
1074 }
1075
1076 fn update_nodes_simd(
1078 &self,
1079 node_features: &Array2<f64>,
1080 aggregated_messages: &Array2<f64>,
1081 ) -> Array2<f64> {
1082 let num_nodes = node_features.nrows();
1083 let output_dim = self.out_features;
1084 let mut updated_features = Array2::zeros((num_nodes, output_dim));
1085
1086 for node_idx in 0..num_nodes {
1087 if node_idx < aggregated_messages.nrows() {
1088 let node_feat = node_features.row(node_idx);
1089 let agg_msg = aggregated_messages.row(node_idx);
1090
1091 let mut update_input = Vec::with_capacity(node_feat.len() + agg_msg.len());
1093 update_input.extend(node_feat.iter());
1094 update_input.extend(agg_msg.iter());
1095
1096 let input_array = Array1::from_vec(update_input);
1097 let updated = self.compute_update_mlp(&input_array);
1098
1099 for (i, &val) in updated.iter().enumerate() {
1101 if i < output_dim {
1102 updated_features[[node_idx, i]] = val;
1103 }
1104 }
1105 }
1106 }
1107
1108 updated_features
1109 }
1110
1111 fn compute_update_mlp(&self, input: &Array1<f64>) -> Array1<f64> {
1113 let mut output = Array1::zeros(self.out_features);
1114
1115 for (i, weight_col) in self.update_weights.axis_iter(Axis(1)).enumerate() {
1117 if i < output.len() {
1118 let dot_product = input
1119 .iter()
1120 .zip(weight_col.iter())
1121 .map(|(&x, &w)| x * w)
1122 .sum::<f64>();
1123
1124 output[i] = dot_product;
1125 }
1126 }
1127
1128 if let Some(ref bias) = self.update_bias {
1130 for i in 0..output.len() {
1131 if i < bias.len() {
1132 output[i] += bias[i];
1133 }
1134 }
1135 }
1136
1137 output.mapv_inplace(|x| x.max(0.0));
1139
1140 output
1141 }
1142
1143 fn tensor_to_array2(&self, tensor: &Tensor) -> Result<Array2<f64>> {
1145 let vec_data = tensor.to_vec()?;
1146 let shape = tensor.shape();
1147 let dims = shape.dims();
1148 if dims.len() != 2 {
1149 return Err(torsh_core::error::TorshError::InvalidArgument(format!(
1150 "tensor_to_array2 requires a 2D tensor, got {dims:?}"
1151 )));
1152 }
1153 let rows = dims[0];
1154 let cols = dims[1];
1155 let data_f64: Vec<f64> = vec_data.iter().map(|&x| x as f64).collect();
1156 Array2::from_shape_vec((rows, cols), data_f64).map_err(|e| {
1157 torsh_core::error::TorshError::InvalidArgument(format!(
1158 "failed to build a {rows}x{cols} array: {e}"
1159 ))
1160 })
1161 }
1162
1163 fn array2_to_tensor(&self, array: &Array2<f64>) -> Result<Tensor> {
1164 let (rows, cols) = array.dim();
1165 let data_f32: Vec<f32> = array.iter().map(|&x| x as f32).collect();
1166
1167 Ok(torsh_tensor::creation::from_vec(
1168 data_f32,
1169 &[rows, cols],
1170 torsh_core::device::DeviceType::Cpu,
1171 )?)
1172 }
1173
1174 fn extract_edge_indices(&self, edge_index: &Tensor) -> Vec<(usize, usize)> {
1175 match edge_index.to_vec() {
1176 Ok(vec_data) => {
1177 let shape = edge_index.shape();
1178 let dims = shape.dims();
1179 if dims.len() == 2 && dims[0] == 2 {
1180 let num_edges = dims[1];
1181 let mut edges = Vec::with_capacity(num_edges);
1182 for i in 0..num_edges {
1183 let src = vec_data[i] as usize;
1184 let dst = vec_data[num_edges + i] as usize;
1185 edges.push((src, dst));
1186 }
1187 edges
1188 } else {
1189 Vec::new()
1190 }
1191 }
1192 Err(_) => Vec::new(),
1193 }
1194 }
1195
1196 fn update_performance_cache(&mut self, num_nodes: usize, num_edges: usize) {
1198 let base_speedup = if num_nodes > self.simd_chunk_size {
1200 2.5 } else {
1202 1.5 };
1204
1205 self.performance_cache.simd_speedup_factor =
1206 base_speedup * (1.0 + (num_edges as f64 / num_nodes as f64).ln());
1207 }
1208}
1209
1210#[derive(Debug, Clone)]
1212pub struct AdvancedMPNNConfig {
1213 pub hidden_dim: usize,
1214 pub use_bias: bool,
1215 pub use_attention: bool,
1216 pub num_attention_heads: usize,
1217 pub simd_chunk_size: usize,
1218 pub memory_efficient: bool,
1219 pub aggregation_config: AdvancedAggregationConfig,
1220}
1221
1222impl Default for AdvancedMPNNConfig {
1223 fn default() -> Self {
1224 Self {
1225 hidden_dim: 128,
1226 use_bias: true,
1227 use_attention: true,
1228 num_attention_heads: 4,
1229 simd_chunk_size: 1024,
1230 memory_efficient: true,
1231 aggregation_config: AdvancedAggregationConfig::default(),
1232 }
1233 }
1234}
1235
1236impl Default for AdvancedAggregationConfig {
1237 fn default() -> Self {
1238 Self {
1239 primary_aggregation: AggregationType::Attention,
1240 secondary_aggregation: Some(AggregationType::Mean),
1241 hierarchical_levels: 2,
1242 attention_temperature: 1.0,
1243 dynamic_routing: true,
1244 }
1245 }
1246}
1247
1248impl PerformanceCache {
1249 fn new() -> Self {
1250 Self {
1251 adjacency_patterns: HashMap::new(),
1252 degree_stats: HashMap::new(),
1253 message_cache: HashMap::new(),
1254 simd_speedup_factor: 1.0,
1255 }
1256 }
1257}