Skip to content

Commit 2adf9ad

Browse files
authored
Merge branch 'main' into remove-39-retworkx
2 parents 7598155 + 8d2742d commit 2adf9ad

File tree

13 files changed

+918
-37
lines changed

13 files changed

+918
-37
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.. _geometry:
2+
3+
Geometry
4+
========
5+
6+
.. autosummary::
7+
:toctree: ../../apiref
8+
9+
rustworkx.hyperbolic_greedy_routing
10+
rustworkx.hyperbolic_greedy_success_rate

docs/source/api/algorithm_functions/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Algorithm Functions
1111
connectivity_and_cycles
1212
dag_algorithms
1313
dominance
14+
geometry
1415
graph_operations
1516
isomorphism
1617
link_analysis
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
features:
2+
- |
3+
Adds the new ``geometry`` module to ``rustworkx_core``.
4+
This module mainly defines the greedy routing algorithm which is useful when each node has a position in a metric space.
5+
The ``greedy_routing`` function returns, if it exists, the greedy path between two nodes and its total length in the metric space.
6+
The ``greedy_routing_success_rate`` returns the proportion of pairs of nodes for which the greedy path reaches the destination.
7+
The greedy routing algorithm may be used with any distance.
8+
The ``geometry`` module implements many distances: ``angular_distance``, ``euclidean_distance``, ``hyperboloid_hyperbolic_distance``, ``lp_distance``, ``maximum_distance`` and ``polar_hyperbolic_distance``.
9+
- |
10+
Adds the function :func:`.hyperbolic_greedy_routing` that performs the greedy routing algorithm in the hyperbolic space, and the function :func:`.hyperbolic_greedy_success_rate` that computes the proportion of pairs of nodes for which the greedy path reaches the destination.

rustworkx-core/src/generators/random_graph.rs

Lines changed: 2 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ use rand_pcg::Pcg64;
3030

3131
use super::star_graph;
3232
use super::InvalidInputError;
33+
use crate::geometry::hyperboloid_hyperbolic_distance;
3334

