Skip to content

Commit

Permalink
Hyperbolic generator improvements + fix (#1212)
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

* 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>
  • Loading branch information
4 people authored Jun 8, 2024
1 parent 5354512 commit 2ad86a4
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 78 deletions.
113 changes: 52 additions & 61 deletions rustworkx-core/src/generators/random_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -751,14 +751,14 @@ where
/// 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.
/// hyperboloid model. The "time" coordinates are inferred from the others, meaning that
/// at least 2 coordinates must be provided per node. 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.
/// position of node i. The "time" coordinates are inferred.
/// * `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`.
Expand All @@ -774,12 +774,12 @@ where
/// 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,
/// &[vec![3_f64.sinh(), 0.],
/// vec![-0.5_f64.sinh(), 0.],
/// vec![-1_f64.sinh(), 0.]],
/// 2.,
/// None,
/// None,
/// || {()},
/// || {()},
/// ).unwrap();
Expand All @@ -788,8 +788,8 @@ where
/// ```
pub fn hyperbolic_random_graph<G, T, F, H, M>(
pos: &[Vec<f64>],
beta: Option<f64>,
r: f64,
beta: Option<f64>,
seed: Option<u64>,
mut default_node_weight: F,
mut default_edge_weight: H,
Expand All @@ -804,11 +804,14 @@ where
if num_nodes == 0 {
return Err(InvalidInputError {});
}
if pos.iter().any(|xs| xs.iter().any(|x| x.is_nan())) {
if pos
.iter()
.any(|xs| xs.iter().any(|x| x.is_nan() || x.is_infinite()))
{
return Err(InvalidInputError {});
}
let dim = pos[0].len();
if dim < 3 || pos.iter().any(|x| x.len() != dim || x[0] < 1.) {
if dim < 2 || pos.iter().any(|x| x.len() != dim) {
return Err(InvalidInputError {});
}
if beta.is_some_and(|b| b < 0. || b.is_nan()) {
Expand Down Expand Up @@ -856,17 +859,23 @@ where
}

#[inline]
fn hyperbolic_distance(p1: &[f64], p2: &[f64]) -> f64 {
if p1.iter().chain(p2.iter()).any(|x| x.is_infinite()) {
f64::INFINITY
fn hyperbolic_distance(x: &[f64], y: &[f64]) -> f64 {
let mut sum_squared_x = 0.;
let mut sum_squared_y = 0.;
let mut inner_product = 0.;
for (x_i, y_i) in x.iter().zip(y.iter()) {
if x_i.is_infinite() || y_i.is_infinite() || x_i.is_nan() || y_i.is_nan() {
return f64::NAN;
}
sum_squared_x = x_i.mul_add(*x_i, sum_squared_x);
sum_squared_y = y_i.mul_add(*y_i, sum_squared_y);
inner_product = x_i.mul_add(*y_i, inner_product);
}
let arg = (1. + sum_squared_x).sqrt() * (1. + sum_squared_y).sqrt() - inner_product;
if arg < 1. {
0.
} else {
(p1[0] * p2[0]
- p1.iter()
.skip(1)
.zip(p2.iter().skip(1))
.map(|(&x, &y)| x * y)
.sum::<f64>())
.acosh()
arg.acosh()
}
}

Expand Down Expand Up @@ -1340,32 +1349,29 @@ mod tests {
#[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.]
),
hyperbolic_distance(&[3_f64.sinh(), 0.], &[-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
hyperbolic_distance(&[f64::INFINITY, 0.], &[0., 0.]).is_nan(),
true
);
}

#[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.],
vec![3_f64.sinh(), 0.],
vec![-0.5_f64.sinh(), 0.],
vec![0.5_f64.sinh(), 0.],
vec![0., 0.],
],
Some(10000.),
0.75,
Some(10000.),
Some(10),
|| (),
|| (),
Expand All @@ -1379,13 +1385,13 @@ mod tests {
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.],
vec![3_f64.sinh(), 0.],
vec![-0.5_f64.sinh(), 0.],
vec![-1_f64.sinh(), 0.],
],
None,
1.,
None,
None,
|| (),
|| (),
)
Expand All @@ -1397,24 +1403,9 @@ mod tests {
#[test]
fn test_hyperbolic_random_graph_invalid_dim_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[vec![1., 0.]],
None,
&[vec![0.]],
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,
|| (),
|| (),
Expand All @@ -1427,10 +1418,10 @@ mod tests {
#[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,
&[vec![0., 0.], vec![0., 0.]],
-1.,
None,
None,
|| (),
|| (),
) {
Expand All @@ -1442,9 +1433,9 @@ mod tests {
#[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.),
&[vec![0., 0.], vec![0., 0.]],
1.,
Some(-1.),
None,
|| (),
|| (),
Expand All @@ -1457,10 +1448,10 @@ mod tests {
#[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,
&[vec![0., 0.], vec![0., 0., 0.]],
1.,
None,
None,
|| (),
|| (),
) {
Expand All @@ -1473,9 +1464,9 @@ mod tests {
fn test_hyperbolic_random_graph_empty_error() {
match hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
&[],
None,
1.,
None,
None,
|| (),
|| (),
) {
Expand All @@ -1487,10 +1478,10 @@ mod tests {
#[test]
fn test_hyperbolic_random_graph_directed_error() {
match hyperbolic_random_graph::<petgraph::graph::DiGraph<(), ()>, _, _, _, _>(
&[vec![1., 0., 0.], vec![1., 0., 0.]],
None,
&[vec![0., 0.], vec![0., 0.]],
1.,
None,
None,
|| (),
|| (),
) {
Expand Down
13 changes: 7 additions & 6 deletions src/random_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -505,19 +505,20 @@ pub fn random_geometric_graph(
///
/// .. math::
///
/// d(u,v) = \text{arccosh}\left[x_u^0 x_v^0 - \sum_{j=1}^D x_u^j x_v^j \right],
/// d(u,v) = \text{arccosh}\left[x_0(u) x_0(v) - \sum_{j=1}^D x_j(u) x_j(v) \right],
///
/// where :math:`D` is the dimension of the hyperbolic space and :math:`x_u^d` is the
/// where :math:`D` is the dimension of the hyperbolic space and :math:`x_d(u)` is the
/// :math:`d` th-dimension coordinate of node :math:`u` in the hyperboloid model. The
/// number of nodes and the dimension are inferred from the coordinates ``pos``.
/// number of nodes and the dimension are inferred from the coordinates ``pos``. The
/// 0-dimension "time" coordinate is inferred from the others.
///
/// If ``beta`` is ``None``, all pairs of nodes with a distance smaller than ``r`` are connected.
///
/// This algorithm has a time complexity of :math:`O(n^2)` for :math:`n` nodes.
///
/// :param list[list[float]] pos: Hyperboloid coordinates of the nodes
/// [[:math:`x_1^0`, ..., :math:`x_1^D`], ...]. Since the first dimension is associated to
/// the positive term in the metric, each :math:`x_u^0` must be at least 1.
/// [[:math:`x_1(1)`, ..., :math:`x_D(1)`], [:math:`x_1(2)`, ..., :math:`x_D(2)`], ...].
/// The "time" coordinate :math:`x_0` is inferred from the other coordinates.
/// :param float beta: Sigmoid sharpness (nonnegative) of the connection probability.
/// :param float r: Distance at which the connection probability is 0.5 for the probabilistic model.
/// Threshold when ``beta`` is ``None``.
Expand All @@ -536,7 +537,7 @@ pub fn hyperbolic_random_graph(
) -> PyResult<graph::PyGraph> {
let default_fn = || py.None();
let graph: StablePyGraph<Undirected> =
match core_generators::hyperbolic_random_graph(&pos, beta, r, seed, default_fn, default_fn)
match core_generators::hyperbolic_random_graph(&pos, r, beta, seed, default_fn, default_fn)
{
Ok(graph) => graph,
Err(_) => return Err(PyValueError::new_err("invalid positions or parameters")),
Expand Down
18 changes: 7 additions & 11 deletions tests/test_random.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,13 +310,13 @@ def test_random_geometric_pos_num_nodes_incomp(self):
class TestHyperbolicRandomGraph(unittest.TestCase):
def test_hyperbolic_random_threshold_empty(self):
graph = rustworkx.hyperbolic_random_graph(
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]], 1.0, None
[[math.sinh(0.5), 0], [-math.sinh(1), 0]], 1.0, None
)
self.assertEqual(graph.num_edges(), 0)

def test_hyperbolic_random_prob_empty(self):
graph = rustworkx.hyperbolic_random_graph(
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]],
[[math.sinh(0.5), 0], [-math.sinh(1), 0]],
1.0,
500.0,
seed=10,
Expand All @@ -325,15 +325,15 @@ def test_hyperbolic_random_prob_empty(self):

def test_hyperbolic_random_threshold_complete(self):
graph = rustworkx.hyperbolic_random_graph(
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]],
[[math.sinh(0.5), 0], [-math.sinh(1), 0]],
1.55,
None,
)
self.assertEqual(graph.num_edges(), 1)

def test_hyperbolic_random_prob_complete(self):
graph = rustworkx.hyperbolic_random_graph(
[[math.cosh(0.5), math.sinh(0.5), 0], [math.cosh(1), -math.sinh(1), 0]],
[[math.sinh(0.5), 0], [-math.sinh(1), 0]],
1.55,
500.0,
seed=10,
Expand All @@ -346,19 +346,15 @@ def test_hyperbolic_random_no_pos(self):

def test_hyperbolic_random_different_dim_pos(self):
with self.assertRaises(ValueError):
rustworkx.hyperbolic_random_graph([[1, 0, 0], [1, 0, 0, 0]], 1.0, None)

def test_hyperbolic_random_outofbounds_first_dim(self):
with self.assertRaises(ValueError):
rustworkx.hyperbolic_random_graph([[1, 0, 0], [0, 0, 0]], 1.0, None)
rustworkx.hyperbolic_random_graph([[0, 0], [0, 0, 0]], 1.0, None)

def test_hyperbolic_random_neg_r(self):
with self.assertRaises(ValueError):
rustworkx.hyperbolic_random_graph([[1, 0, 0], [1, 0, 0]], -1.0, None)
rustworkx.hyperbolic_random_graph([[0, 0], [0, 0]], -1.0, None)

def test_hyperbolic_random_neg_beta(self):
with self.assertRaises(ValueError):
rustworkx.hyperbolic_random_graph([[1, 0, 0], [1, 0, 0]], 1.0, -1.0)
rustworkx.hyperbolic_random_graph([[0, 0], [0, 0]], 1.0, -1.0)


class TestRandomSubGraphIsomorphism(unittest.TestCase):
Expand Down

0 comments on commit 2ad86a4

Please sign in to comment.