Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
473 changes: 256 additions & 217 deletions Cargo.lock

Large diffs are not rendered by default.

24 changes: 15 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,30 @@
authors = ["Davy Duperron <yamafaktory@gmail.com>"]
categories = ["data-structures", "mathematics", "science"]
description = "Hypergraph is data structure library to create a directed hypergraph in which an hyperedge can join any number of vertices."
edition = "2021"
edition = "2024"
exclude = [".github", "hypergraph.svg", "rustfmt.toml"]
keywords = ["data-structure", "graph", "hypergraph"]
license = "MIT"
name = "hypergraph"
readme = "README.md"
repository = "https://github.com/yamafaktory/hypergraph"
rust-version = "1.56"
rust-version = "1.95"
version = "2.2.0"

[features]
serde = ["dep:serde", "indexmap/serde"]

[dependencies]
ahash = "0.8.11"
indexmap = { version = "2.6.0", features = ["rayon"] }
itertools = "0.13.0"
rayon = "1.7.0"
thiserror = "2.0.3"
ahash = "0.8.12"
indexmap = { version = "2.14.0", features = ["rayon"] }
itertools = "0.14.0"
rayon = "1.12.0"
serde = { version = "1.0.228", features = ["derive"], optional = true }
thiserror = "2.0.18"

[dev-dependencies]
criterion = "0.5.1"
criterion = "0.8.2"
serde_json = "1.0.149"

[[bench]]
name = "performance"
Expand All @@ -30,9 +35,10 @@ harness = false
missing_debug_implementations = "warn"
missing_docs = "warn"
nonstandard_style = { level = "deny", priority= -1 }
rust_2021_compatibility = { level = "forbid", priority= -1 }
rust_2024_compatibility = { level = "forbid", priority= -1 }
unreachable_pub = "warn"
unsafe_code = "deny"

[lints.clippy]
all = "deny"
pedantic = "deny"
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,24 @@ One of the main advantages of using a hypergraph model over a graph one is to pr

This library enables you to represent:

- **non-simple** hypergraphs with two or more hyperedges - with different weights - containing the exact same set of vertices
- **self-loops** - i.e., hyperedges containing vertices directed to themselves one or more times
- **unaries** - i.e., hyperedges containing a unique vertex
- **non-simple** hypergraphs with two or more hyperedges containing the exact same set of vertices
- **self-loops** — i.e., hyperedges containing vertices directed to themselves one or more times
- **unaries** — i.e., hyperedges containing a unique vertex

And to compute:

- Graph traversal: **BFS**, **DFS**, reachability, topological sort
- Shortest paths: **Dijkstra** point-to-point and single-source
- Structural analysis: strongly connected components, weakly connected components, all simple paths, subgraph extraction, cycle detection
- Filtered views: `retain_vertices`, `retain_hyperedges`

## ⚗️ Implementation

- 100% safe Rust
- Proper error handling
- Stable indexes assigned for each hyperedge and each vertex
- Stable indexes for each hyperedge and each vertex — identity is the index, not the weight; duplicate weights are allowed on both sides
- Parallelism (with Rayon)
- Optional `serde` support (`features = ["serde"]` in `Cargo.toml`)

## 🛠️ Installation

Expand Down
80 changes: 39 additions & 41 deletions benches/performance.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#![deny(unsafe_code, nonstandard_style)]
#![allow(missing_docs)]

use std::fmt::{
Display,
Expand All @@ -7,6 +8,7 @@ use std::fmt::{
};

use criterion::{
BatchSize,
Criterion,
criterion_group,
criterion_main,
Expand All @@ -18,36 +20,23 @@ use hypergraph::{
};
use itertools::Itertools;

static HYPEREDGES: usize = 10_000;
static VERTICES: usize = 10_000;
const VERTICES: usize = 1_000;

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct Vertex(pub usize);

impl Vertex {
pub fn new(rnd: usize) -> Self {
Vertex(rnd)
}
}
struct Vertex(usize);

impl Display for Vertex {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", self)
write!(f, "{}", self.0)
}
}

#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct Hyperedge(pub usize);

impl Hyperedge {
pub fn new(rnd: usize) -> Self {
Hyperedge(rnd)
}
}
struct Hyperedge(usize);

impl Display for Hyperedge {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(f, "{}", self)
write!(f, "{}", self.0)
}
}

