Skip to content

Commit c039731

Browse files
SILIZ4mtreinishIvanIsCodingmergify[bot]
authored
Add hyperbolic random graph model generator (#1196)
* 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>
1 parent 3e51301 commit c039731

File tree

9 files changed

+429
-2
lines changed

9 files changed

+429
-2
lines changed

docs/source/api/random_graph_generator_functions.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Random Graph Generator Functions
1111
rustworkx.directed_gnm_random_graph
1212
rustworkx.undirected_gnm_random_graph
1313
rustworkx.random_geometric_graph
14+
rustworkx.hyperbolic_random_graph
1415
rustworkx.barabasi_albert_graph
1516
rustworkx.directed_barabasi_albert_graph
1617
rustworkx.directed_random_bipartite_graph
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
features:
2+
- |
3+
Adds new random graph generator function, :func:`.hyperbolic_random_graph`
4+
to sample the hyperbolic random graph model.
5+
- |
6+
Adds new function to the rustworkx-core module ``rustworkx_core::generators``
7+
``hyperbolic_random_graph()`` that samples the hyperbolic random graph model.

rustworkx-core/src/generators/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pub use petersen_graph::petersen_graph;
5959
pub use random_graph::barabasi_albert_graph;
6060
pub use random_graph::gnm_random_graph;
6161
pub use random_graph::gnp_random_graph;
62+
pub use random_graph::hyperbolic_random_graph;
6263
pub use random_graph::random_bipartite_graph;
6364
pub use random_graph::random_geometric_graph;
6465
pub use star_graph::star_graph;

rustworkx-core/src/generators/random_graph.rs

Lines changed: 299 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -619,15 +619,142 @@ where
619619
Ok(graph)
620620
}
621621