3435
/// Generates a random regular graph
3536
///
@@ -1004,7 +1005,7 @@ where
10041005
let between = Uniform::new(0.0, 1.0).unwrap();
10051006
for (v, p1) in pos.iter().enumerate().take(num_nodes - 1) {
10061007
for (w, p2) in pos.iter().enumerate().skip(v + 1) {
1007-
let dist = hyperbolic_distance(p1, p2);
1008+
let dist = hyperboloid_hyperbolic_distance(p1, p2).unwrap();
10081009
let is_edge = match beta {
10091010
Some(b) => {
10101011
let prob_inverse = (b / 2. * (dist - r)).exp() + 1.;
@@ -1025,27 +1026,6 @@ where
10251026
Ok(graph)
10261027
}
10271028

1028-
#[inline]
1029-
fn hyperbolic_distance(x: &[f64], y: &[f64]) -> f64 {
1030-
let mut sum_squared_x = 0.;
1031-
let mut sum_squared_y = 0.;
1032-
let mut inner_product = 0.;
1033-
for (x_i, y_i) in x.iter().zip(y.iter()) {
1034-
if x_i.is_infinite() || y_i.is_infinite() || x_i.is_nan() || y_i.is_nan() {
1035-
return f64::NAN;
1036-
}
1037-
sum_squared_x = x_i.mul_add(*x_i, sum_squared_x);
1038-
sum_squared_y = y_i.mul_add(*y_i, sum_squared_y);
1039-
inner_product = x_i.mul_add(*y_i, inner_product);
1040-
}
1041-
let arg = (1. + sum_squared_x).sqrt() * (1. + sum_squared_y).sqrt() - inner_product;
1042-
if arg < 1. {
1043-
0.
1044-
} else {
1045-
arg.acosh()
1046-
}
1047-
}
1048-
10491029
#[cfg(test)]
10501030
mod tests {
10511031
use crate::generators::InvalidInputError;
@@ -1056,10 +1036,7 @@ mod tests {
10561036
};
10571037
use crate::petgraph;
10581038

1059-
use super::hyperbolic_distance;
1060-
10611039
// Test gnp_random_graph
1062-
10631040
#[test]
10641041
fn test_gnp_random_graph_directed() {
10651042
let g: petgraph::graph::DiGraph<(), ()> =
@@ -1546,18 +1523,6 @@ mod tests {
15461523
// x = sinh(r)cos(theta)
15471524
// y = sinh(r)sin(theta)
15481525

1549-
#[test]
1550-
fn test_hyperbolic_dist() {
1551-
assert_eq!(
1552-
hyperbolic_distance(&[3_f64.sinh(), 0.], &[-0.5_f64.sinh(), 0.]),
1553-
3.5
1554-
);
1555-
}
1556-
#[test]
1557-
fn test_hyperbolic_dist_inf() {
1558-
assert!(hyperbolic_distance(&[f64::INFINITY, 0.], &[0., 0.]).is_nan());
1559-
}
1560-
15611526
#[test]
15621527
fn test_hyperbolic_random_graph_seeded() {
15631528
let g = hyperbolic_random_graph::<petgraph::graph::UnGraph<(), ()>, _, _, _, _>(
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may
2+
// not use this file except in compliance with the License. You may obtain
3+
// a copy of the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations
11+
// under the License.
12+
13+
use num_traits::Float;
14+
use std::iter::Sum;
15+
16+
/// Error returned when two points have a different dimension.
17+
#[derive(Debug, PartialEq, Eq)]
18+
pub struct IncompatiblePointsError;
19+
20+
/// Computes the L^`p` distance between `x` and `y`.
21+
///
22+
/// Works for any `p`>0. An [`IncompatiblePointsError`] is returned when `x` and `y` have different
23+
/// lengths.
24+
pub fn lp_distance<T>(x: &[T], y: &[T], p: i32) -> Result<T, IncompatiblePointsError>
25+
where
26+
T: Float + Sum + From<i32>,
27+
{
28+
if x.len() != y.len() {
29+
Err(IncompatiblePointsError {})
30+
} else {
31+
Ok(x.iter()
32+
.zip(y.iter())
33+
.map(|(&a, &b)| T::powi(T::abs(b - a), p))
34+
.sum::<T>()
35+
.powf(T::one() / p.into()))
36+
}
37+
}
38+
39+
/// Computes the Euclidean distance between `x` and `y`.
40+
///
41+
/// An [`IncompatiblePointsError`] is returned when `x` and `y` have different lengths.
42+
pub fn euclidean_distance<T>(x: &[T], y: &[T]) -> Result<T, IncompatiblePointsError>
43+
where
44+
T: Float + Sum + From<i32>,
45+
{
46+
lp_distance(x, y, 2)
47+
}
48+
49+
/// Computes the maximum distance (Chebyshev distance or L^infinity distance) between `x` and `y`.
50+
///
51+
/// An [`IncompatiblePointsError`] is returned when `x` and `y` have different lengths.
52+
pub fn maximum_distance<T: Float>(x: &[T], y: &[T]) -> Result<T, IncompatiblePointsError> {
53+
if x.len() != y.len() {
54+
Err(IncompatiblePointsError {})
55+
} else {
56+
Ok(x.iter()
57+
.zip(y.iter())
58+
.map(|(&a, &b)| T::abs(b - a))
59+
.reduce(Float::max)
60+
.unwrap_or(T::zero()))
61+
}
62+
}
63+
64+
/// Computes the Euclidean dot product between points the unit n-sphere, where n is the length of
65+
/// `angles1` and `angles2`.
66+
///
67+
/// No check is done on the lengths of `angles1` and `angles2`. The Euclidean dot product is also
68+
/// the cosine of the angular distance between `angles1` and `angles2`.
69+
fn euclidean_dot_product<T: Float>(angles1: &[T], angles2: &[T]) -> T {
70+
let mut total = T::zero();
71+
let mut sin_prod = T::one();
72+
let d = angles1.len();
73+
for (i, (t1, t2)) in angles1.iter().zip(angles2.iter()).enumerate() {
74+
total = T::mul_add(sin_prod, t1.cos() * t2.cos(), total);
75+
sin_prod = sin_prod * t1.sin() * t2.sin();
76+
if i == d - 1 {
77+
total = total + sin_prod;
78+
}
79+
}
80+
total
81+
}
82+
83+
/// Computes the distance between the points `angles1` and `angles2` on the unit n-sphere, where n
84+
/// is the length of `angles1` and `angles2`.
85+
///
86+
/// The last element of `angles1` and `angles2` is assumed to be in [0, 2pi] or [-pi, pi] (and the
87+
/// other elements are in [0, pi]). An [`IncompatiblePointsError`] is returned when `angles1` and
88+
/// `angles2` have different lengths.
89+
pub fn angular_distance<T: Float>(
90+
angles1: &[T],
91+
angles2: &[T],
92+
) -> Result<T, IncompatiblePointsError> {
93+
if angles1.len() != angles2.len() {
94+
Err(IncompatiblePointsError {})
95+
} else {
96+
Ok(euclidean_dot_product(angles1, angles2).acos().abs())
97+
}
98+
}
99+
100+
/// Computes the hyperbolic distance between two points in polar coordinates.
101+
///
102+
/// `r1` and `r2` are the distances to the origin and the last element of `angles1` and `angles2`
103+
/// is assumed to be in [0, 2pi] or [-pi, pi] (and the other elements are in [0, pi]). An
104+
/// [`IncompatiblePointsError`] is returned when `angles1` and `angles2` have different lengths.
105+
pub fn polar_hyperbolic_distance<T: Float>(
106+
r1: T,
107+
angles1: &[T],
108+
r2: T,
109+
angles2: &[T],
110+
) -> Result<T, IncompatiblePointsError> {
111+
if angles1.len() != angles2.len() {
112+
Err(IncompatiblePointsError {})
113+
} else {
114+
let arg = (r1 - r2).cosh()
115+
+ (T::one() - euclidean_dot_product(angles1, angles2)) * r1.sinh() * r2.sinh();
116+
Ok(if arg < T::one() {
117+
T::zero()
118+
} else {
119+
arg.acosh()
120+
})
121+
}
122+
}
123+
124+
/// Computes the hyperbolic distance between the points `x1` and `x2` in the hyperboloid model.
125+
///
126+
/// The "time" coordinate (opposite sign in the metric) is inferred from the others and should not
127+
/// be included in `x1` and `x2`. An [`IncompatiblePointsError`] is returned when `x` and `y` have
128+
/// different lengths.
129+
pub fn hyperboloid_hyperbolic_distance<T: Float>(
130+
x: &[T],
131+
y: &[T],
132+
) -> Result<T, IncompatiblePointsError> {
133+
if x.len() != y.len() {
134+
Err(IncompatiblePointsError {})
135+
} else {
136+
let mut sum_x_squared = T::zero();
137+
let mut sum_y_squared = T::zero();
138+
let mut sum_xy = T::zero();
139+
for (x_i, y_i) in x.iter().zip(y.iter()) {
140+
sum_x_squared = T::mul_add(*x_i, *x_i, sum_x_squared);
141+
sum_y_squared = T::mul_add(*y_i, *y_i, sum_y_squared);
142+
sum_xy = T::mul_add(*x_i, *y_i, sum_xy);
143+
}
144+
let arg = (T::one() + sum_x_squared).sqrt() * (T::one() + sum_y_squared).sqrt() - sum_xy;
145+
Ok(if arg < T::one() {
146+
T::zero()
147+
} else {
148+
arg.acosh()
149+
})
150+
}
151+
}
152+
153+
#[cfg(test)]
154+
mod tests {
155+
use std::f64::consts::PI;
156+
157+
use super::{
158+
angular_distance, euclidean_distance, hyperboloid_hyperbolic_distance, lp_distance,
159+
maximum_distance, polar_hyperbolic_distance, IncompatiblePointsError,
160+
};
161+
162+
#[test]
163+
fn test_l4_dist() {
164+
assert!(
165+
(lp_distance(&[1., 2., 3.], &[5., 3., 1.], 4).unwrap()
166+
- (256_f64 + 1. + 16.).sqrt().sqrt())
167+
.abs()
168+
< 1e-15
169+
);
170+
}
171+
172+
#[test]
173+
fn test_l1_dist() {
174+
assert!(
175+
(lp_distance(&[1., 2., 3.], &[5., 3., 1.], 1).unwrap() - (4. + 1. + 2_f64)).abs()
176+
< 1e-15
177+
);
178+
}
179+
180+
#[test]
181+
fn test_lp_dist_incompatible_error() {
182+
assert_eq!(
183+
lp_distance(&[0., 0.], &[0., 0., 0.], 1),
184+
Err(IncompatiblePointsError {})
185+
);
186+
assert_eq!(
187+
lp_distance(&[0., 0.], &[0., 0., 0.], 4),
188+
Err(IncompatiblePointsError {})
189+
);
190+
}
191+
192+
#[test]
193+
fn test_euclidean_dist() {
194+
assert!(
195+
(euclidean_distance(&[1., 2., 3.], &[5., 3., 1.]).unwrap() - (16_f64 + 1. + 4.).sqrt())
196+
.abs()
197+
< 1e-15
198+
);
199+
}
200+
201+
#[test]
202+
fn test_euclidean_dist_incompatible_error() {
203+
assert_eq!(
204+
euclidean_distance(&[0., 0.], &[0., 0., 0.]),
205+
Err(IncompatiblePointsError {})
206+
);
207+
}
208+
209+
#[test]
210+
fn test_maximum_dist() {
211+
assert!((maximum_distance(&[1., 2., 3.], &[5., 3., 1.]).unwrap() - 4_f64).abs() < 1e-15);
212+
}
213+
214+
#[test]
215+
fn test_maximum_dist_incompatible_error() {
216+
assert_eq!(
217+
maximum_distance(&[0., 0.], &[0., 0., 0.]),
218+
Err(IncompatiblePointsError {})
219+
);
220+
}
221+
222+
#[test]
223+
fn test_angular_dist() {
224+
assert!((angular_distance(&[0.3], &[0.5]).unwrap() - 0.2_f64).abs() < 1e-15);
225+
assert!((angular_distance(&[0.5 * PI, PI], &[0.5 * PI, 0.]).unwrap() - PI).abs() < 1e-15);
226+
assert!((angular_distance(&[0.2, 0., 1.], &[0., 0., 1.]).unwrap() - 0.2_f64).abs() < 1e-15);
227+
assert!(
228+
(angular_distance(&[2. * PI, 0., 1.], &[0., 0., 1.]).unwrap() - 0_f64).abs() < 1e-15
229+
);
230+
}
231+
232+
#[test]
233+
fn test_angular_dist_incompatible_error() {
234+
assert_eq!(
235+
angular_distance(&[0.], &[0., 0.]),
236+
Err(IncompatiblePointsError {})
237+
);
238+
}
239+
240+
#[test]
241+
fn test_polar_hyperbolic_dist() {
242+
assert_eq!(
243+
polar_hyperbolic_distance(3., &[0.], 0.5, &[PI]).unwrap(),
244+
3.5
245+
);
246+
}
247+
248+
#[test]
249+
fn test_polar_hyperbolic_dist_incompatible_error() {
250+
assert_eq!(
251+
polar_hyperbolic_distance(1., &[0.], 1., &[0., 0.]),
252+
Err(IncompatiblePointsError {})
253+
);
254+
}
255+
256+
#[test]
257+
fn test_hyperboloid_dist() {
258+
assert_eq!(
259+
hyperboloid_hyperbolic_distance(&[3_f64.sinh(), 0.], &[-0.5_f64.sinh(), 0.]).unwrap(),
260+
3.5
261+
);
262+
}
263+
264+
#[test]
265+
fn test_hyperboloid_dist_inf() {
266+
assert!(
267+
hyperboloid_hyperbolic_distance(&[f64::INFINITY, 0.], &[0., 0.])
268+
.unwrap()
269+
.is_nan()
270+
);
271+
}
272+
#[test]
273+
fn test_hyperboloid_dist_length_error() {
274+
assert_eq!(
275+
euclidean_distance(&[0., 0.], &[0., 0., 0.]),
276+
Err(IncompatiblePointsError {})
277+
);
278+
}
279+
}

0 commit comments

Comments
 (0)