Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3b3ff1e4d
|
||
|
|
2addbc68b5
|
||
|
|
6294d1e520
|
||
|
|
2f8769012b
|
||
|
|
57891ed230
|
||
|
|
fce4946369
|
No files matched your search
+7
-2
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "vivaldi-nc"
|
name = "vivaldi-nc"
|
||||||
version = "0.4.1"
|
version = "0.5.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Stephen Waits <steve@waits.net>"]
|
authors = ["Stephen Waits <steve@waits.net>"]
|
||||||
description = "Vivaldi network coordinates with a simple interface."
|
description = "Vivaldi network coordinates for fast, distributed latency estimates in multinode networks, with a clean and simple interface."
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = ["vivaldi", "network", "coordinates", "distributed", "p2p"]
|
keywords = ["vivaldi", "network", "coordinates", "distributed", "p2p"]
|
||||||
categories = ["algorithms", "network-programming"]
|
categories = ["algorithms", "network-programming"]
|
||||||
@@ -15,6 +15,11 @@ exclude = [
|
|||||||
"examples/*",
|
"examples/*",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["f32"]
|
||||||
|
f32 = []
|
||||||
|
f64 = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
assert_approx_eq = "1.1.0"
|
assert_approx_eq = "1.1.0"
|
||||||
proptest = "1.0.0"
|
proptest = "1.0.0"
|
||||||
|
|||||||
@@ -110,28 +110,49 @@ and compact binary formats like [bincode](https://crates.io/crates/bincode) and
|
|||||||
See the [module documentation](https://docs.rs/vivaldi-nc) for more detailed
|
See the [module documentation](https://docs.rs/vivaldi-nc) for more detailed
|
||||||
usage examples.
|
usage examples.
|
||||||
|
|
||||||
|
### Cargo Features
|
||||||
|
|
||||||
|
By default, the internal data structures and operations all use `f32`. If you want to use `f64`, you can enable it as a cargo feature:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
vivaldi-nc = { version = "0.5.2", features = ["f64"] }
|
||||||
|
```
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
The repository includes an example which loads a n-to-n latency sample from
|
The repository includes an example which loads a n-to-n latency sample from
|
||||||
PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean error
|
PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean error
|
||||||
is reached. The output is a JSON array of elements that look like this:
|
is reached. The output is a JSON array of elements which contain the NC's
|
||||||
|
position, its height (or stem latency estimate), and its estimation of error
|
||||||
|
(lower is better) that look like this:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"position": [
|
"position": [
|
||||||
{
|
5.563593,
|
||||||
"inner": [ # Euclidean position (3D in this case):
|
-2.332495,
|
||||||
9.249476, # first dimension; "x"
|
7.3957834
|
||||||
23.747225, # second dimension; "x"
|
],
|
||||||
-6.473008 # third dimension; "x"
|
"height": 55.56651,
|
||||||
]
|
"error": 3.241348
|
||||||
},
|
}
|
||||||
72.10599 # Height component (estimate of stem latency in milliseconds)
|
|
||||||
],
|
|
||||||
"error": 2.9166403 # Estimated error (lower is better)
|
|
||||||
},
|
|
||||||
|
|
||||||
```
|
```
|
||||||
|
Adding some detail to these three fields:
|
||||||
|
|
||||||
|
- `position`: Estimated position of this node (or `NetworkCoordinate`) on the
|
||||||
|
network core. Think of this like the node's endpoint into the Internet
|
||||||
|
backbone. This is a cartesian coordinate in a multi-dimensional latency space
|
||||||
|
(in this case 3D), measured in milliseconds.
|
||||||
|
- `height`: Estimated stem time, which is the time from the node itself to the
|
||||||
|
backbone. So, for example this might represent the latency between your
|
||||||
|
node's actual location, like your home, and the point at which it gets into
|
||||||
|
the highest-speed, core part of the Internet. Height helps adjust for
|
||||||
|
[triangle inequality violations](https://en.wikipedia.org/wiki/Triangle_inequality),
|
||||||
|
which are common on the Internet.
|
||||||
|
- `error`: Estimated error of the current `position`, and `height`. Lower is
|
||||||
|
better.
|
||||||
|
|
||||||
To run the example, first clone the repository locally:
|
To run the example, first clone the repository locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+88
-50
@@ -49,6 +49,15 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::vector::Vector;
|
use crate::vector::Vector;
|
||||||
|
|
||||||
|
//
|
||||||
|
// **** Features ****
|
||||||
|
//
|
||||||
|
#[cfg(feature = "f32")]
|
||||||
|
type FloatType = f32;
|
||||||
|
|
||||||
|
#[cfg(feature = "f64")]
|
||||||
|
type FloatType = f64;
|
||||||
|
|
||||||
//
|
//
|
||||||
// **** Constants ****
|
// **** Constants ****
|
||||||
//
|
//
|
||||||
@@ -63,7 +72,11 @@ use crate::vector::Vector;
|
|||||||
///
|
///
|
||||||
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
|
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
|
||||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
|
||||||
pub(crate) struct HeightVector<const N: usize>(Vector<f32, N>, f32);
|
pub(crate) struct HeightVector<const N: usize> {
|
||||||
|
#[serde(flatten)]
|
||||||
|
position: Vector<FloatType, N>,
|
||||||
|
height: FloatType,
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// **** Implementations ****
|
// **** Implementations ****
|
||||||
@@ -80,28 +93,35 @@ impl<const N: usize> HeightVector<N> {
|
|||||||
let mut rng = WyRand::new();
|
let mut rng = WyRand::new();
|
||||||
let mut vec = [0.0; N];
|
let mut vec = [0.0; N];
|
||||||
for i in vec.iter_mut().take(N) {
|
for i in vec.iter_mut().take(N) {
|
||||||
*i = rng.generate::<f32>();
|
*i = rng.generate::<FloatType>();
|
||||||
}
|
}
|
||||||
let height = rng.generate::<f32>().abs();
|
let height = rng.generate::<FloatType>().abs();
|
||||||
Self(Vector::<f32, N>::from(vec), height).normalized()
|
Self {
|
||||||
|
position: Vector::<FloatType, N>::from(vec),
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
.normalized()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The magnitude of a Vivaldi height vector is defined as the magnitude of the vector plus the
|
/// The magnitude of a Vivaldi height vector is defined as the magnitude of the vector plus the
|
||||||
/// height value.
|
/// height value.
|
||||||
pub(crate) fn len(&self) -> f32 {
|
pub(crate) fn len(&self) -> FloatType {
|
||||||
self.0.len() + self.1
|
self.position.len() + self.height
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A normalized Vivaldi height vector is just like a normalized vector: the vector scaled
|
/// A normalized Vivaldi height vector is just like a normalized vector: the vector scaled
|
||||||
/// by the inveerse of its length.
|
/// by the inveerse of its length.
|
||||||
pub(crate) fn normalized(&self) -> Self {
|
pub(crate) fn normalized(&self) -> Self {
|
||||||
let len = self.len();
|
let len = self.len();
|
||||||
if len < f32::EPSILON {
|
if len < FloatType::EPSILON {
|
||||||
// if we have a bad vector, generate a new random vector
|
// if we have a bad vector, generate a new random vector
|
||||||
Self::new()
|
Self::new()
|
||||||
} else {
|
} else {
|
||||||
// otherwise scale it by the inverse of its magnitude like any normal vector
|
// otherwise scale it by the inverse of its magnitude like any normal vector
|
||||||
Self(self.0 / len, self.1 / len)
|
Self {
|
||||||
|
position: self.position / len,
|
||||||
|
height: self.height / len,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +138,10 @@ impl<const N: usize> HeightVector<N> {
|
|||||||
/// In this case, valid means the height is positive, and none of the components are NaN or
|
/// In this case, valid means the height is positive, and none of the components are NaN or
|
||||||
/// Inf.
|
/// Inf.
|
||||||
pub(crate) fn is_invalid(&self) -> bool {
|
pub(crate) fn is_invalid(&self) -> bool {
|
||||||
self.0.is_invalid() || self.1.is_nan() || self.1.is_infinite() || self.1 < 0.0
|
self.position.is_invalid()
|
||||||
|
|| self.height.is_nan()
|
||||||
|
|| self.height.is_infinite()
|
||||||
|
|| self.height < 0.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,14 +152,20 @@ impl<const N: usize> HeightVector<N> {
|
|||||||
impl<const N: usize> Default for HeightVector<N> {
|
impl<const N: usize> Default for HeightVector<N> {
|
||||||
/// Default value for a Vivaldi height vector is just the defaults of its children types.
|
/// Default value for a Vivaldi height vector is just the defaults of its children types.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Default::default(), Default::default())
|
Self {
|
||||||
|
position: Default::default(),
|
||||||
|
height: Default::default(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> From<([f32; N], f32)> for HeightVector<N> {
|
impl<const N: usize> From<([FloatType; N], FloatType)> for HeightVector<N> {
|
||||||
/// Convert from a `([f32; N], f32)` (vector, height) to a Vivaldi height vector type.
|
/// Convert from a `([FloatType; N], FloatType)` (vector, height) to a Vivaldi height vector type.
|
||||||
fn from(value: ([f32; N], f32)) -> Self {
|
fn from(value: ([FloatType; N], FloatType)) -> Self {
|
||||||
let ret = Self(Vector::<f32, N>::from(value.0), value.1);
|
let ret = Self {
|
||||||
|
position: Vector::<FloatType, N>::from(value.0),
|
||||||
|
height: value.1,
|
||||||
|
};
|
||||||
if ret.is_valid() {
|
if ret.is_valid() {
|
||||||
ret
|
ret
|
||||||
} else {
|
} else {
|
||||||
@@ -150,7 +179,10 @@ impl<const N: usize> Add for HeightVector<N> {
|
|||||||
|
|
||||||
/// Add two Vivaldi height vectors.
|
/// Add two Vivaldi height vectors.
|
||||||
fn add(self, rhs: Self) -> Self::Output {
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
let ret = Self(self.0 + rhs.0, self.1 + rhs.1);
|
let ret = Self {
|
||||||
|
position: self.position + rhs.position,
|
||||||
|
height: self.height + rhs.height,
|
||||||
|
};
|
||||||
if ret.is_valid() {
|
if ret.is_valid() {
|
||||||
ret
|
ret
|
||||||
} else {
|
} else {
|
||||||
@@ -165,7 +197,10 @@ impl<const N: usize> Sub for HeightVector<N> {
|
|||||||
/// Subtract two Vivaldi height vectors. Note that this is the difference in the vectors
|
/// Subtract two Vivaldi height vectors. Note that this is the difference in the vectors
|
||||||
/// and the summation of the heights, as defined by Vivaldi's author.
|
/// and the summation of the heights, as defined by Vivaldi's author.
|
||||||
fn sub(self, rhs: Self) -> Self::Output {
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
let ret = Self(self.0 - rhs.0, self.1 + rhs.1);
|
let ret = Self {
|
||||||
|
position: self.position - rhs.position,
|
||||||
|
height: self.height + rhs.height,
|
||||||
|
};
|
||||||
if ret.is_valid() {
|
if ret.is_valid() {
|
||||||
ret
|
ret
|
||||||
} else {
|
} else {
|
||||||
@@ -174,12 +209,15 @@ impl<const N: usize> Sub for HeightVector<N> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> Mul<f32> for HeightVector<N> {
|
impl<const N: usize> Mul<FloatType> for HeightVector<N> {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
/// Multiply a Vivaldi height vector by a scalar. Works the same as normal vector scaling.
|
/// Multiply a Vivaldi height vector by a scalar. Works the same as normal vector scaling.
|
||||||
fn mul(self, rhs: f32) -> Self::Output {
|
fn mul(self, rhs: FloatType) -> Self::Output {
|
||||||
let ret = Self(self.0 * rhs, self.1 * rhs);
|
let ret = Self {
|
||||||
|
position: self.position * rhs,
|
||||||
|
height: self.height * rhs,
|
||||||
|
};
|
||||||
if ret.is_valid() {
|
if ret.is_valid() {
|
||||||
ret
|
ret
|
||||||
} else {
|
} else {
|
||||||
@@ -199,7 +237,7 @@ mod tests {
|
|||||||
|
|
||||||
proptest! {
|
proptest! {
|
||||||
#[test]
|
#[test]
|
||||||
fn proptest_len(x: f32, y: f32, h: f32) {
|
fn proptest_len(x: FloatType, y: FloatType, h: FloatType) {
|
||||||
let len = ((x*x) + (y*y)).sqrt() + h.abs();
|
let len = ((x*x) + (y*y)).sqrt() + h.abs();
|
||||||
let a = HeightVector::<2>::from(([x,y],h));
|
let a = HeightVector::<2>::from(([x,y],h));
|
||||||
if x.is_nan() || x.is_infinite() || y.is_nan() || y.is_infinite() || h.is_nan() || h.is_infinite() || h < 0.0 {
|
if x.is_nan() || x.is_infinite() || y.is_nan() || y.is_infinite() || h.is_nan() || h.is_infinite() || h < 0.0 {
|
||||||
@@ -213,45 +251,45 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn proptest_add(x0 in -1_000_000_000..1_000_000_000i32, y0 in -1_000_000_000..1_000_000_000i32, h0 in 0..1_000_000_000i32, x1 in -1_000_000_000..1_000_000_000i32, y1 in -1_000_000_000..1_000_000_000i32, h1 in 0..1_000_000i32) {
|
fn proptest_add(x0 in -1_000_000_000..1_000_000_000i32, y0 in -1_000_000_000..1_000_000_000i32, h0 in 0..1_000_000_000i32, x1 in -1_000_000_000..1_000_000_000i32, y1 in -1_000_000_000..1_000_000_000i32, h1 in 0..1_000_000i32) {
|
||||||
// convert our integer range inputs to f32s
|
// convert our integer range inputs to FloatType
|
||||||
let (fx0,fy0,fh0) = (x0 as f32 / 1_000.0, y0 as f32 / 1_000.0, h0 as f32 / 1_000.0);
|
let (fx0,fy0,fh0) = (x0 as FloatType / 1_000.0, y0 as FloatType / 1_000.0, h0 as FloatType / 1_000.0);
|
||||||
let (fx1,fy1,fh1) = (x1 as f32 / 1_000.0, y1 as f32 / 1_000.0, h1 as f32 / 1_000.0);
|
let (fx1,fy1,fh1) = (x1 as FloatType / 1_000.0, y1 as FloatType / 1_000.0, h1 as FloatType / 1_000.0);
|
||||||
|
|
||||||
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
|
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
|
||||||
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
|
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
|
||||||
let c = a+b;
|
let c = a+b;
|
||||||
assert_eq!(c.0[0], fx0 + fx1);
|
assert_eq!(c.position[0], fx0 + fx1);
|
||||||
assert_eq!(c.0[1], fy0 + fy1);
|
assert_eq!(c.position[1], fy0 + fy1);
|
||||||
assert_eq!(c.1, fh0 + fh1);
|
assert_eq!(c.height, fh0 + fh1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn proptest_sub(x0 in -1_000_000_000..1_000_000_000i32, y0 in -1_000_000_000..1_000_000_000i32, h0 in 0..1_000_000_000i32, x1 in -1_000_000_000..1_000_000_000i32, y1 in -1_000_000_000..1_000_000_000i32, h1 in 0..1_000_000i32) {
|
fn proptest_sub(x0 in -1_000_000_000..1_000_000_000i32, y0 in -1_000_000_000..1_000_000_000i32, h0 in 0..1_000_000_000i32, x1 in -1_000_000_000..1_000_000_000i32, y1 in -1_000_000_000..1_000_000_000i32, h1 in 0..1_000_000i32) {
|
||||||
// convert our integer range inputs to f32s
|
// convert our integer range inputs to FloatType
|
||||||
let (fx0,fy0,fh0) = (x0 as f32 / 1_000.0, y0 as f32 / 1_000.0, h0 as f32 / 1_000.0);
|
let (fx0,fy0,fh0) = (x0 as FloatType / 1_000.0, y0 as FloatType / 1_000.0, h0 as FloatType / 1_000.0);
|
||||||
let (fx1,fy1,fh1) = (x1 as f32 / 1_000.0, y1 as f32 / 1_000.0, h1 as f32 / 1_000.0);
|
let (fx1,fy1,fh1) = (x1 as FloatType / 1_000.0, y1 as FloatType / 1_000.0, h1 as FloatType / 1_000.0);
|
||||||
|
|
||||||
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
|
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
|
||||||
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
|
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
|
||||||
let c = a-b;
|
let c = a-b;
|
||||||
assert_eq!(c.0[0], fx0 - fx1);
|
assert_eq!(c.position[0], fx0 - fx1);
|
||||||
assert_eq!(c.0[1], fy0 - fy1);
|
assert_eq!(c.position[1], fy0 - fy1);
|
||||||
assert_eq!(c.1, fh0 + fh1);
|
assert_eq!(c.height, fh0 + fh1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn proptest_mul(x in -1_000_000_000..1_000_000_000i32, y in -1_000_000_000..1_000_000_000i32, h in 0..1_000_000_000i32, m in -1_000_000_000..1_000_000_000i32) {
|
fn proptest_mul(x in -1_000_000_000..1_000_000_000i32, y in -1_000_000_000..1_000_000_000i32, h in 0..1_000_000_000i32, m in -1_000_000_000..1_000_000_000i32) {
|
||||||
// convert our integer range inputs to f32s
|
// convert our integer range inputs to FloatType
|
||||||
let (fx,fy,fh,fm) = (x as f32 / 1_000.0, y as f32 / 1_000.0, h as f32 / 1_000.0, m as f32 / 1_000.0);
|
let (fx,fy,fh,fm) = (x as FloatType / 1_000.0, y as FloatType / 1_000.0, h as FloatType / 1_000.0, m as FloatType / 1_000.0);
|
||||||
|
|
||||||
let a = HeightVector::<2>::from(([fx, fy], fh));
|
let a = HeightVector::<2>::from(([fx, fy], fh));
|
||||||
let b = a * fm;
|
let b = a * fm;
|
||||||
if fm < 0.0 {
|
if fm < 0.0 {
|
||||||
assert_approx_eq!(b.len(), 1.0)
|
assert_approx_eq!(b.len(), 1.0)
|
||||||
} else {
|
} else {
|
||||||
assert_eq!(b.0[0], fx * fm);
|
assert_eq!(b.position[0], fx * fm);
|
||||||
assert_eq!(b.0[1], fy * fm);
|
assert_eq!(b.position[1], fy * fm);
|
||||||
assert_eq!(b.1, fh * fm);
|
assert_eq!(b.height, fh * fm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -279,9 +317,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_from() {
|
fn test_from() {
|
||||||
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
||||||
assert_eq!(a.0[0], 1.0);
|
assert_eq!(a.position[0], 1.0);
|
||||||
assert_eq!(a.0[1], 2.0);
|
assert_eq!(a.position[1], 2.0);
|
||||||
assert_eq!(a.1, 3.0);
|
assert_eq!(a.height, 3.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -289,9 +327,9 @@ mod tests {
|
|||||||
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
||||||
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
|
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
|
||||||
let c = a + b;
|
let c = a + b;
|
||||||
assert_eq!(c.0[0], 4.0);
|
assert_eq!(c.position[0], 4.0);
|
||||||
assert_eq!(c.0[1], 4.0);
|
assert_eq!(c.position[1], 4.0);
|
||||||
assert_eq!(c.1, 4.0);
|
assert_eq!(c.height, 4.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -299,23 +337,23 @@ mod tests {
|
|||||||
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
|
||||||
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
|
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
|
||||||
let c = a - b;
|
let c = a - b;
|
||||||
assert_eq!(c.0[0], -2.0);
|
assert_eq!(c.position[0], -2.0);
|
||||||
assert_eq!(c.0[1], 0.0);
|
assert_eq!(c.position[1], 0.0);
|
||||||
assert_eq!(c.1, 4.0);
|
assert_eq!(c.height, 4.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mul_scalar() {
|
fn test_mul_scalar() {
|
||||||
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0;
|
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0;
|
||||||
assert_eq!(a.0[0], 10.0);
|
assert_eq!(a.position[0], 10.0);
|
||||||
assert_eq!(a.0[1], 20.0);
|
assert_eq!(a.position[1], 20.0);
|
||||||
assert_eq!(a.1, 30.0);
|
assert_eq!(a.height, 30.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_serde() {
|
fn test_serde() {
|
||||||
// start with JSON, deserialize it
|
// start with JSON, deserialize it
|
||||||
let s = "[{\"inner\":[1.0,2.0,3.0]},4.0]";
|
let s = "{\"position\":[1.0,2.0,3.0],\"height\":4.0}";
|
||||||
let a: HeightVector<3> = serde_json::from_str(s).unwrap();
|
let a: HeightVector<3> = serde_json::from_str(s).unwrap();
|
||||||
|
|
||||||
// make sure it's the right length and works like we expect a normal NC
|
// make sure it's the right length and works like we expect a normal NC
|
||||||
|
|||||||
+18
-7
@@ -1,4 +1,5 @@
|
|||||||
//! Vivaldi Network Coordinates with a simple interface
|
//! Vivaldi network coordinates for fast, distributed latency estimates in multinode networks, with
|
||||||
|
//! a clean and simple interface.
|
||||||
//!
|
//!
|
||||||
//! Network Coordinates (NC) are a way to represent a node's position in a network's
|
//! Network Coordinates (NC) are a way to represent a node's position in a network's
|
||||||
//! latency space. Nodes with low latency (ping or round trip time) between each
|
//! latency space. Nodes with low latency (ping or round trip time) between each
|
||||||
@@ -19,7 +20,8 @@
|
|||||||
//!
|
//!
|
||||||
//! # Usage
|
//! # Usage
|
||||||
//!
|
//!
|
||||||
//! This crate exports a single struct, [`NetworkCoordinate`]. Typical use of Vivaldi NCs for a
|
//! This crate exports a single struct, [`NetworkCoordinate`] and two type aliases
|
||||||
|
//! ([`NetworkCoordinate2D`] and [`NetworkCoordinate3D`]). Typical use of Vivaldi NCs for a
|
||||||
//! distributed network works like this:
|
//! distributed network works like this:
|
||||||
//!
|
//!
|
||||||
//! 1. Each node in the network has its own instance of [`NetworkCoordinate`]. *See "Note on
|
//! 1. Each node in the network has its own instance of [`NetworkCoordinate`]. *See "Note on
|
||||||
@@ -51,7 +53,7 @@
|
|||||||
//!
|
//!
|
||||||
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
|
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
|
||||||
//! // this from a remote node)
|
//! // this from a remote node)
|
||||||
//! let received_msg = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
|
//! let received_msg = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
|
||||||
//! let remote_nc: NetworkCoordinate<3> = serde_json::from_str(received_msg).unwrap();
|
//! let remote_nc: NetworkCoordinate<3> = serde_json::from_str(received_msg).unwrap();
|
||||||
//!
|
//!
|
||||||
//! // mock up a measured RTT between us and that node (normally the nodes would coordinate
|
//! // mock up a measured RTT between us and that node (normally the nodes would coordinate
|
||||||
@@ -69,16 +71,19 @@
|
|||||||
//! forwarded to us by someone else. And in this case, we aren't measuring the actual RTT. Instead,
|
//! forwarded to us by someone else. And in this case, we aren't measuring the actual RTT. Instead,
|
||||||
//! we're using the NCs to estimate RTT.
|
//! we're using the NCs to estimate RTT.
|
||||||
//!
|
//!
|
||||||
|
//! This example also uses the type alias [`NetworkCoordinate3D`] instead of its equivalent
|
||||||
|
//! [`NetworkCoordinate<3>`].
|
||||||
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use vivaldi_nc::NetworkCoordinate;
|
//! use vivaldi_nc::NetworkCoordinate3D;
|
||||||
//!
|
//!
|
||||||
//! // create our local NC as a 3-dimensional Vivaldi coordinate
|
//! // create our local NC as a 3-dimensional Vivaldi coordinate
|
||||||
//! let mut my_nc = NetworkCoordinate::<3>::new();
|
//! let mut my_nc = NetworkCoordinate3D::new();
|
||||||
//!
|
//!
|
||||||
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
|
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
|
||||||
//! // this from a remote node)
|
//! // this from a remote node)
|
||||||
//! let received_msg = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
|
//! let received_msg = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
|
||||||
//! let remote_nc: NetworkCoordinate<3> = serde_json::from_str(received_msg).unwrap();
|
//! let remote_nc: NetworkCoordinate3D = serde_json::from_str(received_msg).unwrap();
|
||||||
//!
|
//!
|
||||||
//! // estimate the RTT to this remote node without measuring it directly
|
//! // estimate the RTT to this remote node without measuring it directly
|
||||||
//! let rtt_estimated = my_nc.estimated_rtt(&remote_nc);
|
//! let rtt_estimated = my_nc.estimated_rtt(&remote_nc);
|
||||||
@@ -91,6 +96,12 @@
|
|||||||
mod height_vector;
|
mod height_vector;
|
||||||
mod vector;
|
mod vector;
|
||||||
|
|
||||||
|
// make sure we don't have conflicting features
|
||||||
|
#[cfg(all(feature = "f32", feature = "f64"))]
|
||||||
|
compile_error!("feature \"f32\" and feature \"f64\" cannot be enabled at the same time");
|
||||||
|
|
||||||
// publish our interface
|
// publish our interface
|
||||||
pub mod network_coordinate;
|
pub mod network_coordinate;
|
||||||
pub use network_coordinate::NetworkCoordinate;
|
pub use network_coordinate::NetworkCoordinate;
|
||||||
|
pub use network_coordinate::NetworkCoordinate2D;
|
||||||
|
pub use network_coordinate::NetworkCoordinate3D;
|
||||||
+58
-22
@@ -31,19 +31,28 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::height_vector::HeightVector;
|
use crate::height_vector::HeightVector;
|
||||||
|
|
||||||
|
//
|
||||||
|
// **** Features ****
|
||||||
|
//
|
||||||
|
#[cfg(feature = "f32")]
|
||||||
|
type FloatType = f32;
|
||||||
|
|
||||||
|
#[cfg(feature = "f64")]
|
||||||
|
type FloatType = f64;
|
||||||
|
|
||||||
//
|
//
|
||||||
// **** Constants ****
|
// **** Constants ****
|
||||||
//
|
//
|
||||||
|
|
||||||
// Vivaldi tuning parameters
|
// Vivaldi tuning parameters
|
||||||
const C_ERROR: f32 = 0.25;
|
const C_ERROR: FloatType = 0.25;
|
||||||
const C_DELTA: f32 = 0.25;
|
const C_DELTA: FloatType = 0.25;
|
||||||
|
|
||||||
// initial error value
|
// initial error value
|
||||||
const DEFAULT_ERROR: f32 = 200.0;
|
const DEFAULT_ERROR: FloatType = 200.0;
|
||||||
|
|
||||||
// error should always be greater than zero
|
// error should always be greater than zero
|
||||||
const MIN_ERROR: f32 = f32::EPSILON;
|
const MIN_ERROR: FloatType = FloatType::EPSILON;
|
||||||
|
|
||||||
//
|
//
|
||||||
// **** Structs ****
|
// **** Structs ****
|
||||||
@@ -51,10 +60,25 @@ const MIN_ERROR: f32 = f32::EPSILON;
|
|||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct NetworkCoordinate<const N: usize> {
|
pub struct NetworkCoordinate<const N: usize> {
|
||||||
position: HeightVector<N>,
|
#[serde(flatten)]
|
||||||
error: f32,
|
heightvec: HeightVector<N>,
|
||||||
|
error: FloatType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// type aliases for convenience
|
||||||
|
|
||||||
|
/// A 2D [`NetworkCoordinate`]. Includes a 2D Euclidean position and a height.
|
||||||
|
///
|
||||||
|
/// This type alias is just for convenience. It's functionally equivalent to
|
||||||
|
/// `NetworkCoordinate<2>`.
|
||||||
|
pub type NetworkCoordinate2D = NetworkCoordinate<2>;
|
||||||
|
|
||||||
|
/// A 3D [`NetworkCoordinate`]. Includes a 3D Euclidean position and a height.
|
||||||
|
///
|
||||||
|
/// This type alias is just for convenience. It's functionally equivalent to
|
||||||
|
/// `NetworkCoordinate<3>`.
|
||||||
|
pub type NetworkCoordinate3D = NetworkCoordinate<3>;
|
||||||
|
|
||||||
//
|
//
|
||||||
// **** Implementations ****
|
// **** Implementations ****
|
||||||
//
|
//
|
||||||
@@ -106,7 +130,11 @@ impl<const N: usize> NetworkCoordinate<N> {
|
|||||||
///
|
///
|
||||||
pub fn estimated_rtt(&self, rhs: &Self) -> Duration {
|
pub fn estimated_rtt(&self, rhs: &Self) -> Duration {
|
||||||
// estimated rss is euclidean distance between the two plus the sum of the heights
|
// estimated rss is euclidean distance between the two plus the sum of the heights
|
||||||
Duration::from_secs_f32((self.position - rhs.position).len() / 1000.0)
|
#[cfg(feature = "f32")]
|
||||||
|
return Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0);
|
||||||
|
|
||||||
|
#[cfg(feature = "f64")]
|
||||||
|
return Duration::from_secs_f64((self.heightvec - rhs.heightvec).len() / 1000.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round
|
/// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round
|
||||||
@@ -140,15 +168,22 @@ impl<const N: usize> NetworkCoordinate<N> {
|
|||||||
/// local.update(&remote, rtt);
|
/// local.update(&remote, rtt);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn update(&mut self, rhs: &Self, rtt: Duration) -> &Self {
|
pub fn update(&mut self, rhs: &Self, rtt: Duration) -> &Self {
|
||||||
|
// convert Durations into FloatType as fractional milliseconds for convenience
|
||||||
|
#[cfg(feature = "f32")]
|
||||||
|
let rtt_ms = rtt.as_secs_f32() * 1000.0;
|
||||||
|
#[cfg(feature = "f32")]
|
||||||
|
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f32() * 1000.0;
|
||||||
|
|
||||||
|
#[cfg(feature = "f64")]
|
||||||
|
let rtt_ms = rtt.as_secs_f64() * 1000.0;
|
||||||
|
#[cfg(feature = "f64")]
|
||||||
|
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f64() * 1000.0;
|
||||||
|
|
||||||
// rtt needs to be positive
|
// rtt needs to be positive
|
||||||
if rtt.as_secs_f32() < 0.0 {
|
if rtt_ms < 0.0 {
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert Durations into f32s as fractional milliseconds for convenience
|
|
||||||
let rtt_ms = rtt.as_secs_f32() * 1000.0;
|
|
||||||
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f32() * 1000.0;
|
|
||||||
|
|
||||||
// Sample weight balances local and remote error. (1)
|
// Sample weight balances local and remote error. (1)
|
||||||
// w = ei /(ei + ej )
|
// w = ei /(ei + ej )
|
||||||
let w = self.error / (self.error + rhs.error);
|
let w = self.error / (self.error + rhs.error);
|
||||||
@@ -166,11 +201,12 @@ impl<const N: usize> NetworkCoordinate<N> {
|
|||||||
// δ = cc × w
|
// δ = cc × w
|
||||||
let delta = C_DELTA * w;
|
let delta = C_DELTA * w;
|
||||||
// xi = xi + δ × (rtt − ‖xi − xj ‖) × u(xi − xj)
|
// xi = xi + δ × (rtt − ‖xi − xj ‖) × u(xi − xj)
|
||||||
self.position = self.position + (self.position - rhs.position).normalized() * delta * error;
|
self.heightvec =
|
||||||
|
self.heightvec + (self.heightvec - rhs.heightvec).normalized() * delta * error;
|
||||||
|
|
||||||
// if we ended up with an invalid coordinate, return a new random coordinate with default
|
// if we ended up with an invalid coordinate, return a new random coordinate with default
|
||||||
// error
|
// error
|
||||||
if self.position.is_invalid() {
|
if self.heightvec.is_invalid() {
|
||||||
*self = Self::new()
|
*self = Self::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +216,7 @@ impl<const N: usize> NetworkCoordinate<N> {
|
|||||||
|
|
||||||
/// getter for error value - useful for consumers to understand the estimated accuracty of this
|
/// getter for error value - useful for consumers to understand the estimated accuracty of this
|
||||||
/// `NetworkCoordinate`
|
/// `NetworkCoordinate`
|
||||||
pub fn error(&self) -> f32 {
|
pub fn error(&self) -> FloatType {
|
||||||
self.error
|
self.error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,7 +229,7 @@ impl<const N: usize> Default for NetworkCoordinate<N> {
|
|||||||
/// A default `NetworkCoordinate` has a random position and DEFAULT_ERROR
|
/// A default `NetworkCoordinate` has a random position and DEFAULT_ERROR
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
position: HeightVector::<N>::random(),
|
heightvec: HeightVector::<N>::random(),
|
||||||
error: DEFAULT_ERROR,
|
error: DEFAULT_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -258,8 +294,8 @@ mod tests {
|
|||||||
|
|
||||||
// verify the initial error
|
// verify the initial error
|
||||||
let error =
|
let error =
|
||||||
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
|
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
|
||||||
.sqrt();
|
.sqrt();
|
||||||
assert_eq!(error, 400.0);
|
assert_eq!(error, 400.0);
|
||||||
|
|
||||||
// iterate plenty of times to converge and minimize error
|
// iterate plenty of times to converge and minimize error
|
||||||
@@ -292,11 +328,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_serde() {
|
fn test_serde() {
|
||||||
// start with JSON, deserialize it
|
// start with JSON, deserialize it
|
||||||
let s = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
|
let s = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
|
||||||
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
||||||
|
|
||||||
// make sure it's the right length and works like we expect a normal NC
|
// make sure it's the right length and works like we expect a normal NC
|
||||||
assert_approx_eq!(a.position.len(), 2.649_509, 0.001);
|
assert_approx_eq!(a.heightvec.len(), 2.649_509, 0.001);
|
||||||
assert_eq!(a.error, 1.0);
|
assert_eq!(a.error, 1.0);
|
||||||
assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
|
assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
|
||||||
|
|
||||||
@@ -308,9 +344,9 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_estimated_rtt() {
|
fn test_estimated_rtt() {
|
||||||
// start with JSON, deserialize it
|
// start with JSON, deserialize it
|
||||||
let s = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},25.0],\"error\":1.0}";
|
let s = "{\"position\":[1.5,0.5,2.0],\"height\":25.0,\"error\":1.0}";
|
||||||
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
||||||
let s = "{\"position\":[{\"inner\":[-1.5,-0.5,-2.0]},50.0],\"error\":1.0}";
|
let s = "{\"position\":[-1.5,-0.5,-2.0],\"height\":50.0,\"error\":1.0}";
|
||||||
let b: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
let b: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
|
||||||
|
|
||||||
let estimate = a.estimated_rtt(&b);
|
let estimate = a.estimated_rtt(&b);
|
||||||
|
|||||||
+2
-1
@@ -24,6 +24,7 @@ pub(crate) struct Vector<T, const N: usize>
|
|||||||
where
|
where
|
||||||
T: Float + Serialize + for<'d> Deserialize<'d>,
|
T: Float + Serialize + for<'d> Deserialize<'d>,
|
||||||
{
|
{
|
||||||
|
#[serde(rename = "position")]
|
||||||
#[serde_as(as = "[_; N]")]
|
#[serde_as(as = "[_; N]")]
|
||||||
inner: [T; N],
|
inner: [T; N],
|
||||||
}
|
}
|
||||||
@@ -279,7 +280,7 @@ mod tests {
|
|||||||
fn test_serde() {
|
fn test_serde() {
|
||||||
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
||||||
let s = serde_json::to_string(&a);
|
let s = serde_json::to_string(&a);
|
||||||
assert_eq!(s.as_ref().unwrap(), "{\"inner\":[1.0,2.0,3.0]}");
|
assert_eq!(s.as_ref().unwrap(), "{\"position\":[1.0,2.0,3.0]}");
|
||||||
assert!(s.is_ok());
|
assert!(s.is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user