622+
/// Generate a hyperbolic random undirected graph (also called hyperbolic geometric graph).
623+
///
624+
/// The hyperbolic random graph model connects pairs of nodes with a probability
625+
/// that decreases as their hyperbolic distance increases.
626+
///
627+
/// The number of nodes and the dimension are inferred from the coordinates `pos` of the
628+
/// hyperboloid model (at least 3-dimensional). If `beta` is `None`, all pairs of nodes
629+
/// with a distance smaller than ``r`` are connected.
630+
///
631+
/// Arguments:
632+
///
633+
/// * `pos` - Hyperboloid model coordinates of the nodes `[p_1, p_2, ...]` where `p_i` is the
634+
/// position of node i. The first dimension corresponds to the negative term in the metric
635+
/// and so for each node i, `p_i[0]` must be at least 1.
636+
/// * `beta` - Sigmoid sharpness (nonnegative) of the connection probability.
637+
/// * `r` - Distance at which the connection probability is 0.5 for the probabilistic model.
638+
/// Threshold when `beta` is `None`.
639+
/// * `seed` - An optional seed to use for the random number generator.
640+
/// * `default_node_weight` - A callable that will return the weight to use
641+
/// for newly created nodes.
642+
/// * `default_edge_weight` - A callable that will return the weight object
643+
/// to use for newly created edges.
644+
///
645+
/// # Example
646+
/// ```rust
647+
/// use rustworkx_core::petgraph;
648+
/// use rustworkx_core::generators::hyperbolic_random_graph;
649+
///
650+
/// let g: petgraph::graph::UnGraph<(), ()> = hyperbolic_random_graph(
651+
/// &[vec![1_f64.cosh(), 3_f64.sinh(), 0.],
652+
/// vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
653+
/// vec![1_f64.cosh(), -1_f64.sinh(), 0.]],
654+
/// None,
655+
/// 2.,
656+
/// None,
657+
/// || {()},
658+
/// || {()},
659+
/// ).unwrap();
660+
/// assert_eq!(g.node_count(), 3);
661+
/// assert_eq!(g.edge_count(), 1);
662+
/// ```
663+
pub fn hyperbolic_random_graph<G, T, F, H, M>(
664+
pos: &[Vec<f64>],
665+
beta: Option<f64>,
666+
r: f64,
667+
seed: Option<u64>,
668+
mut default_node_weight: F,
669+
mut default_edge_weight: H,
670+
) -> Result<G, InvalidInputError>
671+
where
672+
G: Build + Create + Data<NodeWeight = T, EdgeWeight = M> + NodeIndexable + GraphProp,
673+
F: FnMut() -> T,
674+
H: FnMut() -> M,
675+
G::NodeId: Eq + Hash,
676+
{
677+
let num_nodes = pos.len();
678+
if num_nodes == 0 {
679+
return Err(InvalidInputError {});
680+
}
681+
if pos.iter().any(|xs| xs.iter().any(|x| x.is_nan())) {
682+
return Err(InvalidInputError {});
683+
}
684+
let dim = pos[0].len();
685+
if dim < 3 || pos.iter().any(|x| x.len() != dim || x[0] < 1.) {
686+
return Err(InvalidInputError {});
687+
}
688+
if beta.is_some_and(|b| b < 0. || b.is_nan()) {
689+
return Err(InvalidInputError {});
690+
}
691+
if r < 0. || r.is_nan() {
692+
return Err(InvalidInputError {});
693+
}
694+
695+
let mut rng: Pcg64 = match seed {
696+
Some(seed) => Pcg64::seed_from_u64(seed),
697+
None => Pcg64::from_entropy(),
698+
};
699+
let mut graph = G::with_capacity(num_nodes, num_nodes);
700+
if graph.is_directed() {
701+
return Err(InvalidInputError {});
702+
}
703+
704+
for _ in 0..num_nodes {
705+
graph.add_node(default_node_weight());
706+
}
707+
708+
let between = Uniform::new(0.0, 1.0);
709+
for (v, p1) in pos.iter().enumerate().take(num_nodes - 1) {
710+
for (w, p2) in pos.iter().enumerate().skip(v + 1) {
711+
let dist = hyperbolic_distance(p1, p2);
712+
let is_edge = match beta {
713+
Some(b) => {
714+
let prob_inverse = (b / 2. * (dist - r)).exp() + 1.;
715+
let u: f64 = between.sample(&mut rng);
716+
prob_inverse * u < 1.
717+
}
718+
None => dist < r,
719+
};
720+
if is_edge {
721+
graph.add_edge(
722+
graph.from_index(v),
723+
graph.from_index(w),
724+
default_edge_weight(),
725+
);
726+
}
727+
}
728+
}
729+
Ok(graph)
730+
}
731+
732+
#[inline]
733+
fn hyperbolic_distance(p1: &[f64], p2: &[f64]) -> f64 {
734+
if p1.iter().chain(p2.iter()).any(|x| x.is_infinite()) {
735+
f64::INFINITY
736+
} else {
737+
(p1[0] * p2[0]
738+
- p1.iter()
739+
.skip(1)
740+
.zip(p2.iter().skip(1))
741+
.map(|(&x, &y)| x * y)
742+
.sum::<f64>())
743+
.acosh()
744+
}
745+
}
746+
622747
#[cfg(test)]
623748
mod tests {
624749
use crate::generators::InvalidInputError;
625750
use crate::generators::{
626-
barabasi_albert_graph, gnm_random_graph, gnp_random_graph, path_graph,
627-
random_bipartite_graph, random_geometric_graph,
751+
barabasi_albert_graph, gnm_random_graph, gnp_random_graph, hyperbolic_random_graph,
752+
path_graph, random_bipartite_graph, random_geometric_graph,
628753
};
629754
use crate::petgraph;
630755

756+
use super::hyperbolic_distance;
757+
631758
// Test gnp_random_graph
632759

633760
#[test]
@@ -916,4 +1043,174 @@ mod tests {
9161043
Err(e) => assert_eq!(e, InvalidInputError),
9171044
};
9181045
}
1046+
1047+
// Test hyperbolic_random_graph
1048+
//
1049+
// Hyperboloid (H^2) "polar" coordinates (r, theta) are transformed to "cartesian"
1050+
// coordinates using
1051+
// z = cosh(r)
1052+
// x = sinh(r)cos(theta)
1053+
// y = sinh(r)sin(theta)
1054+
1055+
#[test]
1056+
fn test_hyperbolic_dist() {
1057+
assert_eq!(
1058+
hyperbolic_distance(
1059+
&[3_f64.cosh(), 3_f64.sinh(), 0.],
1060+
&[0.5_f64.cosh(), -0.5_f64.sinh(), 0.]
1061+
),
1062+
3.5
1063+
);
1064+
}
1065+
#[test]
1066+
fn test_hyperbolic_dist_inf() {
1067+
assert_eq!(
1068+
hyperbolic_distance(&[f64::INFINITY, f64::INFINITY, 0.], &[1., 0., 0.]),
1069+
f64::INFINITY
1070+
);
1071+
}
1072+
1073+
#[test]
1074+
fn test_hyperbolic_random_graph_seeded() {
1075+
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1076+
&[
1077+
vec![3_f64.cosh(), 3_f64.sinh(), 0.],
1078+
vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
1079+
vec![0.5_f64.cosh(), 0.5_f64.sinh(), 0.],
1080+
vec![1., 0., 0.],
1081+
],
1082+
Some(10000.),
1083+
0.75,
1084+
Some(10),
1085+
|| (),
1086+
|| (),
1087+
)
1088+
.unwrap();
1089+
assert_eq!(g.node_count(), 4);
1090+
assert_eq!(g.edge_count(), 2);
1091+
}
1092+
1093+
#[test]
1094+
fn test_hyperbolic_random_graph_threshold() {
1095+
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1096+
&[
1097+
vec![1_f64.cosh(), 3_f64.sinh(), 0.],
1098+
vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
1099+
vec![1_f64.cosh(), -1_f64.sinh(), 0.],
1100+
],
1101+
None,
1102+
1.,
1103+
None,
1104+
|| (),
1105+
|| (),
1106+
)
1107+
.unwrap();
1108+
assert_eq!(g.node_count(), 3);
1109+
assert_eq!(g.edge_count(), 1);
1110+
}
1111+
1112+
#[test]
1113+
fn test_hyperbolic_random_graph_invalid_dim_error() {
1114+
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1115+
&[vec![1., 0.]],
1116+
None,
1117+
1.,
1118+
None,
1119+
|| (),
1120+
|| (),
1121+
) {
1122+
Ok(_) => panic!("Returned a non-error"),
1123+
Err(e) => assert_eq!(e, InvalidInputError),
1124+
}
1125+
}
1126+
1127+
#[test]
1128+
fn test_hyperbolic_random_graph_invalid_first_coord_error() {
1129+
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1130+
&[vec![0., 0., 0.]],
1131+
None,
1132+
1.,
1133+
None,
1134+
|| (),
1135+
|| (),
1136+
) {
1137+
Ok(_) => panic!("Returned a non-error"),
1138+
Err(e) => assert_eq!(e, InvalidInputError),
1139+
}
1140+
}
1141+
1142+
#[test]
1143+
fn test_hyperbolic_random_graph_neg_r_error() {
1144+
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1145+
&[vec![1., 0., 0.], vec![1., 0., 0.]],
1146+
None,
1147+
-1.,
1148+
None,
1149+
|| (),
1150+
|| (),
1151+
) {
1152+
Ok(_) => panic!("Returned a non-error"),
1153+
Err(e) => assert_eq!(e, InvalidInputError),
1154+
}
1155+
}
1156+
1157+
#[test]
1158+
fn test_hyperbolic_random_graph_neg_beta_error() {
1159+
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1160+
&[vec![1., 0., 0.], vec![1., 0., 0.]],
1161+
Some(-1.),
1162+
1.,
1163+
None,
1164+
|| (),
1165+
|| (),
1166+
) {
1167+
Ok(_) => panic!("Returned a non-error"),
1168+
Err(e) => assert_eq!(e, InvalidInputError),
1169+
}
1170+
}
1171+
1172+
#[test]
1173+
fn test_hyperbolic_random_graph_diff_dims_error() {
1174+
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1175+
&[vec![1., 0., 0.], vec![1., 0., 0., 0.]],
1176+
None,
1177+
1.,
1178+
None,
1179+
|| (),
1180+
|| (),
1181+
) {
1182+
Ok(_) => panic!("Returned a non-error"),
1183+
Err(e) => assert_eq!(e, InvalidInputError),
1184+
}
1185+
}
1186+
1187+
#[test]
1188+
fn test_hyperbolic_random_graph_empty_error() {
1189+
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1190+
&[],
1191+
None,
1192+
1.,
1193+
None,
1194+
|| (),
1195+
|| (),
1196+
) {
1197+
Ok(_) => panic!("Returned a non-error"),
1198+
Err(e) => assert_eq!(e, InvalidInputError),
1199+
}
1200+
}
1201+
1202+
#[test]
1203+
fn test_hyperbolic_random_graph_directed_error() {
1204+
match hyperbolic_random_graph::<petgraph::graph::DiGraph<(), ()>, _, _, _, _>(
1205+
&[vec![1., 0., 0.], vec![1., 0., 0.]],
1206+
None,
1207+
1.,
1208+
None,
1209+
|| (),
1210+
|| (),
1211+
) {
1212+
Ok(_) => panic!("Returned a non-error"),
1213+
Err(e) => assert_eq!(e, InvalidInputError),
1214+
}
1215+
}
9191216
}

