Skip to content

Commit

Permalink
Add hyperbolic random graph model generator (#1196)
Browse files Browse the repository at this point in the history
* add hyperbolic random graph model generator

* Loosen trait constraints and simplify structure for longest_path (#1195)

In the recently merged #1192 a new generic DAG longest_path function was
added to rustworkx-core. However, the trait bounds on the function were
a bit tighter than they needed to be. The traits were forcing NodeId to
be of a NodeIndex type and this wasn't really required. The only
requirement that the NodeId type can be put on a hashmap and do a
partial compare (that implements Hash, Eq, and PartialOrd). Also the
IntoNeighborsDirected wasn't required because it's methods weren't ever
used. This commit loosens the traits bounds to facilitate this. At the
same time this also simplifies the code structure a bit to reduce the
separation of the rust code structure in the rustworkx crate using
longest_path().

* use vector references

* change to slice (clippy)

* generalize to H^D, improve numerical accuracy

* allow infinite coordinate

* handle infinity in hyperbolic distance

* remove unused import (clippy)

* fix python stub

---------

Co-authored-by: Matthew Treinish <[email protected]>
Co-authored-by: Ivan Carvalho <[email protected]>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored May 22, 2024
1 parent 3e51301 commit c039731
Show file tree
Hide file tree
Showing 9 changed files with 429 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/source/api/random_graph_generator_functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Random Graph Generator Functions
rustworkx.directed_gnm_random_graph
rustworkx.undirected_gnm_random_graph
rustworkx.random_geometric_graph
rustworkx.hyperbolic_random_graph
rustworkx.barabasi_albert_graph
rustworkx.directed_barabasi_albert_graph
rustworkx.directed_random_bipartite_graph
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
features:
- |
Adds new random graph generator function, :func:`.hyperbolic_random_graph`
to sample the hyperbolic random graph model.
- |
Adds new function to the rustworkx-core module ``rustworkx_core::generators``
``hyperbolic_random_graph()`` that samples the hyperbolic random graph model.
1 change: 1 addition & 0 deletions rustworkx-core/src/generators/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pub use petersen_graph::petersen_graph;
pub use random_graph::barabasi_albert_graph;
pub use random_graph::gnm_random_graph;
pub use random_graph::gnp_random_graph;
pub use random_graph::hyperbolic_random_graph;
pub use random_graph::random_bipartite_graph;
pub use random_graph::random_geometric_graph;
pub use star_graph::star_graph;
301 changes: 299 additions & 2 deletions rustworkx-core/src/generators/random_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,15 +619,142 @@ where
Ok(graph)
}

/// Generate a hyperbolic random undirected graph (also called hyperbolic geometric graph).
///
/// The hyperbolic random graph model connects pairs of nodes with a probability
/// that decreases as their hyperbolic distance increases.
///
/// The number of nodes and the dimension are inferred from the coordinates `pos` of the
/// hyperboloid model (at least 3-dimensional). If `beta` is `None`, all pairs of nodes
/// with a distance smaller than ``r`` are connected.
///
/// Arguments:
///
/// * `pos` - Hyperboloid model coordinates of the nodes `[p_1, p_2, ...]` where `p_i` is the
/// position of node i. The first dimension corresponds to the negative term in the metric
/// and so for each node i, `p_i[0]` must be at least 1.
/// * `beta` - Sigmoid sharpness (nonnegative) of the connection probability.
/// * `r` - Distance at which the connection probability is 0.5 for the probabilistic model.
/// Threshold when `beta` is `None`.
/// * `seed` - An optional seed to use for the random number generator.
/// * `default_node_weight` - A callable that will return the weight to use
/// for newly created nodes.
/// * `default_edge_weight` - A callable that will return the weight object
/// to use for newly created edges.
///
/// # Example
/// ```rust
/// use rustworkx_core::petgraph;
/// use rustworkx_core::generators::hyperbolic_random_graph;
///
/// let g: petgraph::graph::UnGraph<(), ()> = hyperbolic_random_graph(
/// &[vec![1_f64.cosh(), 3_f64.sinh(), 0.],
/// vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
/// vec![1_f64.cosh(), -1_f64.sinh(), 0.]],
/// None,
/// 2.,
/// None,
/// || {()},
/// || {()},
/// ).unwrap();
/// assert_eq!(g.node_count(), 3);
/// assert_eq!(g.edge_count(), 1);
/// ```
pub fn hyperbolic_random_graph<G, T, F, H, M>(
pos: &[Vec<f64>],
beta: Option<f64>,
r: f64,
seed: Option<u64>,
mut default_node_weight: F,
mut default_edge_weight: H,
) -> Result<G, InvalidInputError>
where
G: Build + Create + Data<NodeWeight = T, EdgeWeight = M> + NodeIndexable + GraphProp,
F: FnMut() -> T,
H: FnMut() -> M,
G::NodeId: Eq + Hash,
{
let num_nodes = pos.len();
if num_nodes == 0 {
return Err(InvalidInputError {});
}
if pos.iter().any(|xs| xs.iter().any(|x| x.is_nan())) {
return Err(InvalidInputError {});
}
let dim = pos[0].len();
if dim < 3 || pos.iter().any(|x| x.len() != dim || x[0] < 1.) {
return Err(InvalidInputError {});
}
if beta.is_some_and(|b| b < 0. || b.is_nan()) {
return Err(InvalidInputError {});
}
if r < 0. || r.is_nan() {
return Err(InvalidInputError {});
}

let mut rng: Pcg64 = match seed {
Some(seed) => Pcg64::seed_from_u64(seed),
None => Pcg64::from_entropy(),
};
let mut graph = G::with_capacity(num_nodes, num_nodes);
if graph.is_directed() {
return Err(InvalidInputError {});
}

for _ in 0..num_nodes {
graph.add_node(default_node_weight());
}

let between = Uniform::new(0.0, 1.0);
for (v, p1) in pos.iter().enumerate().take(num_nodes - 1) {
for (w, p2) in pos.iter().enumerate().skip(v + 1) {
let dist = hyperbolic_distance(p1, p2);
let is_edge = match beta {
Some(b) => {
let prob_inverse = (b / 2. * (dist - r)).exp() + 1.;
let u: f64 = between.sample(&mut rng);
prob_inverse * u < 1.
}
None => dist < r,
};
if is_edge {
graph.add_edge(
graph.from_index(v),
graph.from_index(w),
default_edge_weight(),
);
}
}
}
Ok(graph)
}

#[inline]
fn hyperbolic_distance(p1: &[f64], p2: &[f64]) -> f64 {
if p1.iter().chain(p2.iter()).any(|x| x.is_infinite()) {
f64::INFINITY
} else {
(p1[0] * p2[0]
- p1.iter()
.skip(1)
.zip(p2.iter().skip(1))
.map(|(&x, &y)| x * y)
.sum::<f64>())
.acosh()
}
}

#[cfg(test)]
mod tests {
use crate::generators::InvalidInputError;
use crate::generators::{
barabasi_albert_graph, gnm_random_graph, gnp_random_graph, path_graph,
random_bipartite_graph, random_geometric_graph,
barabasi_albert_graph, gnm_random_graph, gnp_random_graph, hyperbolic_random_graph,
path_graph, random_bipartite_graph, random_geometric_graph,
};
use crate::petgraph;

use super::hyperbolic_distance;

// Test gnp_random_graph

#[test]
Expand Down Expand Up @@ -916,4 +1043,174 @@ mod tests {
Err(e) => assert_eq!(e, InvalidInputError),
};
}

// Test hyperbolic_random_graph
//
// Hyperboloid (H^2) "polar" coordinates (r, theta) are transformed to "cartesian"
// coordinates using
// z = cosh(r)
// x = sinh(r)cos(theta)
// y = sinh(r)sin(theta)

#[test]
fn test_hyperbolic_dist() {
assert_eq!(
hyperbolic_distance(
&[3_f64.cosh(), 3_f64.sinh(), 0.],
&[0.5_f64.cosh(), -0.5_f64.sinh(), 0.]
),
3.5
);
}
#[test]
fn test_hyperbolic_dist_inf() {
assert_eq!(
hyperbolic_distance(&[f64::INFINITY, f64::INFINITY, 0.], &[1., 0., 0.]),
f64::INFINITY
);
}

#[test]
fn test_hyperbolic_random_graph_seeded() {
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[
vec![3_f64.cosh(), 3_f64.sinh(), 0.],
vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
vec![0.5_f64.cosh(), 0.5_f64.sinh(), 0.],
vec![1., 0., 0.],
],
Some(10000.),
0.75,
Some(10),
|| (),
|| (),
)
.unwrap();
assert_eq!(g.node_count(), 4);
assert_eq!(g.edge_count(), 2);
}

