Skip to main content

data_beans/sparse_io/
helpers.rs

1#[cfg(feature = "ndarray")]
2use ndarray::prelude::*;
3use rustc_hash::FxHashMap as HashMap;
4
5use super::DMatrix;
6
7pub fn build_name2index_map(_names: &[Box<str>]) -> HashMap<Box<str>, usize> {
8    _names
9        .iter()
10        .enumerate()
11        .map(|(r, name)| (name.clone(), r))
12        .collect()
13}
14
15pub fn take_subset_indices_names(
16    new_indices: &[usize],
17    ntot: usize,
18    old_names: Vec<Box<str>>,
19) -> (HashMap<u64, u64>, Vec<Box<str>>) {
20    let mut old2new: HashMap<u64, u64> = Default::default();
21    let mut new2old = vec![];
22    debug_assert!(ntot == old_names.len());
23    let mut k = 0_u64;
24    for idx in new_indices.iter() {
25        if *idx < ntot {
26            old2new.insert(*idx as u64, k);
27            new2old.push(*idx);
28            k += 1;
29        }
30    }
31
32    let new_names = new2old
33        .iter()
34        .map(|&i| old_names[i].clone())
35        .collect::<Vec<Box<str>>>();
36
37    (old2new, new_names)
38}
39
40pub fn take_subset_indices_names_if_needed(
41    new_indices: Option<&Vec<usize>>,
42    ntot: Option<usize>,
43    old_names: Vec<Box<str>>,
44) -> (HashMap<u64, u64>, Vec<Box<str>>) {
45    let ntot = ntot.unwrap_or(old_names.len());
46    if let Some(new_indices) = new_indices {
47        take_subset_indices_names(new_indices, ntot, old_names)
48    } else {
49        let names = old_names;
50        let identity = (0..(ntot as u64))
51            .zip(0..(ntot as u64))
52            .collect::<HashMap<u64, u64>>();
53        (identity, names)
54    }
55}
56
57#[cfg(feature = "ndarray")]
58pub fn ndarray_to_triplets(array: &Array2<f32>) -> Vec<(u64, u64, f32)> {
59    let eps = 1e-6;
60    array
61        .indexed_iter()
62        .filter(|(_, &elem)| elem.abs() > eps)
63        .map(|((row, col), &value)| (row as u64, col as u64, value))
64        .collect::<Vec<(u64, u64, f32)>>()
65}
66
67pub fn dmatrix_to_triplets(matrix: &DMatrix<f32>) -> Vec<(u64, u64, f32)> {
68    let (nrow, _) = matrix.shape();
69    let eps = 1e-6;
70    matrix
71        .iter() // column-major
72        .enumerate()
73        .filter(|(_, &elem)| elem.abs() > eps)
74        .map(|(idx, &value)| {
75            let row = idx % nrow;
76            let col = idx / nrow;
77            (row as u64, col as u64, value)
78        })
79        .collect::<Vec<(u64, u64, f32)>>()
80}
81
82/// Remove a backend at `path`, whether it is a file (`.h5`, `.zarr.zip`) or a
83/// directory (`.zarr`). Nothing happens when the path does not exist.
84pub fn remove_backend_path(path: &str) -> anyhow::Result<()> {
85    let p = std::path::Path::new(path);
86    if p.exists() {
87        if p.is_file() {
88            std::fs::remove_file(p)?;
89        } else {
90            std::fs::remove_dir_all(p)?;
91        }
92    }
93    Ok(())
94}
95
96/// Whether a preload of `nnz` entries fits the budget.
97///
98/// Preloading costs 12 bytes per non-zero (a `u64` index and an `f32` value)
99/// and no consumer ever releases it, so an unchecked preload at imaging scale
100/// is an OOM order, not a request. The budget turns that into a logged skip:
101/// every read path already handles the not-preloaded state, it is just slower.
102///
103/// The budget is half the memory available when the process first asks,
104/// shared by every preload of the process (8 GiB when the system does not
105/// say). `LEGUME_PRELOAD_BUDGET_BYTES` overrides it per preload, following the
106/// `LEGUME_ZARR_CACHE_CAP` precedent for memory knobs.
107pub fn preload_within_budget(nnz: usize, what: &str) -> bool {
108    use std::sync::atomic::{AtomicUsize, Ordering};
109    const BYTES_PER_NNZ: usize = 12;
110    static RESERVED: AtomicUsize = AtomicUsize::new(0);
111
112    let cost = nnz.saturating_mul(BYTES_PER_NNZ);
113    if let Some(budget) = std::env::var("LEGUME_PRELOAD_BUDGET_BYTES")
114        .ok()
115        .and_then(|v| v.parse::<usize>().ok())
116    {
117        return fits_or_warn(cost, budget, nnz, what);
118    }
119    let budget = auto_preload_budget_bytes();
120    let reserved = RESERVED.fetch_add(cost, Ordering::Relaxed);
121    if fits_or_warn(reserved.saturating_add(cost), budget, nnz, what) {
122        true
123    } else {
124        RESERVED.fetch_sub(cost, Ordering::Relaxed);
125        false
126    }
127}
128
129fn fits_or_warn(cost: usize, budget: usize, nnz: usize, what: &str) -> bool {
130    if cost > budget {
131        log::warn!(
132            "skipping {what} preload of {nnz} nnz: {cost} bytes would exceed the \
133             {budget}-byte budget (LEGUME_PRELOAD_BUDGET_BYTES to raise); reads stay on the \
134             streaming path"
135        );
136        false
137    } else {
138        true
139    }
140}
141
142/// Half the memory available at the first call, or 8 GiB when unknown.
143fn auto_preload_budget_bytes() -> usize {
144    static BUDGET: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
145    *BUDGET.get_or_init(|| {
146        const FALLBACK_BYTES: usize = 8 << 30;
147        let mut sys = sysinfo::System::new();
148        sys.refresh_memory();
149        match usize::try_from(sys.available_memory()) {
150            Ok(avail) if avail > 0 => avail / 2,
151            _ => FALLBACK_BYTES,
152        }
153    })
154}
155
156/// Whether loaders preload columns without being asked. On unless
157/// `LEGUME_AUTO_PRELOAD` is `0`, `false` or `off`; the preload itself still
158/// answers to [`preload_within_budget`], so data that does not fit in memory
159/// stays on the streaming path.
160#[allow(dead_code)] // called by library loaders; the bin has none
161pub fn auto_preload_enabled() -> bool {
162    !matches!(
163        std::env::var("LEGUME_AUTO_PRELOAD").as_deref(),
164        Ok("0") | Ok("false") | Ok("off")
165    )
166}
167
168/// Bytes of `(u64, u64, f32)` triplets a streaming-write slab may hold, and the
169/// padded size of one such triplet. One definition, because the two streaming
170/// pipelines (the subset trait method and the handlers' column-selection
171/// writer) each carried their own copy and two memory ceilings drift apart.
172pub const SLAB_BUDGET_BYTES: usize = 256 << 20;
173/// `(u64, u64, f32)` padded.
174pub const TRIPLET_BYTES: usize = 24;