rustworkx/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ from .rustworkx import undirected_gnm_random_graph as undirected_gnm_random_grap
128128
from .rustworkx import directed_gnp_random_graph as directed_gnp_random_graph
129129
from .rustworkx import undirected_gnp_random_graph as undirected_gnp_random_graph
130130
from .rustworkx import random_geometric_graph as random_geometric_graph
131+
from .rustworkx import hyperbolic_random_graph as hyperbolic_random_graph
131132
from .rustworkx import barabasi_albert_graph as barabasi_albert_graph
132133
from .rustworkx import directed_barabasi_albert_graph as directed_barabasi_albert_graph
133134
from .rustworkx import undirected_random_bipartite_graph as undirected_random_bipartite_graph

rustworkx/rustworkx.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,13 @@ def random_geometric_graph(
558558
p: float = ...,
559559
seed: int | None = ...,
560560
) -> PyGraph: ...
561+
def hyperbolic_random_graph(
562+
pos: list[list[float]],
563+
r: float,
564+
beta: float | None,
565+
/,
566+
seed: int | None = ...,
567+
) -> PyGraph: ...
561568
def barabasi_albert_graph(
562569
n: int,
563570
m: int,

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,7 @@ fn rustworkx(py: Python<'_>, m: &Bound<PyModule>) -> PyResult<()> {
477477
m.add_wrapped(wrap_pyfunction!(directed_gnm_random_graph))?;
478478
m.add_wrapped(wrap_pyfunction!(undirected_gnm_random_graph))?;
479479
m.add_wrapped(wrap_pyfunction!(random_geometric_graph))?;
480+
m.add_wrapped(wrap_pyfunction!(hyperbolic_random_graph))?;
480481
m.add_wrapped(wrap_pyfunction!(barabasi_albert_graph))?;
481482
m.add_wrapped(wrap_pyfunction!(directed_barabasi_albert_graph))?;
482483
m.add_wrapped(wrap_pyfunction!(directed_random_bipartite_graph))?;

0 commit comments

Comments
 (0)