1use std::mem;
21
22use oxicuda_blas::GpuFloat;
23
24use crate::error::{SparseError, SparseResult};
25use crate::format::CsrMatrix;
26
27pub const HYB_ELL_SENTINEL: i32 = -1;
29
30#[derive(Debug, Clone, Copy, PartialEq)]
32pub enum HybPartition {
33 Auto,
35 Max,
37 Threshold(f64),
41 Fixed(usize),
43}
44
45#[derive(Debug, Clone)]
54pub struct HybMatrix<T: GpuFloat> {
55 rows: usize,
57 cols: usize,
59 ell_width: usize,
61 ell_col_indices: Vec<i32>,
64 ell_values: Vec<T>,
67 coo_nnz: usize,
69 coo_row_indices: Vec<i32>,
71 coo_col_indices: Vec<i32>,
73 coo_values: Vec<T>,
75}
76
77#[derive(Debug, Clone, Copy)]
79pub struct HybStatistics {
80 pub ell_fraction: f64,
82 pub coo_fraction: f64,
84 pub ell_padding_ratio: f64,
87 pub memory_bytes: usize,
89 pub csr_memory_bytes: usize,
92}
93
94pub fn optimal_ell_width<T: GpuFloat>(row_nnz: &[usize]) -> usize {
106 if row_nnz.is_empty() {
107 return 1;
108 }
109
110 let rows = row_nnz.len();
111 let max_nnz = row_nnz.iter().copied().max().unwrap_or(1);
112 if max_nnz == 0 {
113 return 1;
114 }
115
116 let ell_entry_bytes = mem::size_of::<i32>() + mem::size_of::<T>();
117 let coo_entry_bytes = 2 * mem::size_of::<i32>() + mem::size_of::<T>();
118
119 let mut best_width = 1;
120 let mut best_cost = usize::MAX;
121
122 for w in 1..=max_nnz {
124 let ell_cost = rows * w * ell_entry_bytes;
125 let overflow: usize = row_nnz.iter().map(|&r| r.saturating_sub(w)).sum();
126 let coo_cost = overflow * coo_entry_bytes;
127 let total = ell_cost + coo_cost;
128 if total < best_cost {
129 best_cost = total;
130 best_width = w;
131 }
132 }
133
134 best_width
135}
136
137fn compute_ell_width(row_nnz: &[usize], partition: HybPartition) -> usize {
140 if row_nnz.is_empty() {
141 return 1;
142 }
143 match partition {
144 HybPartition::Auto => {
145 let mut sorted = row_nnz.to_vec();
147 sorted.sort_unstable();
148 let mid = sorted.len() / 2;
149 let median = if sorted.len() % 2 == 0 {
150 (sorted[mid.saturating_sub(1)] + sorted[mid]) / 2
151 } else {
152 sorted[mid]
153 };
154 median.max(1)
155 }
156 HybPartition::Max => row_nnz.iter().copied().max().unwrap_or(1).max(1),
157 HybPartition::Threshold(pct) => {
158 let pct = pct.clamp(0.0, 1.0);
159 let mut sorted = row_nnz.to_vec();
160 sorted.sort_unstable();
161 let idx = ((sorted.len() as f64 * pct).ceil() as usize)
162 .min(sorted.len())
163 .saturating_sub(1);
164 sorted[idx].max(1)
165 }
166 HybPartition::Fixed(w) => w.max(1),
167 }
168}
169
170impl<T: GpuFloat> HybMatrix<T> {
171 #[allow(clippy::too_many_arguments)]
189 pub fn new(
190 rows: usize,
191 cols: usize,
192 ell_width: usize,
193 ell_col_indices: Vec<i32>,
194 ell_values: Vec<T>,
195 coo_row_indices: Vec<i32>,
196 coo_col_indices: Vec<i32>,
197 coo_values: Vec<T>,
198 ) -> SparseResult<Self> {
199 if rows == 0 || cols == 0 {
200 return Err(SparseError::InvalidFormat(
201 "rows and cols must be non-zero".to_string(),
202 ));
203 }
204 if ell_width == 0 {
205 return Err(SparseError::InvalidFormat(
206 "ell_width must be non-zero".to_string(),
207 ));
208 }
209 let ell_total = rows * ell_width;
210 if ell_col_indices.len() != ell_total {
211 return Err(SparseError::InvalidFormat(format!(
212 "ell_col_indices length ({}) must be rows * ell_width ({})",
213 ell_col_indices.len(),
214 ell_total
215 )));
216 }
217 if ell_values.len() != ell_total {
218 return Err(SparseError::InvalidFormat(format!(
219 "ell_values length ({}) must be rows * ell_width ({})",
220 ell_values.len(),
221 ell_total
222 )));
223 }
224 let coo_nnz = coo_values.len();
225 if coo_row_indices.len() != coo_nnz || coo_col_indices.len() != coo_nnz {
226 return Err(SparseError::InvalidFormat(format!(
227 "COO arrays must have equal length: row_indices={}, col_indices={}, values={}",
228 coo_row_indices.len(),
229 coo_col_indices.len(),
230 coo_nnz
231 )));
232 }
233
234 Ok(Self {
235 rows,
236 cols,
237 ell_width,
238 ell_col_indices,
239 ell_values,
240 coo_nnz,
241 coo_row_indices,
242 coo_col_indices,
243 coo_values,
244 })
245 }
246
247 pub fn from_csr(csr: &CsrMatrix<T>, partition: HybPartition) -> SparseResult<Self> {
258 let (h_row_ptr, h_col_idx, h_values) = csr.to_host()?;
259 let rows = csr.rows() as usize;
260 let cols = csr.cols() as usize;
261
262 let mut row_nnz = Vec::with_capacity(rows);
264 for i in 0..rows {
265 row_nnz.push((h_row_ptr[i + 1] - h_row_ptr[i]) as usize);
266 }
267
268 let ell_width = compute_ell_width(&row_nnz, partition);
269 Self::build_from_csr_host(rows, cols, ell_width, &h_row_ptr, &h_col_idx, &h_values)
270 }
271
272 pub fn from_coo(coo: &super::CooMatrix<T>, partition: HybPartition) -> SparseResult<Self> {
281 let csr = coo.to_csr()?;
282 Self::from_csr(&csr, partition)
283 }
284
285 pub fn to_csr(&self) -> SparseResult<CsrMatrix<T>> {
294 let total_nnz = self.total_nnz();
295 if total_nnz == 0 {
296 return Err(SparseError::ZeroNnz);
297 }
298
299 let mut row_counts = vec![0usize; self.rows];
301
302 for (i, count) in row_counts.iter_mut().enumerate() {
304 for k in 0..self.ell_width {
305 let idx = k * self.rows + i;
306 if self.ell_col_indices[idx] != HYB_ELL_SENTINEL {
307 *count += 1;
308 }
309 }
310 }
311
312 for &r in &self.coo_row_indices {
314 let row = r as usize;
315 if row < self.rows {
316 row_counts[row] += 1;
317 }
318 }
319
320 let mut h_row_ptr = vec![0i32; self.rows + 1];
322 for i in 0..self.rows {
323 h_row_ptr[i + 1] = h_row_ptr[i] + row_counts[i] as i32;
324 }
325
326 let mut h_col_idx = vec![0i32; total_nnz];
328 let mut h_values = vec![T::gpu_zero(); total_nnz];
329 let mut write_pos: Vec<i32> = h_row_ptr.clone();
330
331 for (i, pos) in write_pos.iter_mut().enumerate().take(self.rows) {
333 for k in 0..self.ell_width {
334 let idx = k * self.rows + i;
335 let col = self.ell_col_indices[idx];
336 if col != HYB_ELL_SENTINEL {
337 let dest = *pos as usize;
338 h_col_idx[dest] = col;
339 h_values[dest] = self.ell_values[idx];
340 *pos += 1;
341 }
342 }
343 }
344
345 for j in 0..self.coo_nnz {
347 let row = self.coo_row_indices[j] as usize;
348 if row < self.rows {
349 let dest = write_pos[row] as usize;
350 h_col_idx[dest] = self.coo_col_indices[j];
351 h_values[dest] = self.coo_values[j];
352 write_pos[row] += 1;
353 }
354 }
355
356 CsrMatrix::from_host(
357 self.rows as u32,
358 self.cols as u32,
359 &h_row_ptr,
360 &h_col_idx,
361 &h_values,
362 )
363 }
364
365 pub fn total_nnz(&self) -> usize {
367 self.ell_nnz() + self.coo_nnz
368 }
369
370 #[inline]
372 pub fn nnz(&self) -> usize {
373 self.total_nnz()
374 }
375
376 #[inline]
378 pub fn is_empty(&self) -> bool {
379 self.total_nnz() == 0
380 }
381
382 pub fn ell_nnz(&self) -> usize {
384 self.ell_col_indices
385 .iter()
386 .filter(|&&c| c != HYB_ELL_SENTINEL)
387 .count()
388 }
389
390 #[inline]
392 pub fn rows(&self) -> usize {
393 self.rows
394 }
395
396 #[inline]
398 pub fn cols(&self) -> usize {
399 self.cols
400 }
401
402 #[inline]
404 pub fn ell_width(&self) -> usize {
405 self.ell_width
406 }
407
408 #[inline]
410 pub fn ell_col_indices(&self) -> &[i32] {
411 &self.ell_col_indices
412 }
413
414 #[inline]
416 pub fn ell_values(&self) -> &[T] {
417 &self.ell_values
418 }
419
420 #[inline]
422 pub fn coo_nnz(&self) -> usize {
423 self.coo_nnz
424 }
425
426 #[inline]
428 pub fn coo_row_indices(&self) -> &[i32] {
429 &self.coo_row_indices
430 }
431
432 #[inline]
434 pub fn coo_col_indices(&self) -> &[i32] {
435 &self.coo_col_indices
436 }
437
438 #[inline]
440 pub fn coo_values(&self) -> &[T] {
441 &self.coo_values
442 }
443
444 pub fn statistics(&self) -> HybStatistics {
450 let ell_nnz = self.ell_nnz();
451 let total_nnz = ell_nnz + self.coo_nnz;
452
453 let (ell_fraction, coo_fraction) = if total_nnz == 0 {
454 (0.0, 0.0)
455 } else {
456 (
457 ell_nnz as f64 / total_nnz as f64,
458 self.coo_nnz as f64 / total_nnz as f64,
459 )
460 };
461
462 let ell_total_slots = self.rows * self.ell_width;
463 let ell_padding_ratio = if ell_total_slots == 0 {
464 0.0
465 } else {
466 (ell_total_slots - ell_nnz) as f64 / ell_total_slots as f64
467 };
468
469 let ell_mem = ell_total_slots * (mem::size_of::<i32>() + mem::size_of::<T>());
471 let coo_mem = self.coo_nnz * (2 * mem::size_of::<i32>() + mem::size_of::<T>());
472 let memory_bytes = ell_mem + coo_mem;
473
474 let csr_memory_bytes = (self.rows + 1) * mem::size_of::<i32>()
476 + total_nnz * mem::size_of::<i32>()
477 + total_nnz * mem::size_of::<T>();
478
479 HybStatistics {
480 ell_fraction,
481 coo_fraction,
482 ell_padding_ratio,
483 memory_bytes,
484 csr_memory_bytes,
485 }
486 }
487
488 fn build_from_csr_host(
490 rows: usize,
491 cols: usize,
492 ell_width: usize,
493 h_row_ptr: &[i32],
494 h_col_idx: &[i32],
495 h_values: &[T],
496 ) -> SparseResult<Self> {
497 let ell_total = rows * ell_width;
498 let mut ell_col_indices = vec![HYB_ELL_SENTINEL; ell_total];
499 let mut ell_values = vec![T::gpu_zero(); ell_total];
500
501 let mut coo_row_indices = Vec::new();
502 let mut coo_col_indices = Vec::new();
503 let mut coo_values = Vec::new();
504
505 for i in 0..rows {
506 let start = h_row_ptr[i] as usize;
507 let end = h_row_ptr[i + 1] as usize;
508 let row_entries = end - start;
509
510 let ell_count = row_entries.min(ell_width);
512 for k in 0..ell_count {
513 let ell_idx = k * rows + i; ell_col_indices[ell_idx] = h_col_idx[start + k];
515 ell_values[ell_idx] = h_values[start + k];
516 }
517
518 if row_entries > ell_width {
520 for j in (start + ell_width)..end {
521 coo_row_indices.push(i as i32);
522 coo_col_indices.push(h_col_idx[j]);
523 coo_values.push(h_values[j]);
524 }
525 }
526 }
527
528 let coo_nnz = coo_values.len();
529
530 Ok(Self {
531 rows,
532 cols,
533 ell_width,
534 ell_col_indices,
535 ell_values,
536 coo_nnz,
537 coo_row_indices,
538 coo_col_indices,
539 coo_values,
540 })
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 fn hyb_from_csr_host(
550 rows: usize,
551 cols: usize,
552 row_ptr: &[i32],
553 col_idx: &[i32],
554 values: &[f64],
555 partition: HybPartition,
556 ) -> SparseResult<HybMatrix<f64>> {
557 let mut row_nnz = Vec::with_capacity(rows);
558 for i in 0..rows {
559 row_nnz.push((row_ptr[i + 1] - row_ptr[i]) as usize);
560 }
561 let ell_width = compute_ell_width(&row_nnz, partition);
562 HybMatrix::build_from_csr_host(rows, cols, ell_width, row_ptr, col_idx, values)
563 }
564
565 fn test_csr_data() -> (usize, usize, Vec<i32>, Vec<i32>, Vec<f64>) {
571 let rows = 4;
572 let cols = 4;
573 let row_ptr = vec![0, 1, 2, 4, 7];
574 let col_idx = vec![0, 1, 0, 2, 0, 1, 3];
575 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0];
576 (rows, cols, row_ptr, col_idx, values)
577 }
578
579 #[test]
580 fn hyb_from_csr_auto_partition() {
581 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
582 let hyb = hyb_from_csr_host(rows, cols, &row_ptr, &col_idx, &values, HybPartition::Auto);
585 assert!(hyb.is_ok());
586 let hyb = hyb.expect("test helper");
587 assert_eq!(hyb.rows(), 4);
588 assert_eq!(hyb.cols(), 4);
589 assert_eq!(hyb.ell_width(), 1);
590 assert_eq!(hyb.ell_nnz(), 4);
592 assert_eq!(hyb.coo_nnz(), 3);
594 assert_eq!(hyb.total_nnz(), 7);
595 }
596
597 #[test]
598 fn hyb_from_csr_max_partition() {
599 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
600 let hyb = hyb_from_csr_host(rows, cols, &row_ptr, &col_idx, &values, HybPartition::Max);
602 let hyb = hyb.expect("test helper");
603 assert_eq!(hyb.ell_width(), 3);
604 assert_eq!(hyb.coo_nnz(), 0);
605 assert_eq!(hyb.total_nnz(), 7);
606 }
607
608 #[test]
609 fn hyb_from_csr_fixed_partition() {
610 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
611 let hyb = hyb_from_csr_host(
612 rows,
613 cols,
614 &row_ptr,
615 &col_idx,
616 &values,
617 HybPartition::Fixed(2),
618 );
619 let hyb = hyb.expect("test helper");
620 assert_eq!(hyb.ell_width(), 2);
621 assert_eq!(hyb.ell_nnz(), 6);
623 assert_eq!(hyb.coo_nnz(), 1);
625 assert_eq!(hyb.total_nnz(), 7);
626 }
627
628 #[test]
629 fn hyb_from_csr_threshold_partition() {
630 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
631 let hyb = hyb_from_csr_host(
633 rows,
634 cols,
635 &row_ptr,
636 &col_idx,
637 &values,
638 HybPartition::Threshold(0.9),
639 );
640 let hyb = hyb.expect("test helper");
641 assert_eq!(hyb.ell_width(), 3);
642 assert_eq!(hyb.coo_nnz(), 0);
643 }
644
645 #[test]
646 fn hyb_to_csr_roundtrip() {
647 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
648 let hyb = hyb_from_csr_host(
649 rows,
650 cols,
651 &row_ptr,
652 &col_idx,
653 &values,
654 HybPartition::Fixed(2),
655 );
656 let hyb = hyb.expect("test helper");
657
658 assert_eq!(hyb.total_nnz(), 7);
661
662 assert_eq!(hyb.ell_col_indices()[0], 0);
665 assert_eq!(hyb.ell_col_indices()[1], 1);
667 assert_eq!(hyb.ell_col_indices()[2], 0);
669 assert_eq!(hyb.ell_col_indices()[3], 0);
671
672 assert_eq!(hyb.ell_col_indices()[6], 2);
674 assert_eq!(hyb.ell_col_indices()[7], 1);
676
677 assert_eq!(hyb.coo_row_indices(), &[3]);
679 assert_eq!(hyb.coo_col_indices(), &[3]);
680 assert!((hyb.coo_values()[0] - 7.0).abs() < 1e-12);
681 }
682
683 #[test]
684 fn hyb_is_empty_all_padding() {
685 let ell_col = vec![HYB_ELL_SENTINEL; 4];
687 let ell_val = vec![0.0f64; 4];
688 let hyb = HybMatrix::new(2, 2, 2, ell_col, ell_val, vec![], vec![], vec![]);
689 let hyb = hyb.expect("test helper");
690 assert!(hyb.is_empty());
691 assert_eq!(hyb.nnz(), 0);
692 }
693
694 #[test]
695 fn hyb_statistics_pure_ell() {
696 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
697 let hyb = hyb_from_csr_host(rows, cols, &row_ptr, &col_idx, &values, HybPartition::Max);
698 let hyb = hyb.expect("test helper");
699 let stats = hyb.statistics();
700
701 assert!((stats.ell_fraction - 1.0).abs() < 1e-12);
702 assert!((stats.coo_fraction - 0.0).abs() < 1e-12);
703 assert!((stats.ell_padding_ratio - 5.0 / 12.0).abs() < 1e-12);
705 assert!(stats.memory_bytes > 0);
706 assert!(stats.csr_memory_bytes > 0);
707 }
708
709 #[test]
710 fn hyb_statistics_mixed() {
711 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
712 let hyb = hyb_from_csr_host(
713 rows,
714 cols,
715 &row_ptr,
716 &col_idx,
717 &values,
718 HybPartition::Fixed(1),
719 );
720 let hyb = hyb.expect("test helper");
721 let stats = hyb.statistics();
722
723 assert!((stats.ell_fraction - 4.0 / 7.0).abs() < 1e-12);
725 assert!((stats.coo_fraction - 3.0 / 7.0).abs() < 1e-12);
726 assert!((stats.ell_padding_ratio - 0.0).abs() < 1e-12);
728 }
729
730 #[test]
731 fn hyb_new_validation_bad_ell_lengths() {
732 let result = HybMatrix::<f64>::new(
733 2,
734 2,
735 2,
736 vec![0; 3], vec![1.0; 4],
738 vec![],
739 vec![],
740 vec![],
741 );
742 assert!(result.is_err());
743 }
744
745 #[test]
746 fn hyb_new_validation_bad_coo_lengths() {
747 let result = HybMatrix::<f64>::new(
748 2,
749 2,
750 1,
751 vec![HYB_ELL_SENTINEL; 2],
752 vec![0.0; 2],
753 vec![0], vec![0, 1], vec![1.0], );
757 assert!(result.is_err());
758 }
759
760 #[test]
761 fn hyb_new_validation_zero_rows() {
762 let result = HybMatrix::<f64>::new(0, 2, 1, vec![], vec![], vec![], vec![], vec![]);
763 assert!(result.is_err());
764 }
765
766 #[test]
767 fn hyb_new_validation_zero_ell_width() {
768 let result = HybMatrix::<f64>::new(2, 2, 0, vec![], vec![], vec![], vec![], vec![]);
769 assert!(result.is_err());
770 }
771
772 #[test]
773 fn hyb_ell_values_column_major_layout() {
774 let row_ptr = vec![0, 2, 3, 6];
779 let col_idx = vec![0, 1, 2, 0, 1, 2];
780 let values = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
781
782 let hyb = hyb_from_csr_host(3, 3, &row_ptr, &col_idx, &values, HybPartition::Fixed(2));
783 let hyb = hyb.expect("test helper");
784
785 assert_eq!(hyb.ell_col_indices()[0], 0);
788 assert!((hyb.ell_values()[0] - 1.0).abs() < 1e-12);
789
790 assert_eq!(hyb.ell_col_indices()[1], 2);
792 assert!((hyb.ell_values()[1] - 3.0).abs() < 1e-12);
793
794 assert_eq!(hyb.ell_col_indices()[2], 0);
796
797 assert_eq!(hyb.ell_col_indices()[3], 1);
799 assert!((hyb.ell_values()[3] - 2.0).abs() < 1e-12);
800
801 assert_eq!(hyb.ell_col_indices()[4], HYB_ELL_SENTINEL);
803
804 assert_eq!(hyb.ell_col_indices()[5], 1);
806
807 assert_eq!(hyb.coo_nnz(), 1);
809 assert_eq!(hyb.coo_row_indices(), &[2]);
810 assert_eq!(hyb.coo_col_indices(), &[2]);
811 assert!((hyb.coo_values()[0] - 6.0).abs() < 1e-12);
812 }
813
814 #[test]
815 fn optimal_ell_width_basic() {
816 let row_nnz = vec![2, 2, 2, 2];
818 let w = optimal_ell_width::<f64>(&row_nnz);
819 assert_eq!(w, 2);
820 }
821
822 #[test]
823 fn optimal_ell_width_skewed() {
824 let mut row_nnz = vec![1; 99];
826 row_nnz.push(100);
827 let w = optimal_ell_width::<f64>(&row_nnz);
828 assert!(w < 10, "expected small ELL width, got {w}");
830 }
831
832 #[test]
833 fn optimal_ell_width_empty() {
834 let w = optimal_ell_width::<f64>(&[]);
835 assert_eq!(w, 1);
836 }
837
838 #[test]
839 fn optimal_ell_width_all_zero() {
840 let w = optimal_ell_width::<f64>(&[0, 0, 0]);
841 assert_eq!(w, 1);
842 }
843
844 #[test]
845 fn hyb_partition_threshold_boundary() {
846 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
848 let hyb = hyb_from_csr_host(
849 rows,
850 cols,
851 &row_ptr,
852 &col_idx,
853 &values,
854 HybPartition::Threshold(1.0),
855 );
856 let hyb = hyb.expect("test helper");
857 assert_eq!(hyb.ell_width(), 3);
858 assert_eq!(hyb.coo_nnz(), 0);
859 }
860
861 #[test]
862 fn hyb_partition_threshold_zero() {
863 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
865 let hyb = hyb_from_csr_host(
866 rows,
867 cols,
868 &row_ptr,
869 &col_idx,
870 &values,
871 HybPartition::Threshold(0.0),
872 );
873 let hyb = hyb.expect("test helper");
874 assert_eq!(hyb.ell_width(), 1);
875 }
876
877 #[test]
878 fn hyb_statistics_memory_comparison() {
879 let (rows, cols, row_ptr, col_idx, values) = test_csr_data();
881 let hyb = hyb_from_csr_host(
882 rows,
883 cols,
884 &row_ptr,
885 &col_idx,
886 &values,
887 HybPartition::Fixed(2),
888 );
889 let hyb = hyb.expect("test helper");
890 let stats = hyb.statistics();
891
892 let expected_ell = 4 * 2 * (std::mem::size_of::<i32>() + std::mem::size_of::<f64>());
896 let expected_coo = 2 * std::mem::size_of::<i32>() + std::mem::size_of::<f64>();
897 assert_eq!(stats.memory_bytes, expected_ell + expected_coo);
898
899 let expected_csr = 5 * std::mem::size_of::<i32>()
901 + 7 * std::mem::size_of::<i32>()
902 + 7 * std::mem::size_of::<f64>();
903 assert_eq!(stats.csr_memory_bytes, expected_csr);
904 }
905
906 #[test]
909 fn hyb_identity_4x4_no_coo_overflow() {
910 let rows = 4usize;
914 let cols = 4usize;
915 let row_ptr = vec![0i32, 1, 2, 3, 4];
916 let col_idx = vec![0i32, 1, 2, 3];
917 let values = vec![1.0f64; 4];
918 let hyb = hyb_from_csr_host(rows, cols, &row_ptr, &col_idx, &values, HybPartition::Auto)
919 .expect("identity 4x4 hyb construction");
920 assert_eq!(hyb.ell_width(), 1, "ell_width should be 1 for identity");
921 assert_eq!(
922 hyb.coo_nnz(),
923 0,
924 "no COO overflow for uniform-density matrix"
925 );
926 assert_eq!(hyb.total_nnz(), 4);
927 }
928
929 #[test]
930 fn hyb_irregular_matrix_has_coo_entries() {
931 let rows = 4usize;
940 let cols = 5usize;
941 let row_ptr = vec![0i32, 5, 6, 7, 8];
942 let col_idx = vec![0i32, 1, 2, 3, 4, 0, 1, 2];
943 let values = vec![1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
944 let hyb = hyb_from_csr_host(rows, cols, &row_ptr, &col_idx, &values, HybPartition::Auto)
945 .expect("irregular hyb construction");
946 assert_eq!(hyb.ell_width(), 1);
947 assert_eq!(
948 hyb.coo_nnz(),
949 4,
950 "row 0 overflows 4 entries into COO (5 nnz with ell_width=1)"
951 );
952 }
953
954 #[test]
955 fn hyb_spmv_matches_csr() {
956 let rows = 4usize;
960 let cols = 4usize;
961 let row_ptr = vec![0i32, 2, 4, 6, 8];
963 let col_idx = vec![0i32, 1, 1, 2, 2, 3, 3, 0];
964 let values = vec![2.0f64, 1.0, 2.0, 1.0, 2.0, 1.0, 2.0, 1.0];
965 let x = [1.0f64, 2.0, 3.0, 4.0];
966
967 let mut y_csr = vec![0.0f64; rows];
969 for i in 0..rows {
970 let start = row_ptr[i] as usize;
971 let end = row_ptr[i + 1] as usize;
972 for j in start..end {
973 y_csr[i] += values[j] * x[col_idx[j] as usize];
974 }
975 }
976
977 let hyb = hyb_from_csr_host(
979 rows,
980 cols,
981 &row_ptr,
982 &col_idx,
983 &values,
984 HybPartition::Fixed(1),
985 )
986 .expect("banded hyb construction");
987 assert_eq!(hyb.coo_nnz(), 4, "each row overflows 1 entry to COO");
988
989 let mut y_hyb = vec![0.0f64; rows];
992 for k in 0..hyb.ell_width() {
993 for (i, y_val) in y_hyb.iter_mut().enumerate() {
994 let idx = k * rows + i;
995 let c = hyb.ell_col_indices()[idx];
996 if c >= 0 {
997 *y_val += hyb.ell_values()[idx] * x[c as usize];
998 }
999 }
1000 }
1001 for idx in 0..hyb.coo_nnz() {
1003 let r = hyb.coo_row_indices()[idx] as usize;
1004 let c = hyb.coo_col_indices()[idx] as usize;
1005 y_hyb[r] += hyb.coo_values()[idx] * x[c];
1006 }
1007
1008 for i in 0..rows {
1009 assert!(
1010 (y_hyb[i] - y_csr[i]).abs() < 1e-10,
1011 "HYB SpMV mismatch at row {}: hyb={}, csr={}",
1012 i,
1013 y_hyb[i],
1014 y_csr[i]
1015 );
1016 }
1017 }
1018
1019 #[test]
1020 fn hyb_ell_width_is_avg_nnz() {
1021 let rows = 4usize;
1024 let cols = 4usize;
1025 let row_ptr = vec![0i32, 2, 4, 6, 8];
1026 let col_idx = vec![0i32, 1, 1, 2, 2, 3, 3, 0];
1027 let values = vec![1.0f64; 8];
1028 let hyb = hyb_from_csr_host(
1029 rows,
1030 cols,
1031 &row_ptr,
1032 &col_idx,
1033 &values,
1034 HybPartition::Fixed(2),
1035 )
1036 .expect("uniform 2-per-row hyb construction");
1037 assert_eq!(
1038 hyb.ell_width(),
1039 2,
1040 "ell_width should be 2 (= avg nnz per row)"
1041 );
1042 assert_eq!(
1043 hyb.coo_nnz(),
1044 0,
1045 "no overflow when ell_width == max nnz per row"
1046 );
1047 }
1048
1049 #[test]
1050 fn hyb_coo_stores_overflow() {
1051 let rows = 4usize;
1055 let cols = 4usize;
1056 let row_ptr = vec![0i32, 3, 4, 5, 6];
1057 let col_idx = vec![0i32, 1, 2, 1, 2, 3];
1058 let values = vec![10.0f64, 20.0, 30.0, 40.0, 50.0, 60.0];
1059 let hyb = hyb_from_csr_host(
1060 rows,
1061 cols,
1062 &row_ptr,
1063 &col_idx,
1064 &values,
1065 HybPartition::Fixed(1),
1066 )
1067 .expect("overflow test hyb construction");
1068 assert_eq!(hyb.coo_nnz(), 2, "row 0 overflows 2 entries to COO");
1069 for &r in hyb.coo_row_indices() {
1071 assert_eq!(r, 0i32, "all COO overflow entries should be from row 0");
1072 }
1073 }
1074}