Expand All @@ -57,52 +46,61 @@ impl From<Hyperedge> for usize {
}
}

fn criterion_benchmark(criterion: &mut Criterion) {
// Builds a linear-chain hypergraph: vertex 0 → 1 → 2 → … → VERTICES-1,
/// each consecutive pair joined by a hyperedge whose cost equals its index + 1.
fn build_graph() -> Hypergraph<Vertex, Hyperedge> {
let mut graph = Hypergraph::<Vertex, Hyperedge>::new();

for i in 0..VERTICES {
graph.add_vertex(Vertex::new(i)).unwrap();
graph.add_vertex(Vertex(i)).unwrap();
}

for i in 0..HYPEREDGES {
let vertices = (i..i + 1).map(VertexIndex).collect_vec();

graph.add_hyperedge(vertices, Hyperedge::new(i)).unwrap();
for i in 0..VERTICES - 1 {
graph
.add_hyperedge(vec![VertexIndex(i), VertexIndex(i + 1)], Hyperedge(i + 1))
.unwrap();
}

graph
}

fn criterion_benchmark(criterion: &mut Criterion) {
let graph = build_graph();

criterion.bench_function("get-hyperedge-vertices", |bencher| {
bencher.iter(|| graph.get_hyperedge_vertices(HyperedgeIndex((HYPEREDGES / 2) - 1)))
bencher.iter(|| graph.get_hyperedge_vertices(HyperedgeIndex(VERTICES / 2)))
});

criterion.bench_function("get-hyperedge-connecting", |bencher| {
criterion.bench_function("get-hyperedges-connecting", |bencher| {
bencher.iter(|| {
graph.get_hyperedges_connecting(
VertexIndex((VERTICES / 2) - 1),
VertexIndex(VERTICES / 2),
)
graph
.get_hyperedges_connecting(VertexIndex(VERTICES / 2), VertexIndex(VERTICES / 2 + 1))
})
});

criterion.bench_function("get-all-hyperedges-intersections", |bencher| {
bencher.iter(|| {
graph.get_hyperedges_intersections((0..HYPEREDGES).map(HyperedgeIndex).collect_vec())
})
criterion.bench_function("get-hyperedges-intersections", |bencher| {
bencher
.iter(|| graph.get_hyperedges_intersections((0..10).map(HyperedgeIndex).collect_vec()))
});

criterion.bench_function("dijkstra", |bencher| {
bencher.iter(|| graph.get_dijkstra_connections(VertexIndex(0), VertexIndex(VERTICES)))
});

criterion.bench_function("dijkstra-reversed", |bencher| {
bencher.iter(|| graph.get_dijkstra_connections(VertexIndex(VERTICES), VertexIndex(0)))
bencher.iter(|| graph.get_dijkstra_connections(VertexIndex(0), VertexIndex(VERTICES - 1)))
});

criterion.bench_function("remove-vertex", |bencher| {
bencher.iter(|| graph.remove_vertex(VertexIndex(VERTICES)))
bencher.iter_batched(
build_graph,
|mut g| g.remove_vertex(VertexIndex(VERTICES - 1)),
BatchSize::LargeInput,
)
});

criterion.bench_function("remove-hyperedge", |bencher| {
bencher.iter(|| graph.remove_hyperedge(HyperedgeIndex(HYPEREDGES)))
bencher.iter_batched(
build_graph,
|mut g| g.remove_hyperedge(HyperedgeIndex(VERTICES - 2)),
BatchSize::LargeInput,
)
});
}

Expand Down
2 changes: 1 addition & 1 deletion rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
edition = "2021"
edition = "2024"
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
imports_layout="Vertical"
Expand Down
9 changes: 6 additions & 3 deletions src/core/bi_hash_map.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
use std::{
collections::HashMap,
fmt::Debug,
hash::Hash,
};

/// Bi-directional hashmap used to store the mapping between the internal
/// unstable indexes - generated by `IndexMap` and `IndexSet` - and the exposed
/// stable indexes.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub(crate) struct BiHashMap<Index>
where
Index: Copy + Debug + Eq,
Index: Copy + Debug + Eq + Hash,
{
pub(crate) left: HashMap<usize, Index>,
pub(crate) right: HashMap<Index, usize>,
}

impl<Index> BiHashMap<Index>
where
Index: Copy + Debug + Eq,
Index: Copy + Debug + Eq + Hash,
{
/// Creates a new `BiHashMap` with no allocation.
pub(crate) fn new() -> BiHashMap<Index> {
Expand All @@ -29,7 +32,7 @@ where

impl<Index> Default for BiHashMap<Index>
where
Index: Copy + Debug + Eq,
Index: Copy + Debug + Eq + Hash,
{
fn default() -> Self {
BiHashMap::new()
Expand Down
20 changes: 0 additions & 20 deletions src/core/derive_trait.rs

This file was deleted.

23 changes: 20 additions & 3 deletions src/core/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ where
vertices: Vec<VertexIndex>,
},

/// Error when a hyperedge is updated with the weight of another one.
/// Kept for API compatibility.
///
/// Previously returned when a hyperedge was added or updated with a weight
/// already used by another hyperedge. The library no longer enforces
/// hyperedge-weight uniqueness, so this variant is never produced by any
/// public method.
#[error("Hyperedge weight {0} was already assigned")]
HyperedgeWeightAlreadyAssigned(HE),

Expand All @@ -77,15 +82,27 @@ where
#[error("Internal vertex index {0} was not found")]
InternalVertexIndexNotFound(usize),

/// Error when a vertex weight was not found.
/// Kept for API compatibility.
///
/// Previously used as an internal safe-check during vertex insertion.
/// No longer produced by any public method.
#[error("Vertex weight {0} was not found")]
VertexWeightNotFound(V),

/// Error when a vertex weight is updated with the same value.
#[error("VertexIndex {index:?} weight {weight:?} unchanged (no-op)")]
VertexWeightUnchanged { index: VertexIndex, weight: V },

/// Error when a vertex weight is updated with the weight of another one.
/// Kept for API compatibility.
///
/// Previously returned when a vertex was added or updated with a weight
/// already used by another vertex. The library no longer enforces
/// vertex-weight uniqueness, so this variant is never produced by any
/// public method.
#[error("Vertex weight {0} was already assigned")]
VertexWeightAlreadyAssigned(V),

/// Error when the hypergraph contains a cycle and a topological sort is requested.
#[error("Hypergraph contains a cycle and cannot be topologically sorted")]
HypergraphContainsCycle,
}
24 changes: 8 additions & 16 deletions src/core/hyperedges/add_hyperedge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ where
V: VertexTrait,
HE: HyperedgeTrait,
{
/// Adds a hyperedge as an array of vertices indexes and a custom weight in the hypergraph.
/// Returns the weighted index of the hyperedge.
/// Adds a hyperedge connecting `vertices` with the given `weight`.
/// Returns the stable index of the hyperedge.
///
/// Duplicate weights are allowed — multiple hyperedges may carry the same
/// weight value. The unique key is the `(vertices, weight)` combination: if
/// an identical pair already exists the existing [`HyperedgeIndex`] is
/// returned without creating a duplicate entry.
pub fn add_hyperedge(
&mut self,
vertices: Vec<VertexIndex>,
Expand All @@ -27,19 +32,6 @@ where

let internal_vertices = self.get_internal_vertices(vertices)?;

// Return an error if the weight is already assigned to another
// hyperedge.
// We can't use the contains method here since the key is a combination
// of the weight and the vertices.
if self.hyperedges.iter().any(
|HyperedgeKey {
weight: current_weight,
..
}| { *current_weight == weight },
) {
return Err(HypergraphError::HyperedgeWeightAlreadyAssigned(weight));
}

// We don't care about the second member of the tuple returned from
// the insertion since this is an infallible operation.
let (internal_index, _) = self
Expand All @@ -50,7 +42,7 @@ where
for vertex in internal_vertices {
let (_, index_set) = self
.vertices
.get_index_mut(vertex)
.get_mut(vertex)
.ok_or(HypergraphError::InternalVertexIndexNotFound(vertex))?;

index_set.insert(internal_index);
Expand Down
Loading
Loading