#[test]
fn test_hyperbolic_random_graph_threshold() {
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[
vec![1_f64.cosh(), 3_f64.sinh(), 0.],
vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
vec![1_f64.cosh(), -1_f64.sinh(), 0.],
],
None,
1.,
None,
|| (),
|| (),
)
.unwrap();
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 1);
}

#[test]
fn test_hyperbolic_random_graph_invalid_dim_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[vec![1., 0.]],
None,
1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}

#[test]
fn test_hyperbolic_random_graph_invalid_first_coord_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[vec![0., 0., 0.]],
None,
1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}

#[test]
fn test_hyperbolic_random_graph_neg_r_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[vec![1., 0., 0.], vec![1., 0., 0.]],
None,
-1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}

#[test]
fn test_hyperbolic_random_graph_neg_beta_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[vec![1., 0., 0.], vec![1., 0., 0.]],
Some(-1.),
1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}

#[test]
fn test_hyperbolic_random_graph_diff_dims_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[vec![1., 0., 0.], vec![1., 0., 0., 0.]],
None,
1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}

#[test]
fn test_hyperbolic_random_graph_empty_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[],
None,
1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}

#[test]
fn test_hyperbolic_random_graph_directed_error() {
match hyperbolic_random_graph::<petgraph::graph::DiGraph<(), ()>, _, _, _, _>(
&[vec![1., 0., 0.], vec![1., 0., 0.]],
None,
1.,
None,
|| (),
|| (),
) {
Ok(_) => panic!("Returned a non-error"),
Err(e) => assert_eq!(e, InvalidInputError),
}
}
}
1 change: 1 addition & 0 deletions rustworkx/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ from .rustworkx import undirected_gnm_random_graph as undirected_gnm_random_grap
from .rustworkx import directed_gnp_random_graph as directed_gnp_random_graph
from .rustworkx import undirected_gnp_random_graph as undirected_gnp_random_graph
from .rustworkx import random_geometric_graph as random_geometric_graph
from .rustworkx import hyperbolic_random_graph as hyperbolic_random_graph
from .rustworkx import barabasi_albert_graph as barabasi_albert_graph
from .rustworkx import directed_barabasi_albert_graph as directed_barabasi_albert_graph
from .rustworkx import undirected_random_bipartite_graph as undirected_random_bipartite_graph
Expand Down
7 changes: 7 additions & 0 deletions rustworkx/rustworkx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,13 @@ def random_geometric_graph(
p: float = ...,
seed: int | None = ...,
) -> PyGraph: ...
def hyperbolic_random_graph(
pos: list[list[float]],
r: float,
beta: float | None,
/,
seed: int | None = ...,
) -> PyGraph: ...
def barabasi_albert_graph(
n: int,
m: int,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
m.add_wrapped(wrap_pyfunction!(directed_gnm_random_graph))?;
m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?;
m.add_wrapped(wrap_pyfunction!(random_geometric_graph))?;
m.add_wrapped(wrap_pyfunction!(hyperbolic_random_graph))?;
m.add_wrapped(wrap_pyfunction!(barabasi_albert_graph))?;
m.add_wrapped(wrap_pyfunction!(directed_barabasi_albert_graph))?;
m.add_wrapped(wrap_pyfunction!(directed_random_bipartite_graph))?;
Expand Down
Loading

0 comments on commit c039731

Please sign in to comment.