Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hyperbolic random graph model generator #1196

Merged
merged 12 commits into from
May 22, 2024
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.undirected_hyperbolic_random_graph
rustworkx.barabasi_albert_graph
rustworkx.directed_barabasi_albert_graph
rustworkx.directed_random_bipartite_graph
Expand Down
11 changes: 11 additions & 0 deletions releasenotes/notes/hyperbolic-random-graph-d85c115930d8ac08.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
features:
- |
Adds new random graph generator function, :func:`.undirected_hyperbolic_random_graph`
to generate a random graph using the H² random graph model.
- |
Adds new function to the rustworkx-core module ``rustworkx_core::generators``
``hyperbolic_random_graph()`` that generates a H^2 hyperbolic random graph.

issues:
- |
Related to issue `#150 <https://github.com/Qiskit/rustworkx/issues/150>`.
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
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;
278 changes: 276 additions & 2 deletions rustworkx-core/src/generators/random_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

#![allow(clippy::float_cmp)]

use std::f64::consts::PI;
use std::hash::Hash;

use petgraph::data::{Build, Create};
Expand Down Expand Up @@ -619,14 +620,141 @@ where
Ok(graph)
}

/// Generate a hyperbolic random undirected graph.
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
///
/// The H² hyperbolic random graph model connects pairs of nodes with a probability
/// that decreases as their hyperbolic distance increases.
///
/// The number of nodes is inferred from the coordinates `radii` and `angles`. `radii`
/// and `angles` must have the same size and cannot be empty. If `beta` is `None`,
/// all pairs of nodes with a distance smaller than ``r`` are connected.
///
/// D. Krioukov et al. "Hyperbolic geometry of complex networks", Phys. Rev. E 82, pp 036106, 2010.
///
/// Arguments:
///
/// * `radii` - radial coordinates (nonnegative) of the nodes.
/// * `angles` - angular coordinates (between -pi and pi) of the nodes.
/// * `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![0.4, 2., 3.],
/// &vec![0., 0., 0.],
/// None,
/// 2.,
/// None,
/// || {()},
/// || {()},
/// ).unwrap();
/// assert_eq!(g.node_count(), 3);
/// assert_eq!(g.edge_count(), 2);
/// ```
pub fn hyperbolic_random_graph<G, T, F, H, M>(
radii: &[f64],
angles: &[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 = radii.len();
if num_nodes == 0 || radii.len() != angles.len() {
return Err(InvalidInputError {});
}
if radii.iter().any(|x| x.is_infinite() || x.is_nan()) {
return Err(InvalidInputError {});
}
if angles.iter().any(|x| x.is_infinite() || x.is_nan()) {
return Err(InvalidInputError {});
}
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
if radii.iter().fold(0_f64, |a, &b| a.min(b)) < 0. {
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
return Err(InvalidInputError {});
}
if angles.iter().map(|x| x.abs()).fold(0_f64, |a, b| a.max(b)) > PI {
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
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, (r1, theta1)) in radii
.iter()
.zip(angles.iter())
.enumerate()
.take(num_nodes - 1)
{
for (w, (r2, theta2)) in radii.iter().zip(angles.iter()).enumerate().skip(v + 1) {
let dist = hyperbolic_distance(r1, theta1, r2, theta2);
let is_edge = match beta {
Some(b) => {
let prob = 1. / ((b / 2. * (dist - r)).exp() + 1.);
let u: f64 = between.sample(&mut rng);
u < prob
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
}
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(r1: &f64, theta1: &f64, r2: &f64, theta2: &f64) -> f64 {
(r1.cosh() * r2.cosh() - r1.sinh() * r2.sinh() * (theta1 - theta2).cos()).acosh()
SILIZ4 marked this conversation as resolved.
Show resolved Hide resolved
}

#[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 std::f64::consts::PI;

// Test gnp_random_graph

Expand Down Expand Up @@ -916,4 +1044,150 @@ mod tests {
Err(e) => assert_eq!(e, InvalidInputError),
};
}

// Test hyperbolic_random_graph

#[test]
fn test_hyperbolic_random_graph_seeded() {
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&vec![3., 0.5, 0.5, 0.],
&vec![0., PI, 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_empty() {
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&vec![3., 0.5, 1.],
&vec![0., PI, 0.],
None,
1.,
None,
|| (),
|| (),
)
.unwrap();
assert_eq!(g.node_count(), 3);
assert_eq!(g.edge_count(), 0);
}

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

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

#[test]
fn test_hyperbolic_random_graph_len_coord_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&vec![1.],
&vec![1., 2.],
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<(), ()>, _, _, _, _>(
&vec![],
&vec![],
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![0.],
&vec![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 undirected_hyperbolic_random_graph as undirected_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
8 changes: 8 additions & 0 deletions rustworkx/rustworkx.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,14 @@ def random_geometric_graph(
p: float = ...,
seed: int | None = ...,
) -> PyGraph: ...
def undirected_hyperbolic_random_graph(
radii: list[float],
angles: 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!(undirected_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