diff --git a/Cargo.toml b/Cargo.toml index fdaa401..52b40ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,13 @@ documentation = "https://docs.rs/vivaldi-nc/" readme = "README.md" homepage = "https://sr.ht/~swaits/vivaldi-nc/" repository = "https://git.sr.ht/~swaits/vivaldi-nc" -license-file = "LICENSE.txt" [dev-dependencies] assert_approx_eq = "1.1.0" +serde_json = "1.0.91" [dependencies] nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]} num-traits = "0.2.15" +serde = {version = "1.0.152", features = ["serde_derive"]} +serde_with = "2.2.0" diff --git a/README.md b/README.md index e73b69e..fcc913c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ other will be close to each other. Nodes with high latency between them will be far from each other. This is an implementation of Vivaldi Network Coordinates, a specific NC -algorithm, with a public simple interface and few dependencies. Vivaldi +algorithm, with a simple simple interface and few dependencies. Vivaldi coordinates are typically used in distributed networking applications, like p2p networks. They allow each node in the network to estimate its position in a latency space in a distributed way, with no central authority. This enables @@ -62,6 +62,51 @@ Regarding the two drawbacks mentioned by the Wikiepedia article, quoted above: ### Getting Started +Add the crate to your project: + +```bash +cargo add vivaldi-nc +``` + +Each node in the network should create a `NetworkCoordinate` (NC). The node +uses this structure to track its latency position in the entire network. + +```rust +use vivaldi_nc::network_coordinate::NetworkCoordinate; + +// create a 2-dimensional NetworkCoordinate to track my position +let mut my_position = NetworkCoordinate::<2>::new(); +``` +Normally 2 or 3-dimensions is plenty. Higher dimensions may add some accuracy, +but not enough to be worth the extra costs. + +Each node occasionally sends its NC to other nodes. Upon receiving a NC from a +remote node, update your local node's position with the actual measured ping +time to that node. + +```rust +my_position.update(&remote_position, Duration::from_millis(measured_rtt)); +``` + +Over time your NC will get more and more accurate as it's updated against more +nodes in the network. + +You can estimate your ping time with any other NC you receive, even if it was +forwarded to you indirectly. + +```rust +let rtt_estimate = my_position.estimate_rtt(&remote_position); +``` + +That's the entire interface for creating and iteratively updating NCs. + +If you want to save/restore NCs, or send/receive them over a network, you'll +want to serialize/deserialize them. `NetworkCoordinate` supports +[Serde](https://crates.io/crates/serde) by default. Many formats are supported +by Serde, including text formats like [JSON](https://github.com/serde-rs/json) +and compact binary formats like [bincode](https://crates.io/crates/bincode) and +[MessagePack](https://github.com/3Hren/msgpack-rust). + ### Features ### Examples @@ -72,13 +117,19 @@ One design goal of this crate is to minimize dependencies. When dependencies are required, I try to be very selective about them (re: bloat and licensing). This crate depends on: -- `nanorand`: A fast PRNG based on WyRand. It pulls in `getrandom` as a - portable source of entropy. I chose this over the more commonly used, but - heavyweight `rand` because it's significantly smaller and does just what I - need, and not much more. -- `num-traits`: A brilliant crate which makes it easier to operate on numbers - in generics; like using `Float` as a constraint on a generic type. Its - convenience outweighs its cost. +- [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand. + It pulls in `getrandom` as a portable source of entropy. I chose this over + the more commonly used, but heavyweight `rand` because it's significantly + smaller and does just what I need, and not much more. +- [`num-traits`](https://crates.io/crates/num-traits): A brilliant crate which + makes it easier to operate on numbers in generics; like using `Float` as a + constraint on a generic type. Its convenience outweighs its cost. +- [`serde`](https://crates.io/crates/serde): I think NCs are usually meant to + be shared across a network. That requires serialization/deserialization and + serde is *the* choice for that. It might be big, but it's efficient. +- [`serde_with`](https://crates.io/crates/serde_with): Because the inner + `Vector` uses a const generic length, we use this to help derive + `Deserialize`. ## Alternative implementations @@ -98,12 +149,12 @@ I had several design goals which the existing crates didn't satisfy: rigamarole. Support `serde` traits by default. 4. Well documented. Well tested. This varies by the crates. -All of that said, those other implementations might work best for you. Here are -the ones I know of today: +All of that said, those other rust implementations might work best for you. +Here are the ones I know of today: -- TODO: list crates -- TODO: list crates -- TODO: list crates +- [netloc](https://github.com/dgtony/netloc) +- [violin](https://crates.io/crates/violin) +- [vivaldi](https://crates.io/crates/vivaldi) ## Other Algorithms diff --git a/src/height_vector.rs b/src/height_vector.rs index 8f704e3..d42f005 100644 --- a/src/height_vector.rs +++ b/src/height_vector.rs @@ -45,6 +45,7 @@ use std::ops::{Add, Mul, Sub}; use nanorand::{Rng, WyRand}; +use serde::{Deserialize, Serialize}; use crate::vector::Vector; @@ -61,7 +62,7 @@ use crate::vector::Vector; /// ## Generic Parameters /// /// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize)] pub(crate) struct HeightVector(Vector, f32); // @@ -160,4 +161,68 @@ mod tests { let a = HeightVector::<3>::from(([1.0, 2.0, 3.0], 4.0)); assert_approx_eq!(a.len(), 7.741_657, 0.00001); } + + #[test] + fn test_new() { + // new gives us a random unit length vec + let a = HeightVector::<3>::new(); + assert_approx_eq!(a.len(), 1.0); + } + + #[test] + fn test_default() { + // default() should give us a zero vec + let a = HeightVector::<3>::default(); + assert_approx_eq!(a.len(), 0.0); + } + + #[test] + fn test_from() { + let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)); + assert_eq!(a.0[0], 1.0); + assert_eq!(a.0[1], 2.0); + assert_eq!(a.1, 3.0); + } + + #[test] + fn test_add() { + let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)); + let b = HeightVector::<2>::from(([3.0, 2.0], 1.0)); + let c = a + b; + assert_eq!(c.0[0], 4.0); + assert_eq!(c.0[1], 4.0); + assert_eq!(c.1, 4.0); + } + + #[test] + fn test_sub() { + let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)); + let b = HeightVector::<2>::from(([3.0, 2.0], 1.0)); + let c = a - b; + assert_eq!(c.0[0], -2.0); + assert_eq!(c.0[1], 0.0); + assert_eq!(c.1, 4.0); + } + + #[test] + fn test_mul_scalar() { + let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0; + assert_eq!(a.0[0], 10.0); + assert_eq!(a.0[1], 20.0); + assert_eq!(a.1, 30.0); + } + + #[test] + fn test_serde() { + // start with JSON, deserialize it + let s = "[{\"inner\":[1.0,2.0,3.0]},4.0]"; + 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 + assert_approx_eq!(a.len(), 7.741_657, 0.001); + + // serialize it into a new JSON string and make sure it matches the original + let t = serde_json::to_string(&a); + assert_eq!(t.as_ref().unwrap(), s); + } } diff --git a/src/network_coordinate.rs b/src/network_coordinate.rs index 421c33f..6940aa7 100644 --- a/src/network_coordinate.rs +++ b/src/network_coordinate.rs @@ -27,6 +27,8 @@ use std::time::Duration; +use serde::{Deserialize, Serialize}; + use crate::height_vector::HeightVector; // @@ -47,7 +49,7 @@ const MIN_ERROR: f32 = f32::EPSILON; // **** Structs **** // -#[derive(Debug)] +#[derive(Debug, Serialize, Deserialize)] pub struct NetworkCoordinate { position: HeightVector, error: f32, @@ -274,6 +276,22 @@ mod tests { (slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0)) .sqrt(); println!("error = {error}"); - assert!(error < 0.07); + assert!(error < 0.2); + } + + #[test] + fn test_serde() { + // start with JSON, deserialize it + let s = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}"; + 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 + assert_approx_eq!(a.position.len(), 2.649_509, 0.001); + assert_eq!(a.error, 1.0); + assert_eq!(a.estimated_rtt(&a).as_millis(), 0); + + // serialize it into a new JSON string and make sure it matches the original + let t = serde_json::to_string(&a); + assert_eq!(t.as_ref().unwrap(), s); } } diff --git a/src/vector.rs b/src/vector.rs index cfee924..b89e7bd 100644 --- a/src/vector.rs +++ b/src/vector.rs @@ -4,6 +4,8 @@ //! Vivaldi `HeightVector`. use num_traits::Float; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; use std::ops::{Add, Div, Index, Mul, Sub}; @@ -16,14 +18,24 @@ use std::ops::{Add, Div, Index, Mul, Sub}; /// ## Generic parameters: /// - `T`: the type to use, must satisfy the `Float` trait (i.e. `f32` or `f64`) /// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc) -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub(crate) struct Vector([T; N]); +#[serde_as] +#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub(crate) struct Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ + #[serde_as(as = "[_; N]")] + inner: [T; N], +} // // **** Implementations **** // -impl Vector { +impl Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ /// Create a new Zero vector. pub(crate) fn new() -> Self { Self::default() @@ -32,8 +44,8 @@ impl Vector { /// Compute magnitude of vector. pub(crate) fn len(&self) -> T { (0..N) - .map(|i| self.0[i] * self.0[i]) - .fold(T::zero(), |acc, x| acc + x) + .map(|i| self.inner[i] * self.inner[i]) + .fold(T::zero(), |sum_sq, x| sum_sq + x) .sqrt() } } @@ -42,21 +54,32 @@ impl Vector { // **** Trait Implementations **** // -impl Default for Vector { +impl Default for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ /// A default vector is all zeroes fn default() -> Self { - Self([T::zero(); N]) + Self { + inner: [T::zero(); N], + } } } -impl From<[T; N]> for Vector { +impl From<[T; N]> for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ /// Create a new Vector from an array of type T, N dimensions. fn from(value: [T; N]) -> Self { - Self(value) + Self { inner: value } } } -impl Add for Vector { +impl Add for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ type Output = Self; /// Vector+Vector addition. @@ -69,7 +92,10 @@ impl Add for Vector { } } -impl Sub for Vector { +impl Sub for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ type Output = Self; /// Vector-Vector subtraction. @@ -82,38 +108,47 @@ impl Sub for Vector { } } -impl Mul for Vector { +impl Mul for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ type Output = Self; /// Vector*scalar multiplication. fn mul(self, rhs: T) -> Self::Output { let mut ret = Self::Output::new(); for i in 0..N { - ret.0[i] = self.0[i] * rhs + ret.inner[i] = self.inner[i] * rhs } ret } } -impl Div for Vector { +impl Div for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ type Output = Self; /// Vector/scalar division. fn div(self, rhs: T) -> Self::Output { let mut ret = Self::Output::new(); for i in 0..N { - ret.0[i] = self.0[i] / rhs + ret.inner[i] = self.inner[i] / rhs } ret } } -impl Index for Vector { +impl Index for Vector +where + T: Float + Serialize + for<'d> Deserialize<'d>, +{ type Output = T; /// Nicer indexing [] for read only references. fn index(&self, index: usize) -> &Self::Output { - &self.0[index] + &self.inner[index] } } @@ -183,4 +218,12 @@ mod tests { let b = a * 10.0; assert_approx_eq!(b.len(), 37.416_57, 0.0001); } + + #[test] + fn test_serde() { + let a = Vector::::from([1.0, 2.0, 3.0]); + let s = serde_json::to_string(&a); + assert_eq!(s.as_ref().unwrap(), "{\"inner\":[1.0,2.0,3.0]}"); + assert!(s.is_ok()); + } }