21 Commits
Author SHA1 Message Date
warrence 783a82fc18 Refactor generators tests and add missing tests 2026-10-22 20:40:27 +02:00
warrence be7acfce1f Fix integer underflows in path and complete graph generators 2026-10-22 20:37:32 +02:00
warrence f5bb3ad07a Change generate_as and petersen to return Vecs instead of arrays 2026-10-22 20:30:24 +02:00
warrence baa6775156 Fix docs in testing helper assert_valid_path 2026-10-21 22:03:40 +02:00
warrence 32fae0b24c Add generators::complete, without tests 2026-10-18 11:58:26 +02:00
warrence 026005fe46 Change path graph size parameter by 1, add doc link and P(n) notation 2026-10-18 11:57:51 +02:00
warrence 421207a1cd Update cycle graph docs with link and C(n) denotation 2026-10-18 11:53:37 +02:00
warrence 9b11090e82 Fix missing loop var usage 2026-10-18 11:48:23 +02:00
warrence d1463544f7 Add generators::cycle, without tests 2026-10-17 00:40:33 +02:00
warrence ba3717f246 Add generators::path, without tests 2026-10-17 00:32:42 +02:00
warrence f34a7e6541 Simplify generators::petersen function signature formatting 2026-10-17 00:20:58 +02:00
warrence 5fc586f58e Use prelude import in generators doc example 2026-10-17 00:19:40 +02:00
warrence 0cd965db4f Merge pull request 'Update package version' (#10) from release-0.3.1 into main
Reviewed-on: #10
2026-10-16 20:36:17 +02:00
warrence 6f557a92aa Update package version 2026-10-16 20:35:54 +02:00
warrence 536a39c82b Merge pull request 'Common neighbors' (#9) from common-neighbors into main
Reviewed-on: #9
2026-10-16 20:30:31 +02:00
warrence 6403862977 Merge branch 'fixes' into common-neighbors 2026-10-16 20:29:11 +02:00
warrence 3ae0aa9b67 Update generators unit tests to cover generate_as and better validate Petersen graph 2026-10-16 20:28:48 +02:00
warrence 5b6ad13f0e Add generators::generate_as 2026-10-16 20:25:40 +02:00
warrence f41b77903f Add docs for MakeTestGraph generator methods 2026-10-16 20:23:58 +02:00
warrence 3fb3e67e8e Add GraphTopology::common_neighbors 2026-10-16 20:19:08 +02:00
warrence 8c57f5d2c4 Fix import order 2026-10-15 20:09:35 +02:00
6 changed files with 755 additions and 58 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "grapherity"
version = "0.3.0"
version = "0.3.1"
authors = ["Stefan Müller"]
edition = "2024"
rust-version = "1.85.0"
+548 -37
View File
@@ -1,6 +1,119 @@
//! Generators for common graphs.
use crate::traits::{GraphTopology, GraphTopologyAddition};
use crate::models::AppendGraph;
use crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyConversion};
/// Builds a graph of type `G` by running a generator on a temporary graph.
///
/// `generate_as` fills a temporary [`AppendGraph`] and converts it to `G`. It returns the handles
/// of the elements it created as handles of the resulting graph. This makes it possible to obtain
/// graph types directly that do not implement [`GraphTopologyAddition`], such as [`FrozenGraph`].
///
/// # Examples
///
/// ```
/// use grapherity::prelude::*;
/// use grapherity::generators;
/// use grapherity::models::FrozenGraph;
///
/// // Generates a parameterless Petersen graph as a FrozenGraph model.
/// let (graph, vertices, edges) = generators::generate_as::<FrozenGraph>(generators::petersen);
/// assert_eq!(graph.vertex_count(), 10);
/// assert!(graph.vertices().all(|v| graph.degree(v) == 3));
///
/// // Generates a parameterized complete graph as a FrozenGraph model.
/// let (graph, vertices, edges) = generators::generate_as::<FrozenGraph>(|g| generators::complete(g, 5));
/// assert_eq!(graph.vertex_count(), 5);
/// assert!(graph.vertices().all(|v| graph.degree(v) == 4));
/// ```
///
/// [`FrozenGraph`]: crate::models::FrozenGraph
pub fn generate_as<G>(
generator: impl FnOnce(
&mut AppendGraph,
) -> (
Vec<<AppendGraph as GraphTopology>::Vertex>,
Vec<<AppendGraph as GraphTopology>::Edge>,
),
) -> (G, Vec<G::Vertex>, Vec<G::Edge>)
where
G: GraphTopologyConversion,
{
let mut builder = AppendGraph::new();
let (vertices, edges) = generator(&mut builder);
let (graph, iso) = G::convert(&builder);
(
graph,
vertices.into_iter().map(|v| iso.vertex(v)).collect(),
edges.into_iter().map(|e| iso.edge(e)).collect(),
)
}
/// Generates a path graph.
///
/// Generates a [path graph] *P(n)* with `n` vertices and `n - 1` edges in `graph` as a new
/// connected component and returns the new vertices *v0, v1, ..., v(n - 1)* and edges
/// *e0, e1, ..., e(n - 2)* in order, i.e. *e0 = (v0, v1)*, *e1 = (v1, v2)*, etc. and *v0* and
/// *v(n-1)* being the two terminal vertices
///
/// For `n == 0`, nothing is added. For `n == 1`, only a single vertex and no edges are added.
///
/// [Path graph]: https://en.wikipedia.org/wiki/Path_graph
pub fn path<G: GraphTopologyAddition>(graph: &mut G, n: usize) -> (Vec<G::Vertex>, Vec<G::Edge>) {
let edge_capacity = n.saturating_sub(1);
graph.reserve_vertices(n);
graph.reserve_edges(edge_capacity);
let vertices: Vec<_> = (0..n).map(|_| graph.add_vertex()).collect();
let edges = (0..edge_capacity)
.map(|i| graph.add_edge(vertices[i], vertices[i + 1]))
.collect();
(vertices, edges)
}
/// Generates a cycle graph.
///
/// Generates a [cycle graph] *C(n)* with `n` vertices and `n` edges in `graph` as a new connected
/// component and returns the new vertices *v0, v1, ..., v(n - 1)* and edges *e0, e1, ..., e(n - 1)*
/// in order, i.e. *e0 = (v0, v1), e1 = (v1, v2), ..., e(n - 1) = (v(n - 1), v0)*.
///
/// For `n == 0`, nothing is added. For `n == 1`, a single vertex with a loop is added. For
/// `n == 2`, two vertices and two edges between them (multi-edge) are added.
///
/// [Cycle graph]: https://en.wikipedia.org/wiki/Cycle_graph
pub fn cycle<G: GraphTopologyAddition>(graph: &mut G, n: usize) -> (Vec<G::Vertex>, Vec<G::Edge>) {
graph.reserve_vertices(n);
graph.reserve_edges(n);
let vertices: Vec<_> = (0..n).map(|_| graph.add_vertex()).collect();
let edges = (0..n)
.map(|i| graph.add_edge(vertices[i], vertices[if i < n - 1 { i + 1 } else { 0 }]))
.collect();
(vertices, edges)
}
/// Generates a complete graph.
///
/// Generates a [complete graph] *K(n)* with `n` vertices and `n * (n - 1) / 2` edges in `graph` as
/// a new connected component and returns the new vertices and edges.
///
/// For `n == 0`, nothing is added. For `n == 1`, only a single vertex is added.
///
/// [Complete graph]: https://en.wikipedia.org/wiki/Complete_graph
pub fn complete<G: GraphTopologyAddition>(
graph: &mut G,
n: usize,
) -> (Vec<G::Vertex>, Vec<G::Edge>) {
let edge_capacity = n * n.saturating_sub(1) / 2;
graph.reserve_vertices(n);
graph.reserve_edges(edge_capacity);
let vertices: Vec<_> = (0..n).map(|_| graph.add_vertex()).collect();
let mut edges = Vec::with_capacity(edge_capacity);
for (i, &u) in vertices.iter().enumerate() {
for &v in &vertices[i + 1..] {
edges.push(graph.add_edge(u, v));
}
}
(vertices, edges)
}
/// Generates a Petersen graph.
///
@@ -8,22 +121,15 @@ use crate::traits::{GraphTopology, GraphTopologyAddition};
/// vertices and edges. This graph is also the Kneser graph *KG(5, 2)*.
///
/// [Petersen graph]: https://en.wikipedia.org/wiki/Petersen_graph
pub fn petersen<G>(
graph: &mut G,
) -> (
[<G as GraphTopology>::Vertex; 10],
[<G as GraphTopology>::Edge; 15],
)
where
G: GraphTopologyAddition,
{
pub fn petersen<G: GraphTopologyAddition>(graph: &mut G) -> (Vec<G::Vertex>, Vec<G::Edge>) {
const N: usize = 5;
const K: usize = 2;
graph.reserve_vertices(10);
graph.reserve_edges(15);
let vertices = core::array::from_fn(|_| graph.add_vertex());
let edges = core::array::from_fn(|j| {
let vertices: Vec<_> = (0..10).map(|_| graph.add_vertex()).collect();
let edges = (0..15)
.map(|j| {
let i = j / 3;
match j % 3 {
0 => {
@@ -36,44 +142,449 @@ where
}
_ => graph.add_edge(vertices[i], vertices[i + N]),
}
});
})
.collect();
(vertices, edges)
}
#[cfg(test)]
mod tests {
use std::fmt::Debug;
use super::*;
use crate::models::AppendGraph;
use crate::testing::fixtures::MakeTestGraph;
#[derive(Debug, Clone, Copy)]
enum Spec {
Path(usize),
Cycle(usize),
Complete(usize),
Petersen,
}
impl Spec {
fn generate<G: GraphTopologyAddition>(
&self,
graph: &mut G,
) -> (Vec<G::Vertex>, Vec<G::Edge>) {
match *self {
Spec::Path(n) => path(graph, n),
Spec::Cycle(n) => cycle(graph, n),
Spec::Complete(n) => complete(graph, n),
Spec::Petersen => petersen(graph),
}
}
fn assert<G: GraphTopology>(&self, built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
match *self {
Spec::Path(n) => assert_path(built, n),
Spec::Cycle(n) => assert_cycle(built, n),
Spec::Complete(n) => assert_complete(built, n),
Spec::Petersen => assert_petersen(built),
}
}
}
struct Built<G: GraphTopology> {
graph: G,
vertices: Vec<G::Vertex>,
edges: Vec<G::Edge>,
initial_vertex_count: usize,
initial_edge_count: usize,
}
/// Generates the test for the `append_graph` harness for the given `Spec` expression.
macro_rules! min_harness_tests {
($spec:expr) => {
#[test]
fn append_graph() {
run($spec, add_to_empty::<$crate::models::AppendGraph>);
}
};
}
/// Generates one test per harness for the given `Spec` expression.
macro_rules! all_harness_tests {
($spec:expr) => {
min_harness_tests!($spec);
#[test]
fn petersen() {
let mut graph = AppendGraph::new();
let (vertices, edges) = super::petersen(&mut graph);
assert_eq!(
graph.vertex_count(),
10,
"unexpected number of added vertices"
);
assert_eq!(graph.edge_count(), 15, "unexpected number of added edges");
assert!(
graph.vertices().all(|v| graph.degree(v) == 3),
"all vertices should have degree 3"
);
assert_handles_in_graph(&graph, &vertices, &edges);
fn append_graph_non_empty() {
run($spec, add_to_standard::<$crate::models::AppendGraph>);
}
fn assert_handles_in_graph<G: GraphTopology>(
graph: &G,
vertices: &[G::Vertex],
edges: &[G::Edge],
#[test]
fn graph() {
run($spec, add_to_empty::<$crate::models::Graph>);
}
#[test]
fn graph_non_empty() {
run($spec, add_to_standard::<$crate::models::Graph>);
}
#[test]
fn frozen_graph() {
run($spec, conv_generate_as::<$crate::models::FrozenGraph>);
}
};
}
/// Generates the module tree `<group>::<case>::<harness>` for each generator case.
///
/// The `all_harnesses` cases run on every harness, the `min_harnesses` cases only on a reduced
/// set of harnesses.
macro_rules! generator_tests {
($($group:ident {
all { $($all_case:ident => $all_generator:expr),+ $(,)? }
min { $($min_case:ident => $min_generator:expr),* $(,)? }
})+) => {
$(
mod $group {
use super::*;
$(
mod $all_case {
use super::*;
all_harness_tests!($all_generator);
}
)+
$(
mod $min_case {
use super::*;
min_harness_tests!($min_generator);
}
)*
}
)+
};
}
generator_tests! {
path {
all {
small => Spec::Path(4),
}
min {
empty => Spec::Path(0),
single_vertex => Spec::Path(1),
single_edge => Spec::Path(2),
large => Spec::Path(21),
}
}
cycle {
all {
small_even => Spec::Cycle(4),
}
min {
empty => Spec::Cycle(0),
loop_edge => Spec::Cycle(1),
multi_edge => Spec::Cycle(2),
large_odd => Spec::Cycle(21),
}
}
complete {
all {
small_even => Spec::Complete(4),
}
min {
empty => Spec::Complete(0),
single_vertex => Spec::Complete(1),
single_edge => Spec::Complete(2),
large_odd => Spec::Complete(13),
}
}
}
mod petersen {
use super::*;
all_harness_tests!(Spec::Petersen);
}
/// Builds a graph with `harness` and asserts that it matches what `spec` should produce.
fn run<G: GraphTopology>(spec: Spec, harness: impl FnOnce(&Spec) -> Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
let built = harness(&spec);
spec.assert(&built);
}
/// Runs `spec` on an empty graph of type `G`.
fn add_to_empty<G: GraphTopologyAddition>(spec: &Spec) -> Built<G> {
let mut graph = G::default();
let (vertices, edges) = spec.generate(&mut graph);
Built {
graph,
vertices,
edges,
initial_vertex_count: 0,
initial_edge_count: 0,
}
}
/// Runs `spec` on a non-empty test graph of type `G`.
fn add_to_standard<G: GraphTopologyAddition>(spec: &Spec) -> Built<G> {
let (mut graph, initial_vertices, initial_edges, _) = G::standard();
let (vertices, edges) = spec.generate(&mut graph);
Built {
graph,
vertices,
edges,
initial_vertex_count: initial_vertices.len(),
initial_edge_count: initial_edges.len(),
}
}
/// Runs `spec` within `generate_as` for type `G`.
fn conv_generate_as<G: GraphTopologyConversion>(spec: &Spec) -> Built<G> {
let (graph, vertices, edges) = generate_as::<G>(|graph| spec.generate(graph));
Built {
graph,
vertices,
edges,
initial_vertex_count: 0,
initial_edge_count: 0,
}
}
fn assert_path<G: GraphTopology>(built: &Built<G>, n: usize)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, n, n.saturating_sub(1));
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
assert_no_loops(built);
assert_no_multi_edges(built);
match n {
0 => {}
1 => assert_eq!(
built.graph.degree(built.vertices[0]),
0,
"path n == 1 should be an isolated vertex"
),
_ => {
for (i, &v) in built.vertices.iter().enumerate() {
assert_eq!(
built.graph.degree(v),
if i == 0 || i == n - 1 { 1 } else { 2 },
"unexpected degree of vertex {v:?} in path n == {n}"
);
}
}
}
assert_path_order(built);
}
fn assert_cycle<G: GraphTopology>(built: &Built<G>, n: usize)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, n, n);
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
if n != 1 {
assert_no_loops(built);
}
if n != 2 {
assert_no_multi_edges(built);
}
assert!(
built.vertices.iter().all(|&v| built.graph.degree(v) == 2),
"all new vertices should have degree 2"
);
assert_path_order(built);
// Asserts the closing edge e(n - 1) == (v(n - 1), v0).
if n > 0 {
// For n == 1, u == v and this validates the loop. For n == 2, the above asserted one of
// the two multi-edges, and this asserts the other.
let e = built.edges[n - 1];
let u = built.vertices[0];
let v = built.vertices[n - 1];
let incident_vertices = built.graph.incident_vertices(e);
assert!(
incident_vertices == (u, v) || incident_vertices == (v, u),
"edge {e:?} should have incident vertices {u:?} and {v:?}",
);
}
}
fn assert_complete<G: GraphTopology>(built: &Built<G>, n: usize)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, n, n * n.saturating_sub(1) / 2);
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
// The predicate in `all` is only executed if `vertices` has any elements, i.e. for n > 0.
assert!(
built
.vertices
.iter()
.all(|&v| built.graph.degree(v) == n - 1),
"all new vertices should have degree {}",
n - 1
);
// Asserts that all pairs of vertices have exactly one edge between them. This also includes
// checks for loops and multi-edges. If there are no duplicates, then this implies that all
// expected edges were also matched due to the validated counts from above.
let mut seen = vec![false; n * n];
let position = |v, e| {
built
.vertices
.iter()
.position(|&x| x == v)
.unwrap_or_else(|| panic!("edge {e:?} has an invalid incident vertex {v:?}"))
};
for &e in &built.edges {
let (u, v) = built.graph.incident_vertices(e);
assert_ne!(u, v, "unexpected loop edge {e:?} at vertex {u:?}");
let (i, j) = (position(u, e), position(v, e));
let (i, j) = (i.min(j), i.max(j));
assert!(
!seen[i * n + j],
"unexpected multi-edge {e:?} between vertices {u:?} and {v:?}"
);
seen[i * n + j] = true;
}
}
fn assert_petersen<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
assert_element_counts(built, 10, 15);
assert_handles(built.graph.vertices(), &built.vertices, "vertex");
assert_handles(built.graph.edges(), &built.edges, "edge");
assert_no_loops(built);
assert_no_multi_edges(built);
assert!(
built.vertices.iter().all(|&v| built.graph.degree(v) == 3),
"all new vertices should have degree 3"
);
// Checks that each pair of vertices (u, v) has no common neighbor if they are adjacent, and
// exactly one common neighbor otherwise.
for (i, &v) in built.vertices.iter().enumerate() {
for &u in &built.vertices[i + 1..] {
if built.graph.are_adjacent(v, u) {
assert!(
built.graph.common_neighbors(u, v).next().is_none(),
"adjacent vertices {u:?} and {v:?} should not have a common neighbor"
);
} else {
assert_eq!(
built.graph.common_neighbors(u, v).count(),
1,
"non-adjacent vertices {u:?} and {v:?} should have exactly one common neighbor"
);
}
}
}
}
fn assert_element_counts<G: GraphTopology>(
built: &Built<G>,
expected_vertex_count: usize,
expected_edge_count: usize,
) {
assert!(
vertices.iter().all(|&v| graph.vertices().any(|u| u == v)),
"all returned vertices should be in the graph"
assert_eq!(
built.graph.vertex_count(),
built.initial_vertex_count + expected_vertex_count,
"unexpected number of added vertices"
);
assert_eq!(
built.vertices.len(),
expected_vertex_count,
"unexpected number of returned vertices"
);
assert_eq!(
built.graph.edge_count(),
built.initial_edge_count + expected_edge_count,
"unexpected number of added edges"
);
assert_eq!(
built.edges.len(),
expected_edge_count,
"unexpected number of returned edges"
);
}
/// Asserts that every handle in `returned` is unique and in `all`.
fn assert_handles<T: PartialEq + Debug>(
all: impl Iterator<Item = T>,
returned: &[T],
kind: &str,
) {
let mut remaining: Vec<T> = all.collect();
for e in returned {
let Some(position) = remaining.iter().position(|x| x == e) else {
panic!("returned {kind} {e:?} is not in the graph or appears more than once");
};
remaining.swap_remove(position);
}
}
fn assert_no_loops<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
for &e in &built.edges {
let (u, v) = built.graph.incident_vertices(e);
assert_ne!(u, v, "unexpected loop edge {e:?} at vertex {u:?}");
}
}
fn assert_no_multi_edges<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
{
for &v in &built.vertices {
let mut neighbors = Vec::new();
let mut loop_count = 0;
for u in built.graph.adjacent_vertices(v) {
if u == v {
loop_count += 1;
assert!(loop_count < 3, "unexpected multi-edge loop at vertex {v:?}");
} else {
assert!(
edges.iter().all(|&e| graph.edges().any(|f| f == e)),
"all returned edges should be in the graph"
!neighbors.contains(&u),
"unexpected multi-edge between vertices {v:?} and {u:?}"
);
neighbors.push(u);
}
}
}
}
/// Asserts that each returned edge *ei* connects the returned vertices *vi* and *v(i + 1)*.
fn assert_path_order<G: GraphTopology>(built: &Built<G>)
where
G::Vertex: Debug,
G::Edge: Debug,
{
for (w, &e) in built.vertices.windows(2).zip(&built.edges) {
let incident_vertices = built.graph.incident_vertices(e);
assert!(
incident_vertices == (w[0], w[1]) || incident_vertices == (w[1], w[0]),
"edge {e:?} should have incident vertices {:?} and {:?}",
w[0],
w[1]
);
}
}
}
+2 -1
View File
@@ -141,9 +141,10 @@ impl<E: Copy, T: Clone> IndexMut<E> for ElementMap<E, T> {
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
use super::*;
#[test]
fn initial_values_are_default() {
let map = ElementMap::new(42, |e| e, 6);
+19 -7
View File
@@ -5,6 +5,7 @@ use crate::models::{AppendGraph, FrozenGraph};
use crate::traits::{GraphTopology, GraphTopologyAddition, GraphTopologyConversion, Incidence};
pub trait MakeTestGraph: GraphTopology + Sized {
/// Returns a versatile, pre-defined standard test graph.
// Since this is only a test fixture, the complex return type here is deemed acceptable.
#[allow(clippy::type_complexity)]
fn standard() -> (
@@ -14,20 +15,32 @@ pub trait MakeTestGraph: GraphTopology + Sized {
[Vec<Incidence<Self::Vertex, Self::Edge>>; 10],
);
/// Returns an empty graph.
fn empty() -> Self;
/// Returns a graph with a single vertex and no edges.
fn single_vertex() -> (Self, Self::Vertex);
/// Returns a graph with two vertices connected by a single edge.
fn single_edge() -> (Self, [Self::Vertex; 2], Self::Edge);
/// Returns a graph with vertices *v0*, *v1*, and *v2*, where *v0* is isolated, and the other
/// two are connected by a single edge.
fn disconnected() -> (Self, [Self::Vertex; 3]);
/// Returns a graph with a single vertex that has a single loop edge.
fn loop_edge() -> (Self, Self::Vertex, Self::Edge);
/// Returns a graph with two vertices that are connected by `K` multi-edges.
fn multiple_edges<const K: usize>() -> (Self, [Self::Vertex; 2], [Self::Edge; K]);
/// Returns a graph that is a path with three vertices and two edges between them, returned in
/// order.
fn two_edge_path() -> (Self, [Self::Vertex; 3], [Self::Edge; 2]);
/// Returns a graph that is a path with three vertices *v0*, *v1*, and *v2*, and two edges
/// *e0 = (v0, v1)* and *e1 = (v1, v2)* between them, and two loops on the first two vertices
/// *e2 = (v0, v0)* and *e3 = (v1, v1)*.
fn two_edge_path_with_loops() -> (Self, [Self::Vertex; 3], [Self::Edge; 4]);
}
@@ -268,13 +281,12 @@ where
F: Fn(V) -> Option<u32>,
{
let expected = [0, 1, 2, 2, 2, 3, 3, 3, 3, 4];
for (i, v) in vertices.iter().enumerate() {
for (i, &v) in vertices.iter().enumerate() {
assert_eq!(
actual_for(*v),
actual_for(v),
Some(expected[i]),
"unexpected distance from vertex {:?} to vertex {:?}",
vertices[0],
vertices[i]
"unexpected distance from vertex {:?} to vertex {v:?}",
vertices[0]
);
}
}
@@ -308,8 +320,8 @@ pub fn assert_standard_unweighted_predecessors_v0<V: Debug + Copy + PartialEq>(
/// Asserts that the path given as `path_option` is a valid path in `graph`
///
/// Walks the path and asserts that it is `Some`, that it is empty if and only if
/// `source == target`, that its edges are pairwise incident in the order given, and that it starts
/// at `source` and ends at `target`.
/// `source == target` for any `target` in `targets`, that its edges are pairwise incident in the
/// order given, and that it starts at `source` and ends at any `target` in `targets`.
pub fn assert_valid_path<G: GraphTopology>(
graph: &G,
path_option: Option<Vec<G::Edge>>,
+121
View File
@@ -49,6 +49,12 @@ macro_rules! graph_topology_tests {
incidence_cursor_copy,
incident_vertices_incidences_consistency,
incident_edges_incidences_consistency,
common_neighbors,
common_neighbors_same_vertex,
common_neighbors_single,
common_neighbors_disconnected,
common_neighbors_multiple_edges,
common_neighbors_loop_edge,
);
};
@@ -771,3 +777,118 @@ where
);
}
}
pub fn common_neighbors<G: MakeTestGraph>()
where
G::Vertex: Debug,
{
let (graph, vertices, _, _) = G::standard();
// Pairs of vertex indices with the indices of their expected common neighbors.
let cases: [(usize, usize, &[usize]); _] = [
(0, 3, &[1]),
(1, 6, &[2, 3]),
(5, 8, &[]),
(2, 9, &[5, 6]),
(7, 3, &[]),
(0, 1, &[]),
(6, 7, &[9]),
// Loop only on one side, adjacent.
(2, 6, &[2]),
// Loop only on one side, not adjacent.
(2, 7, &[4]),
// Adjacent through a double edge, and both have a loop.
(2, 4, &[1, 2, 4]),
];
for (i, j, expected) in cases {
let expected: Vec<_> = expected.iter().map(|&k| vertices[k]).collect();
assert_common_neighbors(&graph, vertices[i], vertices[j], &expected);
}
}
pub fn common_neighbors_same_vertex<G: MakeTestGraph>()
where
G::Vertex: Debug,
{
let (graph, vertices, _, _) = G::standard();
// Vertex 1 has a double edge to vertex 0, but no loop.
assert_common_neighbors(
&graph,
vertices[1],
vertices[1],
&[vertices[0], vertices[2], vertices[3], vertices[4]],
);
// Vertex 4 has a loop, and is therefore its own common neighbor with itself.
assert_common_neighbors(
&graph,
vertices[4],
vertices[4],
&[
vertices[1],
vertices[2],
vertices[4],
vertices[7],
vertices[8],
],
);
}
pub fn common_neighbors_single<G: MakeTestGraph>()
where
G::Vertex: Debug,
{
let (graph, v) = G::single_vertex();
assert_common_neighbors(&graph, v, v, &[]);
}
pub fn common_neighbors_disconnected<G: MakeTestGraph>()
where
G::Vertex: Debug,
{
let (graph, v) = G::disconnected();
assert_common_neighbors(&graph, v[0], v[0], &[]);
assert_common_neighbors(&graph, v[0], v[1], &[]);
}
pub fn common_neighbors_multiple_edges<G: MakeTestGraph>()
where
G::Vertex: Debug,
{
let (graph, [v0, v1], _) = G::multiple_edges::<3>();
assert_common_neighbors(&graph, v0, v1, &[]);
assert_common_neighbors(&graph, v1, v1, &[v0]);
}
pub fn common_neighbors_loop_edge<G: MakeTestGraph>()
where
G::Vertex: Debug,
{
let (graph, v, _) = G::loop_edge();
assert_common_neighbors(&graph, v, v, &[v]);
}
/// Asserts that `u` and `v` have exactly the `expected` common neighbors, each yielded once, for
/// both argument orders.
fn assert_common_neighbors<G: MakeTestGraph>(
graph: &G,
u: G::Vertex,
v: G::Vertex,
expected: &[G::Vertex],
) where
G::Vertex: Debug,
{
for (a, b) in [(u, v), (v, u)] {
let mut remaining = expected.to_vec();
for w in graph.common_neighbors(a, b) {
let pos = remaining.iter().position(|&x| x == w).unwrap_or_else(|| {
panic!(
"unexpected or repeated common neighbor {w:?} of vertices {a:?} and {b:?} from iterator"
)
});
remaining.swap_remove(pos);
}
assert!(
remaining.is_empty(),
"expected common neighbors {remaining:?} of vertices {a:?} and {b:?} were not matched by iterator"
);
}
}
+52
View File
@@ -175,6 +175,58 @@ pub trait GraphTopology {
///
/// Panics if `v` is not a valid vertex of this graph.
fn incidence_cursor(&self, v: Self::Vertex) -> Self::IncidenceCursor;
/// Returns an iterator over all vertices that are adjacent to both `u` and `v`.
///
/// Every common neighbor is yielded exactly once, in unspecified order, regardless of how
/// many parallel edges connect it to `u` or `v`. This method is symmetric up to permutations,
/// i.e. swapping the arguments will yield the same vertices, but not necessarily in the same
/// order.
///
/// If `u` has a loop and is adjacent to `v`, then `u` is a common neighbor, and will be yielded
/// exactly once, and analogously for a loop at `v`.
///
/// If `u == v`, then the common neighbors are all neighbors of `u`, including `u` itself if and
/// only if it has a loop.
///
/// All neighbors of `u` are collected when this method is called, while those of `v` are
/// traversed lazily.
///
/// # Panics
///
/// Panics if `u` or `v` is not a valid vertex of this graph.
///
/// # Examples
///
/// ```
/// use grapherity::models::AppendGraph;
/// use grapherity::prelude::*;
///
/// let mut graph = AppendGraph::new();
/// let [u, w, v] = core::array::from_fn(|_| graph.add_vertex());
/// // Parallel edges do not yield `w` more than once.
/// graph.add_edge(u, w);
/// graph.add_edge(u, w);
/// graph.add_edge(w, v);
/// assert!(graph.common_neighbors(u, v).eq([w]));
/// ```
// TODO: FrozenGraph could provide a sorted-merge override, if its vertices were sorted.
fn common_neighbors(
&self,
u: Self::Vertex,
v: Self::Vertex,
) -> impl Iterator<Item = Self::Vertex> {
// TODO: Instead of Vec, this could use a vertex ElementMap with marked neighbors if the product of the degrees of u and v is large compared to |V|.
// TODO: Instead of Vec, this could use a reusable, user-provided(?) "scratch-pad" vertex ElementMap, to not pay its allocation.
let mut u_neighbors: Vec<_> = self.adjacent_vertices(u).collect();
let mut v_neighbors = self.adjacent_vertices(v);
std::iter::from_fn(move || {
let w = v_neighbors.find(|w| u_neighbors.contains(w))?;
// Removes all parallel entries, so that `w` is not yielded again.
u_neighbors.retain(|x| *x != w);
Some(w)
})
}
}
/// A trait that adds construction operations to an undirected graph topology.