Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

crates/mimir-graph/src/arrow.rs

//! CSR straight from Arrow edge batches (plan §11: sorted scan of
//! `code_edges` + cumsum over `from` → `GrB_Matrix`).
//!
//! Input: one or more `RecordBatch`es with two non-null `UInt64` columns
//! holding node ordinals `0..n_nodes`. Three paths, picked by one scan:
//!
//! - **sorted by (from, to), no duplicates** (what a sorted `code_edges` scan
//!   yields): offsets by counting + cumsum written into a malloc'd array, the
//!   `to` buffer copied once, `GxB_Matrix_pack_CSR` (no further copy).
//! - **sorted by from only**: same, rows marked jumbled; a duplicate check
//!   runs and, if any `(from, to)` repeats, falls through to the build path.
//! - **unsorted**: `GxB_Matrix_build_Scalar` (GraphBLAS' parallel sort,
//!   duplicates collapse). With one batch it reads the Arrow buffers in place.

use arrow_array::{Array, RecordBatch, UInt64Array};
use arrow_schema::DataType;

use crate::csr::{MallocBuf, build_iso, pack, validate};
use crate::error::{Error, Result};
use crate::matrix::Matrix;

/// Which import path [`from_arrow_edge_batches`] took (for benches and logs).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImportPath {
    /// Sorted by (from, to), packed without sorting.
    SortedPack,
    /// Sorted by from, rows jumbled, packed (GraphBLAS sorts rows lazily).
    FromSortedPack,
    /// Unsorted or duplicated: built by GraphBLAS.
    Build,
}

fn u64_column<'a>(batch: &'a RecordBatch, name: &str) -> Result<&'a [u64]> {
    let col = batch.column_by_name(name).ok_or_else(|| {
        Error::Arrow(format!(
            "no column `{name}` in batch {:?}",
            batch
                .schema()
                .fields()
                .iter()
                .map(|f| f.name())
                .collect::<Vec<_>>()
        ))
    })?;
    if col.data_type() != &DataType::UInt64 {
        return Err(Error::Arrow(format!(
            "column `{name}` is {}, want UInt64",
            col.data_type()
        )));
    }
    if col.null_count() > 0 {
        return Err(Error::Arrow(format!(
            "column `{name}` has {} nulls",
            col.null_count()
        )));
    }
    let a = col
        .as_any()
        .downcast_ref::<UInt64Array>()
        .ok_or_else(|| Error::Arrow(format!("column `{name}` is not a UInt64Array")))?;
    Ok(a.values().as_ref())
}

/// `n_nodes × n_nodes` boolean adjacency from one edge batch.
pub fn from_arrow_edges(
    batch: &RecordBatch,
    from_col: &str,
    to_col: &str,
    n_nodes: u64,
) -> Result<Matrix<bool>> {
    Ok(from_arrow_edge_batches(std::slice::from_ref(batch), from_col, to_col, n_nodes)?.0)
}

/// Same over a scan's batches (treated as one concatenated edge list); also
/// returns which path was taken.
pub fn from_arrow_edge_batches(
    batches: &[RecordBatch],
    from_col: &str,
    to_col: &str,
    n_nodes: u64,
) -> Result<(Matrix<bool>, ImportPath)> {
    let mut chunks = Vec::with_capacity(batches.len());
    for b in batches {
        chunks.push((u64_column(b, from_col)?, u64_column(b, to_col)?));
    }
    from_edge_chunks(&chunks, n_nodes)
}

/// The engine behind the Arrow entry points, over plain `(from, to)` slices.
pub fn from_edge_chunks(
    chunks: &[(&[u64], &[u64])],
    n_nodes: u64,
) -> Result<(Matrix<bool>, ImportPath)> {
    let mut m = 0usize;
    for (f, t) in chunks {
        if f.len() != t.len() {
            return Err(Error::Arrow(format!(
                "from has {} rows, to has {}",
                f.len(),
                t.len()
            )));
        }
        m += f.len();
    }

    // One scan: bounds, sorted by from, sorted by (from, to) strictly.
    let mut from_sorted = true;
    let mut pair_sorted = true;
    let mut prev: Option<(u64, u64)> = None;
    for (f, t) in chunks {
        for (&a, &b) in f.iter().zip(t.iter()) {
            if a >= n_nodes || b >= n_nodes {
                return Err(Error::invalid(format!(
                    "edge ({a}, {b}) out of range for {n_nodes} nodes"
                )));
            }
            if let Some((pa, pb)) = prev {
                if a < pa {
                    from_sorted = false;
                    pair_sorted = false;
                } else if a == pa && b <= pb {
                    pair_sorted = false;
                }
            }
            prev = Some((a, b));
        }
        if !from_sorted {
            break;
        }
    }

    if from_sorted {
        // Counting + cumsum straight into the array GraphBLAS will own.
        let n = n_nodes as usize;
        let mut offsets = MallocBuf::<u64>::zeroed(n + 1)?;
        {
            let o = offsets.as_mut_slice();
            for (f, _) in chunks {
                for &a in f.iter() {
                    o[a as usize + 1] += 1;
                }
            }
            for i in 0..n {
                o[i + 1] += o[i];
            }
        }
        let mut cols = MallocBuf::<u64>::new(m)?;
        {
            let c = cols.as_mut_slice();
            let mut at = 0;
            for (_, t) in chunks {
                c[at..at + t.len()].copy_from_slice(t);
                at += t.len();
            }
        }
        if pair_sorted {
            let mat = pack::<bool>(n_nodes, n_nodes, offsets, cols, None, true)?;
            // a pack pins CSR: let GraphBLAS go hypersparse when rows are mostly empty
            mat.conform()?;
            return Ok((mat, ImportPath::SortedPack));
        }
        // Rows may be jumbled; duplicates are not allowed in a pack.
        if validate(n_nodes, n_nodes, offsets.as_slice(), cols.as_slice()).is_ok() {
            let mat = pack::<bool>(n_nodes, n_nodes, offsets, cols, None, false)?;
            mat.conform()?;
            return Ok((mat, ImportPath::FromSortedPack));
        }
    }

    let mat = Matrix::<bool>::new(n_nodes, n_nodes)?;
    if let [(f, t)] = chunks {
        build_iso(&mat, f.as_ptr(), t.as_ptr(), f.len() as u64)?;
    } else {
        let f: Vec<u64> = chunks.iter().flat_map(|c| c.0.iter().copied()).collect();
        let t: Vec<u64> = chunks.iter().flat_map(|c| c.1.iter().copied()).collect();
        build_iso(&mat, f.as_ptr(), t.as_ptr(), f.len() as u64)?;
    }
    Ok((mat, ImportPath::Build))
}