#![forbid(unsafe_code)]
#![warn(missing_docs)]
pub(crate) mod adjacency;
#[cfg(test)]
mod bit_keys;
pub(crate) mod budget;
pub mod cli;
pub mod collapse;
pub(crate) mod combinadic;
pub mod distances;
pub(crate) mod field;
pub mod io;
pub mod oracle;
pub(crate) mod parallel;
pub(crate) mod reduce;
pub(crate) mod simplex;
pub(crate) mod solver;
mod union_find;
use std::fmt;
pub use distances::{DistanceMatrix, SparseDistanceMatrix};
pub const GIT_HASH: &str = env!("HOLOS_GIT_HASH");
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const BUILD_PROFILE: &str = env!("HOLOS_BUILD_PROFILE");
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bar {
pub dim: usize,
pub birth: f64,
pub death: f64,
}
impl Bar {
pub fn is_essential(&self) -> bool {
self.death == f64::INFINITY
}
}
#[derive(Debug, Clone, Default)]
pub struct Diagram {
pub bars: Vec<Bar>,
}
impl Diagram {
pub fn in_dim(&self, dim: usize) -> impl Iterator<Item = &Bar> {
self.bars.iter().filter(move |b| b.dim == dim)
}
pub fn canonicalize(&mut self) {
self.bars.sort_by(|a, b| {
a.dim
.cmp(&b.dim)
.then(a.birth.total_cmp(&b.birth))
.then(a.death.total_cmp(&b.death))
});
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct RipsParams {
pub max_dim: usize,
pub threshold: Option<f64>,
pub modulus: u32,
pub threads: usize,
pub use_emergent_pairs: bool,
pub use_apparent_pairs: bool,
pub use_clearing: bool,
pub use_adjacency_rows: bool,
pub collapse_edges: bool,
pub collapse_schedule: CollapseSchedule,
pub engine: Engine,
pub dense_storage: DenseStorage,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Engine {
#[default]
Auto,
Dense,
Sparse,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum DenseStorage {
#[default]
Auto,
Compact,
Square,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum CollapseSchedule {
#[default]
Serial,
Ordered,
Rounds,
}
impl Default for RipsParams {
fn default() -> Self {
Self {
max_dim: 1,
threshold: None,
modulus: 2,
threads: 1,
use_emergent_pairs: true,
use_apparent_pairs: true,
use_clearing: true,
use_adjacency_rows: true,
collapse_edges: false,
collapse_schedule: CollapseSchedule::Serial,
engine: Engine::Auto,
dense_storage: DenseStorage::Auto,
}
}
}
impl RipsParams {
pub fn new(max_dim: usize) -> Self {
Self {
max_dim,
..Self::default()
}
}
pub fn with_threshold(mut self, threshold: f64) -> Self {
self.threshold = Some(threshold);
self
}
pub fn with_modulus(mut self, modulus: u32) -> Self {
self.modulus = modulus;
self
}
pub fn with_threads(mut self, threads: usize) -> Self {
self.threads = threads.max(1);
self
}
pub fn with_edge_collapse(mut self) -> Self {
self.collapse_edges = true;
self
}
pub fn with_collapse_schedule(mut self, schedule: CollapseSchedule) -> Self {
self.collapse_edges = true;
self.collapse_schedule = schedule;
self
}
pub fn with_engine(mut self, engine: Engine) -> Self {
self.engine = engine;
self
}
pub fn with_dense_storage(mut self, storage: DenseStorage) -> Self {
self.dense_storage = storage;
self
}
}
#[derive(Debug, Clone, PartialEq)]
#[allow(missing_docs)]
pub enum Error {
InvalidDistance(String),
InvalidInput(String),
IndexOverflow { n: usize, dim: usize },
Io(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::InvalidDistance(msg) => write!(f, "invalid distance: {msg}"),
Error::InvalidInput(msg) => write!(f, "invalid input: {msg}"),
Error::IndexOverflow { n, dim } => write!(
f,
"simplex index space overflows u64 for {n} points in dimension {dim}"
),
Error::Io(msg) => write!(f, "io error: {msg}"),
}
}
}
impl std::error::Error for Error {}
pub type Result<T> = std::result::Result<T, Error>;
const N_MIN: usize = 32;
const RHO_MAX_NUM: u128 = 4;
const RHO_MAX_DEN: u128 = 5;
fn resolved_threshold(dist: &DistanceMatrix, params: &RipsParams) -> f64 {
params.threshold.unwrap_or_else(|| dist.enclosing_radius())
}
fn may_route(n: usize, threshold: f64) -> bool {
n >= N_MIN && threshold >= 0.0
}
fn density_routes(n: usize, edges: usize) -> bool {
RHO_MAX_DEN * edges as u128 <= RHO_MAX_NUM * pair_count(n)
}
const CONVERSION_BYTES_PER_EDGE: u128 = 24;
const CONVERSION_BYTES_PER_POINT: u128 = 24;
const MIN_CONVERSION_BYTES: u128 = 32 * 1024 * 1024;
fn pair_count(n: usize) -> u128 {
let n = n as u128;
n * n.saturating_sub(1) / 2
}
fn memory_routes(n: usize, edges: usize) -> bool {
let extra =
CONVERSION_BYTES_PER_EDGE * edges as u128 + CONVERSION_BYTES_PER_POINT * n as u128 + 8;
let budget = MIN_CONVERSION_BYTES.max(pair_count(n) * 8);
extra <= budget
}
fn graph_routes(n: usize, edges: usize) -> bool {
density_routes(n, edges) && memory_routes(n, edges)
}
const SQUARE_MIN_BYTES: u128 = 4 << 20;
const SQUARE_EXTRA_MAX_BYTES: u128 = 256 << 20;
const SQUARE_READS_PER_CELL: u128 = 4;
fn square_extra_bytes(n: usize) -> u128 {
let n = n as u128;
n * (n + 1) / 2 * 8
}
fn fold_reads(n: usize, edges: usize, max_dim: usize) -> u128 {
let n = n as u128;
n * n + edges as u128 * n * max_dim.max(1) as u128
}
fn square_size_fits(n: usize) -> bool {
pair_count(n) * 8 >= SQUARE_MIN_BYTES && square_extra_bytes(n) <= SQUARE_EXTRA_MAX_BYTES
}
fn square_work_pays(n: usize, edges: usize, max_dim: usize) -> bool {
let cells = n as u128 * n as u128;
fold_reads(n, edges, max_dim) >= SQUARE_READS_PER_CELL * cells
}
fn square_selected(
dist: &DistanceMatrix,
params: &RipsParams,
threshold: f64,
edges: Option<usize>,
) -> bool {
match params.dense_storage {
DenseStorage::Compact => false,
DenseStorage::Square => true,
DenseStorage::Auto => {
let n = dist.len();
if !square_size_fits(n) {
return false;
}
let edges = edges.unwrap_or_else(|| dist.count_edges_at(threshold));
square_work_pays(n, edges, params.max_dim)
}
}
}
fn solve_dense(
dist: &DistanceMatrix,
params: &RipsParams,
threshold: f64,
edges: Option<usize>,
) -> Result<Diagram> {
if square_selected(dist, params, threshold, edges) {
return solver::compute(&dist.to_square(), params);
}
solver::compute(dist, params)
}
fn solve_thresholded(
dist: &DistanceMatrix,
params: &RipsParams,
threshold: f64,
) -> Result<Diagram> {
let sparse = dist.to_sparse_at(threshold)?;
let mut inner = params.clone();
inner.threshold = Some(threshold);
solver::compute(&sparse, &inner)
}
pub fn rips_persistence(dist: &DistanceMatrix, params: &RipsParams) -> Result<Diagram> {
if params.collapse_edges {
return collapse_and_solve(dist, params, |_| {});
}
let threshold = resolved_threshold(dist, params);
let mut resolved = params.clone();
resolved.threshold = Some(threshold);
match params.engine {
Engine::Dense => solve_dense(dist, &resolved, threshold, None),
Engine::Sparse => solve_thresholded(dist, params, threshold),
Engine::Auto => {
let mut counted = None;
if may_route(dist.len(), threshold) {
let edges = dist.count_edges_at(threshold);
if graph_routes(dist.len(), edges) {
return solve_thresholded(dist, &resolved, threshold);
}
counted = Some(edges);
}
solve_dense(dist, &resolved, threshold, counted)
}
}
}
pub fn rips_persistence_sparse(
dist: &SparseDistanceMatrix,
params: &RipsParams,
) -> Result<Diagram> {
if params.collapse_edges {
return collapse_and_solve(dist, params, |_| {});
}
solver::compute(dist, params)
}
pub(crate) fn collapse_and_solve<D: distances::Distances + Sync>(
dist: &D,
params: &RipsParams,
report: impl FnOnce(&collapse::CollapsedRips),
) -> Result<Diagram> {
let build_pool = || -> Result<Option<rayon::ThreadPool>> {
if params.threads > 1 {
Ok(Some(
rayon::ThreadPoolBuilder::new()
.num_threads(params.threads)
.build()
.map_err(|e| Error::Io(format!("thread pool: {e}")))?,
))
} else {
Ok(None)
}
};
let (collapsed, pool) = match params.collapse_schedule {
CollapseSchedule::Serial => {
let collapsed = collapse::collapse_serial_in(dist, params.threshold)?;
(collapsed, build_pool()?)
}
CollapseSchedule::Ordered => {
let pool = build_pool()?;
let collapsed = collapse::collapse_ordered_in(dist, params.threshold, pool.as_ref())?;
(collapsed, pool)
}
CollapseSchedule::Rounds => {
let pool = build_pool()?;
let collapsed = collapse::collapse_rounds_in(dist, params.threshold, pool.as_ref())?;
(collapsed, pool)
}
};
report(&collapsed);
let mut inner = params.clone();
inner.collapse_edges = false;
inner.threshold = Some(collapsed.certificate.terminal_level());
solver::compute_in(&collapsed.matrix, &inner, pool)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::collapse::verify::verify_dense;
fn grid(side: usize) -> DistanceMatrix {
let mut points = Vec::new();
for i in 0..side {
for j in 0..side {
points.push(vec![i as f64, j as f64]);
}
}
DistanceMatrix::from_points(&points).unwrap()
}
fn bits(d: &Diagram) -> Vec<(usize, u64, u64)> {
d.bars
.iter()
.map(|b| (b.dim, b.birth.to_bits(), b.death.to_bits()))
.collect()
}
fn counting<T>(f: impl FnOnce() -> T) -> (usize, T) {
distances::SQUARE_BUILDS.with(|c| c.set(0));
let value = f();
(distances::SQUARE_BUILDS.with(|c| c.get()), value)
}
#[test]
fn the_routing_rule_holds_its_constants() {
assert!(!may_route(N_MIN - 1, 1.0), "under N_MIN nothing routes");
assert!(may_route(N_MIN, 1.0));
assert!(
may_route(N_MIN, f64::INFINITY),
"an infinite threshold routes"
);
for bad in [f64::NAN, -1.0] {
assert!(!may_route(N_MIN, bad), "threshold {bad} must not route");
}
for (n, last_routed) in [
(N_MIN, 396usize),
(33, 422),
(100, 3960),
(1001, 400_400),
(4000, 6_398_400),
] {
let pairs = n * (n - 1) / 2;
assert_eq!(last_routed, 4 * pairs / 5, "{n}: the cutoff moved");
assert!(density_routes(n, 0), "{n}: an empty graph routes");
assert!(density_routes(n, last_routed), "{n}: the cutoff routes");
assert!(!density_routes(n, last_routed + 1), "{n}: one edge over");
assert!(!density_routes(n, pairs), "{n}: a complete graph");
}
}
#[test]
fn the_conversion_budget_gates_at_the_byte() {
let small = 1000;
assert!(pair_count(small) * 8 < MIN_CONVERSION_BYTES);
let last_fit = ((MIN_CONVERSION_BYTES - 24 * small as u128 - 8) / 24) as usize;
assert_eq!(last_fit, 1_397_101);
assert!(memory_routes(small, last_fit));
assert!(!memory_routes(small, last_fit + 1));
let large = 20_000;
let budget = pair_count(large) * 8;
assert!(budget > MIN_CONVERSION_BYTES);
let last_fit = ((budget - 24 * large as u128 - 8) / 24) as usize;
assert_eq!(last_fit, 66_643_333);
assert!(memory_routes(large, last_fit));
assert!(!memory_routes(large, last_fit + 1));
}
#[test]
fn a_large_near_clique_stays_dense() {
let n = 2000;
let data: Vec<f64> = (0..n * (n - 1) / 2)
.map(|k| if k % 4 == 0 { 3.0 } else { 1.0 })
.collect();
let dist = DistanceMatrix::from_condensed(data).unwrap();
let threshold = 1.0;
let edges = dist.count_edges_at(threshold);
assert!(edges > 1_000_000, "{edges} edges");
assert!(may_route(n, threshold));
assert!(density_routes(n, edges), "the density cutoff accepts it");
assert!(!memory_routes(n, edges), "the budget must refuse it");
assert!(!graph_routes(n, edges));
}
#[test]
fn the_storage_rule_holds_its_constants() {
assert!(!square_size_fits(1024), "1024 points stay compact");
assert!(
square_size_fits(1025),
"1025 points may take both triangles"
);
assert!(square_size_fits(8191), "8191 points fit the byte budget");
assert!(!square_size_fits(8192), "8192 points exceed it");
assert_eq!(square_extra_bytes(8191), 268_402_688);
for n in [1025usize, 2400, 8191] {
let last_refused = (SQUARE_READS_PER_CELL as usize - 1) * n - 1;
assert!(!square_work_pays(n, last_refused, 1), "{n}: one edge under");
assert!(
square_work_pays(n, last_refused + 1, 1),
"{n}: at the cutoff"
);
assert!(
square_work_pays(n, last_refused / 2 + 1, 2),
"{n}: max_dim 2"
);
}
assert!(!square_work_pays(2400, 863, 1), "a low threshold refuses");
assert!(
square_work_pays(2400, 14_273, 1),
"a sparse block graph pays"
);
}
#[test]
fn a_routed_run_never_builds_the_full_form() {
let n = 1030;
let dist = band(n, 4);
let threshold = resolved_threshold(&dist, &RipsParams::new(1));
let edges = dist.count_edges_at(threshold);
assert!(
may_route(n, threshold) && graph_routes(n, edges),
"the fixture must route: {edges} edges"
);
assert!(
square_size_fits(n) && square_work_pays(n, edges, 1),
"the storage rule must want the full form here"
);
let params = RipsParams::new(1);
let mut reference = None;
for storage in [
DenseStorage::Auto,
DenseStorage::Compact,
DenseStorage::Square,
] {
let p = params.clone().with_dense_storage(storage);
let (built, diagram) = counting(|| rips_persistence(&dist, &p).unwrap());
assert_eq!(built, 0, "{storage:?}: a routed run stays compact");
let bits = bits(&diagram);
assert_eq!(*reference.get_or_insert(bits.clone()), bits, "{storage:?}");
}
for (storage, want) in [
(DenseStorage::Auto, 1),
(DenseStorage::Compact, 0),
(DenseStorage::Square, 1),
] {
let p = params
.clone()
.with_engine(Engine::Dense)
.with_dense_storage(storage);
let (built, diagram) = counting(|| rips_persistence(&dist, &p).unwrap());
assert_eq!(built, want, "{storage:?}: conversions");
assert_eq!(bits(&diagram), *reference.as_ref().unwrap(), "{storage:?}");
}
}
#[test]
fn every_storage_form_gives_one_diagram() {
let dist = grid(6);
assert!(
!square_size_fits(dist.len()),
"the fixture is under the bound"
);
let params = RipsParams::new(2);
let mut reference = None;
for engine in [Engine::Auto, Engine::Dense, Engine::Sparse] {
for storage in [
DenseStorage::Auto,
DenseStorage::Compact,
DenseStorage::Square,
] {
let p = params
.clone()
.with_engine(engine)
.with_dense_storage(storage);
let (built, diagram) = counting(|| rips_persistence(&dist, &p).unwrap());
let dense_run = engine == Engine::Dense
|| (engine == Engine::Auto
&& !graph_routes(
dist.len(),
dist.count_edges_at(resolved_threshold(&dist, &p)),
));
let want = usize::from(dense_run && storage == DenseStorage::Square);
assert_eq!(built, want, "{engine:?}, {storage:?}: conversions");
let bits = bits(&diagram);
assert_eq!(
*reference.get_or_insert(bits.clone()),
bits,
"{engine:?}, {storage:?}"
);
}
}
}
fn band(n: usize, width: usize) -> DistanceMatrix {
let mut data = Vec::with_capacity(n * (n - 1) / 2);
for i in 1..n {
for j in 0..i {
data.push(if i - j <= width {
(i - j) as f64
} else {
f64::INFINITY
});
}
}
DistanceMatrix::from_condensed(data).unwrap()
}
#[test]
fn an_infinite_threshold_routes_on_density() {
let n = 40;
let dist = band(n, 3);
assert_eq!(dist.enclosing_radius(), f64::INFINITY);
let edges = dist.count_edges_at(f64::INFINITY);
assert_eq!(edges, 3 * n - 6, "the band holds its own edges");
assert!(may_route(n, f64::INFINITY) && graph_routes(n, edges));
let complete = grid(7);
let n = complete.len();
let all = complete.count_edges_at(f64::INFINITY);
assert_eq!(all, n * (n - 1) / 2, "every pair of a grid is finite");
assert!(!graph_routes(n, all), "a complete matrix stays dense");
let params = RipsParams::new(2);
let dense = rips_persistence(&dist, ¶ms.clone().with_engine(Engine::Dense)).unwrap();
for threshold in [None, Some(f64::INFINITY)] {
let mut p = params.clone();
p.threshold = threshold;
for engine in [Engine::Auto, Engine::Sparse] {
let got = rips_persistence(&dist, &p.clone().with_engine(engine)).unwrap();
assert_eq!(bits(&got), bits(&dense), "{threshold:?}, {engine:?}");
}
}
}
#[test]
fn a_routed_input_gives_the_dense_diagram() {
let side = (N_MIN as f64).sqrt().ceil() as usize + 1;
let dist = grid(side);
let threshold = 1.5;
let edges = dist.count_edges_at(threshold);
assert!(
may_route(dist.len(), threshold) && graph_routes(dist.len(), edges),
"the fixture must route: {} points, {edges} edges",
dist.len()
);
let params = RipsParams::new(1).with_threshold(threshold);
let dense = rips_persistence(&dist, ¶ms.clone().with_engine(Engine::Dense)).unwrap();
for engine in [Engine::Auto, Engine::Sparse] {
let got = rips_persistence(&dist, ¶ms.clone().with_engine(engine)).unwrap();
assert_eq!(bits(&got), bits(&dense), "{engine:?}");
}
}
#[test]
fn routing_carries_the_dense_default_threshold() {
let side = (N_MIN as f64).sqrt().ceil() as usize + 1;
let dist = grid(side);
let radius = dist.enclosing_radius();
let params = RipsParams::new(1);
let auto = rips_persistence(&dist, ¶ms).unwrap();
let explicit = rips_persistence(
&dist,
¶ms
.clone()
.with_engine(Engine::Sparse)
.with_threshold(radius),
)
.unwrap();
assert_eq!(bits(&auto), bits(&explicit));
assert!(dist.count_edges_at(radius) < dist.len() * (dist.len() - 1) / 2);
}
#[test]
fn pipeline_runs_the_selected_schedule() {
let dist = grid(5);
let plain = rips_persistence(&dist, &RipsParams::new(2)).unwrap();
let mut seen = Vec::new();
for schedule in [
CollapseSchedule::Serial,
CollapseSchedule::Ordered,
CollapseSchedule::Rounds,
] {
let params = RipsParams::new(2)
.with_threads(4)
.with_collapse_schedule(schedule);
let mut captured = None;
let diagram =
collapse_and_solve(&dist, ¶ms, |c| captured = Some(c.clone())).unwrap();
let captured = captured.expect("report must see the collapse");
verify_dense(&dist, None, &captured).unwrap();
let expected_version = if schedule == CollapseSchedule::Rounds {
2
} else {
1
};
assert_eq!(captured.certificate.algorithm_version(), expected_version);
let mut a = diagram.clone();
let mut b = plain.clone();
a.canonicalize();
b.canonicalize();
assert_eq!(a.bars, b.bars, "{schedule:?}");
seen.push((schedule, captured.stats));
}
let serial = seen[0].1;
let ordered = seen[1].1;
assert_eq!(ordered.logical_tests, serial.edge_tests);
assert!(
ordered.edge_tests > serial.edge_tests,
"the ordered schedule did not speculate: {} vs {}",
ordered.edge_tests,
serial.edge_tests
);
assert_eq!(serial.window_batches, 0);
assert!(ordered.window_batches > 0);
}
}