1use crate::{CooTensor, CsrTensor, SparseTensor, TorshResult};
7
8use torsh_core::{Shape, TorshError};
9use torsh_tensor::{
10 creation::{randn, zeros},
11 Tensor,
12};
13
14pub struct GraphConvolution {
19 weight: Tensor,
21 bias: Option<Tensor>,
23 in_features: usize,
25 out_features: usize,
27 add_self_loops: bool,
29 normalize: bool,
31}
32
33impl GraphConvolution {
34 pub fn new(
36 in_features: usize,
37 out_features: usize,
38 use_bias: bool,
39 add_self_loops: bool,
40 normalize: bool,
41 ) -> TorshResult<Self> {
42 let _std_dev = (2.0 / (in_features + out_features) as f32).sqrt();
44 let weight = randn::<f32>(&[in_features, out_features])?;
45
46 let bias = if use_bias {
48 Some(zeros::<f32>(&[out_features])?)
49 } else {
50 None
51 };
52
53 Ok(Self {
54 weight,
55 bias,
56 in_features,
57 out_features,
58 add_self_loops,
59 normalize,
60 })
61 }
62
63 pub fn forward(&self, node_features: &Tensor, adjacency: &CsrTensor) -> TorshResult<Tensor> {
65 let feature_shape = node_features.shape();
67 if feature_shape.ndim() != 2 {
68 return Err(TorshError::InvalidArgument(
69 "Node features must be 2D tensor (num_nodes x in_features)".to_string(),
70 ));
71 }
72
73 let num_nodes = feature_shape.dims()[0];
74 let input_features = feature_shape.dims()[1];
75
76 if input_features != self.in_features {
77 return Err(TorshError::InvalidArgument(format!(
78 "Input features {} don't match layer input features {}",
79 input_features, self.in_features
80 )));
81 }
82
83 let adj_shape = adjacency.shape();
85 if adj_shape.dims() != [num_nodes, num_nodes] {
86 return Err(TorshError::InvalidArgument(
87 "Adjacency matrix must be square and match number of nodes".to_string(),
88 ));
89 }
90
91 let adj_processed = if self.add_self_loops {
93 self.add_self_loops_to_adjacency(adjacency)?
94 } else {
95 adjacency.clone()
96 };
97
98 let adj_normalized = if self.normalize {
100 self.normalize_adjacency(&adj_processed)?
101 } else {
102 adj_processed
103 };
104
105 let transformed_features = zeros::<f32>(&[num_nodes, self.out_features])?;
107 for i in 0..num_nodes {
108 for j in 0..self.out_features {
109 let mut sum = 0.0;
110 for k in 0..self.in_features {
111 sum += node_features.get(&[i, k])? * self.weight.get(&[k, j])?;
112 }
113 transformed_features.set(&[i, j], sum)?;
114 }
115 }
116
117 let output = zeros::<f32>(&[num_nodes, self.out_features])?;
119 for i in 0..num_nodes {
120 let (neighbors, weights) = adj_normalized.get_row(i)?;
121 for j in 0..self.out_features {
122 let mut sum = 0.0;
123 for (&neighbor, &weight) in neighbors.iter().zip(weights.iter()) {
124 sum += weight * transformed_features.get(&[neighbor, j])?;
125 }
126 output.set(&[i, j], sum)?;
127 }
128 }
129
130 if let Some(ref bias) = self.bias {
132 for i in 0..num_nodes {
133 for j in 0..self.out_features {
134 let current = output.get(&[i, j])?;
135 output.set(&[i, j], current + bias.get(&[j])?)?;
136 }
137 }
138 }
139
140 Ok(output)
141 }
142
143 fn add_self_loops_to_adjacency(&self, adjacency: &CsrTensor) -> TorshResult<CsrTensor> {
145 let coo = adjacency.to_coo()?;
146 let mut triplets = coo.triplets();
147 let num_nodes = adjacency.shape().dims()[0];
148
149 let mut self_loop_set = std::collections::HashSet::new();
151 for (row, col, _) in &triplets {
152 if row == col {
153 self_loop_set.insert(*row);
154 }
155 }
156
157 for i in 0..num_nodes {
159 if !self_loop_set.contains(&i) {
160 triplets.push((i, i, 1.0));
161 }
162 }
163
164 let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
165 triplets.into_iter().fold(
166 (Vec::new(), Vec::new(), Vec::new()),
167 |(mut rows, mut cols, mut vals), (r, c, v)| {
168 rows.push(r);
169 cols.push(c);
170 vals.push(v);
171 (rows, cols, vals)
172 },
173 );
174
175 let new_coo = CooTensor::new(row_indices, col_indices, values, adjacency.shape().clone())?;
176 CsrTensor::from_coo(&new_coo)
177 }
178
179 fn normalize_adjacency(&self, adjacency: &CsrTensor) -> TorshResult<CsrTensor> {
181 let num_nodes = adjacency.shape().dims()[0];
182
183 let mut degrees = vec![0.0; num_nodes];
185 let coo = adjacency.to_coo()?;
186 let triplets = coo.triplets();
187
188 for (row, _col, val) in &triplets {
189 degrees[*row] += val;
190 }
191
192 let inv_sqrt_degrees: Vec<f32> = degrees
194 .iter()
195 .map(|&d| if d > 0.0 { 1.0 / d.sqrt() } else { 0.0 })
196 .collect();
197
198 let normalized_triplets: Vec<_> = triplets
200 .into_iter()
201 .map(|(row, col, val)| {
202 let normalized_val = inv_sqrt_degrees[row] * val * inv_sqrt_degrees[col];
203 (row, col, normalized_val)
204 })
205 .collect();
206
207 let (row_indices, col_indices, values): (Vec<_>, Vec<_>, Vec<_>) =
208 normalized_triplets.into_iter().fold(
209 (Vec::new(), Vec::new(), Vec::new()),
210 |(mut rows, mut cols, mut vals), (r, c, v)| {
211 rows.push(r);
212 cols.push(c);
213 vals.push(v);
214 (rows, cols, vals)
215 },
216 );
217
218 let normalized_coo =
219 CooTensor::new(row_indices, col_indices, values, adjacency.shape().clone())?;
220 CsrTensor::from_coo(&normalized_coo)
221 }
222
223 pub fn num_parameters(&self) -> usize {
225 let weight_params = self.in_features * self.out_features;
226 let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
227 weight_params + bias_params
228 }
229
230 pub fn in_features(&self) -> usize {
232 self.in_features
233 }
234
235 pub fn out_features(&self) -> usize {
237 self.out_features
238 }
239}
240
241pub struct SparseConv2d {
246 kernel: CsrTensor,
248 bias: Option<Tensor>,
250 in_channels: usize,
252 out_channels: usize,
254 kernel_size: (usize, usize),
256 stride: (usize, usize),
258 padding: (usize, usize),
260 dilation: (usize, usize),
262}
263
264impl SparseConv2d {
265 #[allow(clippy::too_many_arguments)]
267 pub fn new(
268 in_channels: usize,
269 out_channels: usize,
270 kernel_size: (usize, usize),
271 stride: Option<(usize, usize)>,
272 padding: Option<(usize, usize)>,
273 dilation: Option<(usize, usize)>,
274 sparsity: f32,
275 use_bias: bool,
276 ) -> TorshResult<Self> {
277 if !(0.0..=1.0).contains(&sparsity) {
278 return Err(TorshError::InvalidArgument(
279 "Sparsity must be between 0.0 and 1.0".to_string(),
280 ));
281 }
282
283 let stride = stride.unwrap_or((1, 1));
284 let padding = padding.unwrap_or((0, 0));
285 let dilation = dilation.unwrap_or((1, 1));
286
287 let kernel =
289 Self::generate_sparse_kernel(out_channels, in_channels, kernel_size, sparsity)?;
290
291 let bias = if use_bias {
293 Some(zeros::<f32>(&[out_channels])?)
294 } else {
295 None
296 };
297
298 Ok(Self {
299 kernel,
300 bias,
301 in_channels,
302 out_channels,
303 kernel_size,
304 stride,
305 padding,
306 dilation,
307 })
308 }
309
310 pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
312 let input_shape = input.shape();
314 if input_shape.ndim() != 4 {
315 return Err(TorshError::InvalidArgument(
316 "Input must be 4D tensor (batch_size, in_channels, height, width)".to_string(),
317 ));
318 }
319
320 let batch_size = input_shape.dims()[0];
321 let input_channels = input_shape.dims()[1];
322 let input_height = input_shape.dims()[2];
323 let input_width = input_shape.dims()[3];
324
325 if input_channels != self.in_channels {
326 return Err(TorshError::InvalidArgument(format!(
327 "Input channels {} don't match layer input channels {}",
328 input_channels, self.in_channels
329 )));
330 }
331
332 let output_height =
334 (input_height + 2 * self.padding.0 - self.dilation.0 * (self.kernel_size.0 - 1) - 1)
335 / self.stride.0
336 + 1;
337 let output_width =
338 (input_width + 2 * self.padding.1 - self.dilation.1 * (self.kernel_size.1 - 1) - 1)
339 / self.stride.1
340 + 1;
341
342 let mut output =
343 zeros::<f32>(&[batch_size, self.out_channels, output_height, output_width])?;
344
345 for b in 0..batch_size {
347 self.conv2d_single(
348 input,
349 &mut output,
350 b,
351 input_height,
352 input_width,
353 output_height,
354 output_width,
355 )?;
356 }
357
358 if let Some(ref bias) = self.bias {
360 for b in 0..batch_size {
361 for c in 0..self.out_channels {
362 let bias_val = bias.get(&[c])?;
363 for h in 0..output_height {
364 for w in 0..output_width {
365 let current = output.get(&[b, c, h, w])?;
366 output.set(&[b, c, h, w], current + bias_val)?;
367 }
368 }
369 }
370 }
371 }
372
373 Ok(output)
374 }
375
376 #[allow(clippy::too_many_arguments)]
378 fn conv2d_single(
379 &self,
380 input: &Tensor,
381 output: &mut Tensor,
382 batch_idx: usize,
383 input_height: usize,
384 input_width: usize,
385 output_height: usize,
386 output_width: usize,
387 ) -> TorshResult<()> {
388 let kernel_coo = self.kernel.to_coo()?;
390 let kernel_triplets = kernel_coo.triplets();
391
392 for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
394 let (out_c, in_c, kh, kw) = self.flat_to_4d_kernel(kernel_flat_idx);
396
397 for out_h in 0..output_height {
399 for out_w in 0..output_width {
400 let in_h = out_h * self.stride.0 + kh * self.dilation.0;
402 let in_w = out_w * self.stride.1 + kw * self.dilation.1;
403
404 if in_h >= self.padding.0 && in_w >= self.padding.1 {
406 let padded_in_h = in_h - self.padding.0;
407 let padded_in_w = in_w - self.padding.1;
408
409 if padded_in_h < input_height && padded_in_w < input_width {
411 let input_val =
412 input.get(&[batch_idx, in_c, padded_in_h, padded_in_w])?;
413 let current_out = output.get(&[batch_idx, out_c, out_h, out_w])?;
414 output.set(
415 &[batch_idx, out_c, out_h, out_w],
416 current_out + kernel_val * input_val,
417 )?;
418 }
419 }
420 }
421 }
422 }
423
424 Ok(())
425 }
426
427 fn flat_to_4d_kernel(&self, flat_idx: usize) -> (usize, usize, usize, usize) {
429 let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
430 let channel_size = self.in_channels * kernel_size_total;
431
432 let out_c = flat_idx / channel_size;
433 let remaining = flat_idx % channel_size;
434 let in_c = remaining / kernel_size_total;
435 let remaining = remaining % kernel_size_total;
436 let kh = remaining / self.kernel_size.1;
437 let kw = remaining % self.kernel_size.1;
438
439 (out_c, in_c, kh, kw)
440 }
441
442 #[allow(dead_code)]
444 fn kernel_4d_to_flat(&self, out_c: usize, in_c: usize, kh: usize, kw: usize) -> usize {
445 let kernel_size_total = self.kernel_size.0 * self.kernel_size.1;
446 let channel_size = self.in_channels * kernel_size_total;
447
448 out_c * channel_size + in_c * kernel_size_total + kh * self.kernel_size.1 + kw
449 }
450
451 fn generate_sparse_kernel(
453 out_channels: usize,
454 in_channels: usize,
455 kernel_size: (usize, usize),
456 sparsity: f32,
457 ) -> TorshResult<CsrTensor> {
458 let total_elements = out_channels * in_channels * kernel_size.0 * kernel_size.1;
459 let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
460
461 let mut row_indices = Vec::with_capacity(nnz);
462 let mut col_indices = Vec::with_capacity(nnz);
463 let mut values = Vec::with_capacity(nnz);
464
465 let mut positions = std::collections::HashSet::new();
467 while positions.len() < nnz {
468 let mut rng = scirs2_core::random::thread_rng();
469 let flat_idx = rng.gen_range(0..total_elements);
470 positions.insert(flat_idx);
471 }
472
473 let kernel_size_total = kernel_size.0 * kernel_size.1;
477 let channel_size = in_channels * kernel_size_total;
478
479 for flat_idx in positions {
480 let out_c = flat_idx / channel_size;
481 let col_idx = flat_idx % channel_size; row_indices.push(out_c);
484 col_indices.push(col_idx);
485
486 let fan_in = in_channels * kernel_size.0 * kernel_size.1;
488 let std_dev = (2.0 / fan_in as f32).sqrt();
489 let mut rng = scirs2_core::random::thread_rng();
490 values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
491 }
492
493 let shape = Shape::new(vec![out_channels, channel_size]);
494 let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
495 CsrTensor::from_coo(&coo)
496 }
497
498 pub fn num_parameters(&self) -> usize {
500 let kernel_params = self.kernel.nnz();
501 let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
502 kernel_params + bias_params
503 }
504
505 pub fn kernel_sparsity(&self) -> f32 {
507 let total_elements =
508 self.out_channels * self.in_channels * self.kernel_size.0 * self.kernel_size.1;
509 let nnz = self.kernel.nnz();
510 1.0 - (nnz as f32 / total_elements as f32)
511 }
512}
513
514pub struct SparseConv1d {
518 kernel: CsrTensor,
520 bias: Option<Tensor>,
522 in_channels: usize,
524 out_channels: usize,
526 kernel_size: usize,
528 stride: usize,
530 padding: usize,
532 dilation: usize,
534}
535
536impl SparseConv1d {
537 #[allow(clippy::too_many_arguments)]
539 pub fn new(
540 in_channels: usize,
541 out_channels: usize,
542 kernel_size: usize,
543 stride: Option<usize>,
544 padding: Option<usize>,
545 dilation: Option<usize>,
546 sparsity: f32,
547 use_bias: bool,
548 ) -> TorshResult<Self> {
549 if !(0.0..=1.0).contains(&sparsity) {
550 return Err(TorshError::InvalidArgument(
551 "Sparsity must be between 0.0 and 1.0".to_string(),
552 ));
553 }
554
555 let stride = stride.unwrap_or(1);
556 let padding = padding.unwrap_or(0);
557 let dilation = dilation.unwrap_or(1);
558
559 let kernel =
561 Self::generate_sparse_kernel_1d(out_channels, in_channels, kernel_size, sparsity)?;
562
563 let bias = if use_bias {
565 Some(zeros::<f32>(&[out_channels])?)
566 } else {
567 None
568 };
569
570 Ok(Self {
571 kernel,
572 bias,
573 in_channels,
574 out_channels,
575 kernel_size,
576 stride,
577 padding,
578 dilation,
579 })
580 }
581
582 pub fn forward(&self, input: &Tensor) -> TorshResult<Tensor> {
584 let input_shape = input.shape();
586 if input_shape.ndim() != 3 {
587 return Err(TorshError::InvalidArgument(
588 "Input must be 3D tensor (batch_size, in_channels, length)".to_string(),
589 ));
590 }
591
592 let batch_size = input_shape.dims()[0];
593 let input_channels = input_shape.dims()[1];
594 let input_length = input_shape.dims()[2];
595
596 if input_channels != self.in_channels {
597 return Err(TorshError::InvalidArgument(format!(
598 "Input channels {} don't match layer input channels {}",
599 input_channels, self.in_channels
600 )));
601 }
602
603 let output_length =
605 (input_length + 2 * self.padding - self.dilation * (self.kernel_size - 1) - 1)
606 / self.stride
607 + 1;
608
609 let mut output = zeros::<f32>(&[batch_size, self.out_channels, output_length])?;
610
611 for b in 0..batch_size {
613 self.conv1d_single(input, &mut output, b, input_length, output_length)?;
614 }
615
616 if let Some(ref bias) = self.bias {
618 for b in 0..batch_size {
619 for c in 0..self.out_channels {
620 let bias_val = bias.get(&[c])?;
621 for l in 0..output_length {
622 let current = output.get(&[b, c, l])?;
623 output.set(&[b, c, l], current + bias_val)?;
624 }
625 }
626 }
627 }
628
629 Ok(output)
630 }
631
632 fn conv1d_single(
634 &self,
635 input: &Tensor,
636 output: &mut Tensor,
637 batch_idx: usize,
638 input_length: usize,
639 output_length: usize,
640 ) -> TorshResult<()> {
641 let kernel_coo = self.kernel.to_coo()?;
643 let kernel_triplets = kernel_coo.triplets();
644
645 for (kernel_flat_idx, _, kernel_val) in kernel_triplets {
647 let (out_c, in_c, k_pos) = self.flat_to_3d_kernel(kernel_flat_idx);
649
650 for out_pos in 0..output_length {
652 let in_pos = out_pos * self.stride + k_pos * self.dilation;
654
655 if in_pos >= self.padding {
657 let padded_in_pos = in_pos - self.padding;
658
659 if padded_in_pos < input_length {
661 let input_val = input.get(&[batch_idx, in_c, padded_in_pos])?;
662 let current_out = output.get(&[batch_idx, out_c, out_pos])?;
663 output.set(
664 &[batch_idx, out_c, out_pos],
665 current_out + kernel_val * input_val,
666 )?;
667 }
668 }
669 }
670 }
671
672 Ok(())
673 }
674
675 fn flat_to_3d_kernel(&self, flat_idx: usize) -> (usize, usize, usize) {
677 let channel_size = self.in_channels * self.kernel_size;
678
679 let out_c = flat_idx / channel_size;
680 let remaining = flat_idx % channel_size;
681 let in_c = remaining / self.kernel_size;
682 let k_pos = remaining % self.kernel_size;
683
684 (out_c, in_c, k_pos)
685 }
686
687 fn generate_sparse_kernel_1d(
689 out_channels: usize,
690 in_channels: usize,
691 kernel_size: usize,
692 sparsity: f32,
693 ) -> TorshResult<CsrTensor> {
694 let total_elements = out_channels * in_channels * kernel_size;
695 let nnz = ((total_elements as f32) * (1.0 - sparsity)) as usize;
696
697 let mut row_indices = Vec::with_capacity(nnz);
698 let mut col_indices = Vec::with_capacity(nnz);
699 let mut values = Vec::with_capacity(nnz);
700
701 let mut positions = std::collections::HashSet::new();
703 while positions.len() < nnz {
704 let mut rng = scirs2_core::random::thread_rng();
705 let flat_idx = rng.gen_range(0..total_elements);
706 positions.insert(flat_idx);
707 }
708
709 let channel_size = in_channels * kernel_size;
713
714 for flat_idx in positions {
715 let out_c = flat_idx / channel_size;
716 let col_idx = flat_idx % channel_size; row_indices.push(out_c);
719 col_indices.push(col_idx);
720
721 let fan_in = in_channels * kernel_size;
723 let std_dev = (2.0 / fan_in as f32).sqrt();
724 let mut rng = scirs2_core::random::thread_rng();
725 values.push((rng.random::<f32>() * 2.0 - 1.0) * std_dev);
726 }
727
728 let shape = Shape::new(vec![out_channels, channel_size]);
729 let coo = CooTensor::new(row_indices, col_indices, values, shape)?;
730 CsrTensor::from_coo(&coo)
731 }
732
733 pub fn num_parameters(&self) -> usize {
735 let kernel_params = self.kernel.nnz();
736 let bias_params = self.bias.as_ref().map_or(0, |b| b.shape().numel());
737 kernel_params + bias_params
738 }
739
740 pub fn kernel_sparsity(&self) -> f32 {
742 let total_elements = self.out_channels * self.in_channels * self.kernel_size;
743 let nnz = self.kernel.nnz();
744 1.0 - (nnz as f32 / total_elements as f32)
745 }
746}