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

Hyperbolic generator improvements + fix #1212

Merged
merged 18 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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