Skip to content

Commit 2ad86a4

Browse files
SILIZ4mtreinishIvanIsCodingmergify[bot]
authored
Hyperbolic generator improvements + fix (#1212)
* 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 * Rename deprecated cargo config file (#1211) This commit migrates the .cargo/config file which has been deprecated to the new path .cargo/config.toml. This will fix warnings that are emitted when compiling with the latest stable release. This new path has been supported since Rust 1.38 which is much older than our current MSRV of 1.70. * fix hyperbolic distance, swap r and beta, infer time coordinate * use mul_add in hyperbolic distance, remove release note * replace clone with dereference --------- 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 5354512 commit 2ad86a4

File tree

3 files changed

+66
-78
lines changed

3 files changed

+66
-78
lines changed

rustworkx-core/src/generators/random_graph.rs

Lines changed: 52 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -751,14 +751,14 @@ where
751751
/// that decreases as their hyperbolic distance increases.
752752
///
753753
/// The number of nodes and the dimension are inferred from the coordinates `pos` of the
754-
/// hyperboloid model (at least 3-dimensional). If `beta` is `None`, all pairs of nodes
755-
/// with a distance smaller than ``r`` are connected.
754+
/// hyperboloid model. The "time" coordinates are inferred from the others, meaning that
755+
/// at least 2 coordinates must be provided per node. If `beta` is `None`, all pairs of
756+
/// nodes with a distance smaller than ``r`` are connected.
756757
///
757758
/// Arguments:
758759
///
759760
/// * `pos` - Hyperboloid model coordinates of the nodes `[p_1, p_2, ...]` where `p_i` is the
760-
/// position of node i. The first dimension corresponds to the negative term in the metric
761-
/// and so for each node i, `p_i[0]` must be at least 1.
761+
/// position of node i. The "time" coordinates are inferred.
762762
/// * `beta` - Sigmoid sharpness (nonnegative) of the connection probability.
763763
/// * `r` - Distance at which the connection probability is 0.5 for the probabilistic model.
764764
/// Threshold when `beta` is `None`.
@@ -774,12 +774,12 @@ where
774774
/// use rustworkx_core::generators::hyperbolic_random_graph;
775775
///
776776
/// let g: petgraph::graph::UnGraph<(), ()> = hyperbolic_random_graph(
777-
/// &[vec![1_f64.cosh(), 3_f64.sinh(), 0.],
778-
/// vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
779-
/// vec![1_f64.cosh(), -1_f64.sinh(), 0.]],
780-
/// None,
777+
/// &[vec![3_f64.sinh(), 0.],
778+
/// vec![-0.5_f64.sinh(), 0.],
779+
/// vec![-1_f64.sinh(), 0.]],
781780
/// 2.,
782781
/// None,
782+
/// None,
783783
/// || {()},
784784
/// || {()},
785785
/// ).unwrap();
@@ -788,8 +788,8 @@ where
788788
/// ```
789789
pub fn hyperbolic_random_graph<G, T, F, H, M>(
790790
pos: &[Vec<f64>],
791-
beta: Option<f64>,
792791
r: f64,
792+
beta: Option<f64>,
793793
seed: Option<u64>,
794794
mut default_node_weight: F,
795795
mut default_edge_weight: H,
@@ -804,11 +804,14 @@ where
804804
if num_nodes == 0 {
805805
return Err(InvalidInputError {});
806806
}
807-
if pos.iter().any(|xs| xs.iter().any(|x| x.is_nan())) {
807+
if pos
808+
.iter()
809+
.any(|xs| xs.iter().any(|x| x.is_nan() || x.is_infinite()))
810+
{
808811
return Err(InvalidInputError {});
809812
}
810813
let dim = pos[0].len();
811-
if dim < 3 || pos.iter().any(|x| x.len() != dim || x[0] < 1.) {
814+
if dim < 2 || pos.iter().any(|x| x.len() != dim) {
812815
return Err(InvalidInputError {});
813816
}
814817
if beta.is_some_and(|b| b < 0. || b.is_nan()) {
@@ -856,17 +859,23 @@ where
856859
}
857860

858861
#[inline]
859-
fn hyperbolic_distance(p1: &[f64], p2: &[f64]) -> f64 {
860-
if p1.iter().chain(p2.iter()).any(|x| x.is_infinite()) {
861-
f64::INFINITY
862+
fn hyperbolic_distance(x: &[f64], y: &[f64]) -> f64 {
863+
let mut sum_squared_x = 0.;
864+
let mut sum_squared_y = 0.;
865+
let mut inner_product = 0.;
866+
for (x_i, y_i) in x.iter().zip(y.iter()) {
867+
if x_i.is_infinite() || y_i.is_infinite() || x_i.is_nan() || y_i.is_nan() {
868+
return f64::NAN;
869+
}
870+
sum_squared_x = x_i.mul_add(*x_i, sum_squared_x);
871+
sum_squared_y = y_i.mul_add(*y_i, sum_squared_y);
872+
inner_product = x_i.mul_add(*y_i, inner_product);
873+
}
874+
let arg = (1. + sum_squared_x).sqrt() * (1. + sum_squared_y).sqrt() - inner_product;
875+
if arg < 1. {
876+
0.
862877
} else {
863-
(p1[0] * p2[0]
864-
- p1.iter()
865-
.skip(1)
866-
.zip(p2.iter().skip(1))
867-
.map(|(&x, &y)| x * y)
868-
.sum::<f64>())
869-
.acosh()
878+
arg.acosh()
870879
}
871880
}
872881

@@ -1340,32 +1349,29 @@ mod tests {
13401349
#[test]
13411350
fn test_hyperbolic_dist() {
13421351
assert_eq!(
1343-
hyperbolic_distance(
1344-
&[3_f64.cosh(), 3_f64.sinh(), 0.],
1345-
&[0.5_f64.cosh(), -0.5_f64.sinh(), 0.]
1346-
),
1352+
hyperbolic_distance(&[3_f64.sinh(), 0.], &[-0.5_f64.sinh(), 0.]),
13471353
3.5
13481354
);
13491355
}
13501356
#[test]
13511357
fn test_hyperbolic_dist_inf() {
13521358
assert_eq!(
1353-
hyperbolic_distance(&[f64::INFINITY, f64::INFINITY, 0.], &[1., 0., 0.]),
1354-
f64::INFINITY
1359+
hyperbolic_distance(&[f64::INFINITY, 0.], &[0., 0.]).is_nan(),
1360+
true
13551361
);
13561362
}
13571363

13581364
#[test]
13591365
fn test_hyperbolic_random_graph_seeded() {
13601366
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
13611367
&[
1362-
vec![3_f64.cosh(), 3_f64.sinh(), 0.],
1363-
vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
1364-
vec![0.5_f64.cosh(), 0.5_f64.sinh(), 0.],
1365-
vec![1., 0., 0.],
1368+
vec![3_f64.sinh(), 0.],
1369+
vec![-0.5_f64.sinh(), 0.],
1370+
vec![0.5_f64.sinh(), 0.],
1371+
vec![0., 0.],
13661372
],
1367-
Some(10000.),
13681373
0.75,
1374+
Some(10000.),
13691375
Some(10),
13701376
|| (),
13711377
|| (),
@@ -1379,13 +1385,13 @@ mod tests {
13791385
fn test_hyperbolic_random_graph_threshold() {
13801386
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
13811387
&[
1382-
vec![1_f64.cosh(), 3_f64.sinh(), 0.],
1383-
vec![0.5_f64.cosh(), -0.5_f64.sinh(), 0.],
1384-
vec![1_f64.cosh(), -1_f64.sinh(), 0.],
1388+
vec![3_f64.sinh(), 0.],
1389+
vec![-0.5_f64.sinh(), 0.],
1390+
vec![-1_f64.sinh(), 0.],
13851391
],
1386-
None,
13871392
1.,
13881393
None,
1394+
None,
13891395
|| (),
13901396
|| (),
13911397
)
@@ -1397,24 +1403,9 @@ mod tests {
13971403
#[test]
13981404
fn test_hyperbolic_random_graph_invalid_dim_error() {
13991405
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1400-
&[vec![1., 0.]],
1401-
None,
1406+
&[vec![0.]],
14021407
1.,
14031408
None,
1404-
|| (),
1405-
|| (),
1406-
) {
1407-
Ok(_) => panic!("Returned a non-error"),
1408-
Err(e) => assert_eq!(e, InvalidInputError),
1409-
}
1410-
}
1411-
1412-
#[test]
1413-
fn test_hyperbolic_random_graph_invalid_first_coord_error() {
1414-
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1415-
&[vec![0., 0., 0.]],
1416-
None,
1417-
1.,
14181409
None,
14191410
|| (),
14201411
|| (),
@@ -1427,10 +1418,10 @@ mod tests {
14271418
#[test]
14281419
fn test_hyperbolic_random_graph_neg_r_error() {
14291420
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1430-
&[vec![1., 0., 0.], vec![1., 0., 0.]],
1431-
None,
1421+
&[vec![0., 0.], vec![0., 0.]],
14321422
-1.,
14331423
None,
1424+
None,
14341425
|| (),
14351426
|| (),
14361427
) {
@@ -1442,9 +1433,9 @@ mod tests {
14421433
#[test]
14431434
fn test_hyperbolic_random_graph_neg_beta_error() {
14441435
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1445-
&[vec![1., 0., 0.], vec![1., 0., 0.]],
1446-
Some(-1.),
1436+
&[vec![0., 0.], vec![0., 0.]],
14471437
1.,
1438+
Some(-1.),
14481439
None,
14491440
|| (),
14501441
|| (),
@@ -1457,10 +1448,10 @@ mod tests {
14571448
#[test]
14581449
fn test_hyperbolic_random_graph_diff_dims_error() {
14591450
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
1460-
&[vec![1., 0., 0.], vec![1., 0., 0., 0.]],
1461-
None,
1451+
&[vec![0., 0.], vec![0., 0., 0.]],
14621452
1.,
14631453
None,
1454+
None,
14641455
|| (),
14651456
|| (),
14661457
) {
@@ -1473,9 +1464,9 @@ mod tests {
14731464
fn test_hyperbolic_random_graph_empty_error() {
14741465
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
14751466
&[],
1476-
None,
14771467
1.,
14781468
None,
1469+
None,
14791470
|| (),
14801471
|| (),
14811472
) {
@@ -1487,10 +1478,10 @@ mod tests {
14871478
#[test]
14881479
fn test_hyperbolic_random_graph_directed_error() {
14891480
match hyperbolic_random_graph::<petgraph::graph::DiGraph<(), ()>, _, _, _, _>(
1490-
&[vec![1., 0., 0.], vec![1., 0., 0.]],
1491-
None,
1481+
&[vec![0., 0.], vec![0., 0.]],
14921482
1.,
14931483
None,
1484+
None,
14941485
|| (),
14951486
|| (),
14961487
) {

src/random_graph.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -505,19 +505,20 @@ pub fn random_geometric_graph(
505505
///
506506
/// .. math::
507507
///
508-
/// d(u,v) = \text{arccosh}\left[x_u^0 x_v^0 - \sum_{j=1}^D x_u^j x_v^j \right],
508+
/// d(u,v) = \text{arccosh}\left[x_0(u) x_0(v) - \sum_{j=1}^D x_j(u) x_j(v) \right],
509509
///
510-
/// where :math:`D` is the dimension of the hyperbolic space and :math:`x_u^d` is the
510+
/// where :math:`D` is the dimension of the hyperbolic space and :math:`x_d(u)` is the
511511
/// :math:`d` th-dimension coordinate of node :math:`u` in the hyperboloid model. The
512-
/// number of nodes and the dimension are inferred from the coordinates ``pos``.
512+
/// number of nodes and the dimension are inferred from the coordinates ``pos``. The
513+
/// 0-dimension "time" coordinate is inferred from the others.
513514
///
514515
/// If ``beta`` is ``None``, all pairs of nodes with a distance smaller than ``r`` are connected.
515516
///
516517
/// This algorithm has a time complexity of :math:`O(n^2)` for :math:`n` nodes.
517518
///
518519
/// :param list[list[float]] pos: Hyperboloid coordinates of the nodes
519-
/// [[:math:`x_1^0`, ..., :math:`x_1^D`], ...]. Since the first dimension is associated to
520-
/// the positive term in the metric, each :math:`x_u^0` must be at least 1.
520+
/// [[:math:`x_1(1)`, ..., :math:`x_D(1)`], [:math:`x_1(2)`, ..., :math:`x_D(2)`], ...].
521+
/// The "time" coordinate :math:`x_0` is inferred from the other coordinates.
521522
/// :param float beta: Sigmoid sharpness (nonnegative) of the connection probability.
522523
/// :param float r: Distance at which the connection probability is 0.5 for the probabilistic model.
523524
/// Threshold when ``beta`` is ``None``.
@@ -536,7 +537,7 @@ pub fn hyperbolic_random_graph(
536537
) -> PyResult<graph::PyGraph> {
537538
let default_fn = || py.None();
538539
let graph: StablePyGraph<Undirected> =
539-
match core_generators::hyperbolic_random_graph(&pos, beta, r, seed, default_fn, default_fn)
540+
match core_generators::hyperbolic_random_graph(&pos, r, beta, seed, default_fn, default_fn)
540541
{
541542
Ok(graph) => graph,
542543
Err(_) => return Err(PyValueError::new_err("invalid positions or parameters")),

tests/test_random.py

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -310,13 +310,13 @@ def test_random_geometric_pos_num_nodes_incomp(self):
310310
class TestHyperbolicRandomGraph(unittest.TestCase):
311311
def test_hyperbolic_random_threshold_empty(self):
312312
graph = rustworkx.hyperbolic_random_graph(
313-
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]], 1.0, None
313+
[[math.sinh(0.5), 0], [-math.sinh(1), 0]], 1.0, None
314314
)
315315
self.assertEqual(graph.num_edges(), 0)
316316

317317
def test_hyperbolic_random_prob_empty(self):
318318
graph = rustworkx.hyperbolic_random_graph(
319-
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]],
319+
[[math.sinh(0.5), 0], [-math.sinh(1), 0]],
320320
1.0,
321321
500.0,
322322
seed=10,
@@ -325,15 +325,15 @@ def test_hyperbolic_random_prob_empty(self):
325325

326326
def test_hyperbolic_random_threshold_complete(self):
327327
graph = rustworkx.hyperbolic_random_graph(
328-
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]],
328+
[[math.sinh(0.5), 0], [-math.sinh(1), 0]],
329329
1.55,
330330
None,
331331
)
332332
self.assertEqual(graph.num_edges(), 1)
333333

334334
def test_hyperbolic_random_prob_complete(self):
335335
graph = rustworkx.hyperbolic_random_graph(
336-
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]],
336+
[[math.sinh(0.5), 0], [-math.sinh(1), 0]],
337337
1.55,
338338
500.0,
339339
seed=10,
@@ -346,19 +346,15 @@ def test_hyperbolic_random_no_pos(self):
346346

347347
def test_hyperbolic_random_different_dim_pos(self):
348348
with self.assertRaises(ValueError):
349-
rustworkx.hyperbolic_random_graph([[1, 0, 0], [1, 0, 0, 0]], 1.0, None)
350-
351-
def test_hyperbolic_random_outofbounds_first_dim(self):
352-
with self.assertRaises(ValueError):
353-
rustworkx.hyperbolic_random_graph([[1, 0, 0], [0, 0, 0]], 1.0, None)
349+
rustworkx.hyperbolic_random_graph([[0, 0], [0, 0, 0]], 1.0, None)
354350

355351
def test_hyperbolic_random_neg_r(self):
356352
with self.assertRaises(ValueError):
357-
rustworkx.hyperbolic_random_graph([[1, 0, 0], [1, 0, 0]], -1.0, None)
353+
rustworkx.hyperbolic_random_graph([[0, 0], [0, 0]], -1.0, None)
358354

359355
def test_hyperbolic_random_neg_beta(self):
360356
with self.assertRaises(ValueError):
361-
rustworkx.hyperbolic_random_graph([[1, 0, 0], [1, 0, 0]], 1.0, -1.0)
357+
rustworkx.hyperbolic_random_graph([[0, 0], [0, 0]], 1.0, -1.0)
362358

363359

364360
class TestRandomSubGraphIsomorphism(unittest.TestCase):

0 commit comments

Comments
 (0)