7 Commits
Author SHA1 Message Date
swaits c3b3ff1e4d feat: f32/f64 cargo features added
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 17:01:19 -07:00
swaits 2addbc68b5 doc: minor updates re: type aliases, crate to 0.5.1
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:59:15 -07:00
swaits 6294d1e520 doc: udpate crate description 2023-02-08 15:48:46 -07:00
swaits 2f8769012b bump crate to 0.5.0
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:40:47 -07:00
swaits 57891ed230 feat: type aliases for 2D/3D NetworkCoordinates
This just adds a few type aliases for convenience, and slightly better ergonomics for the most common usage (2D or 3D).

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:40:23 -07:00
swaits fce4946369 feat: improve ergonomics for serialization format
Goal: To make the serialization format more sensible instead of it being a reflection of our internal (ie private) data model.

- mostly handled with #[serde()] macros (renaming, flattening)
- renamed NetworkCoordinate::position to heightvec which is a better name, even if not seen in the published interface
- changed HeightVector from a tuple struct to a named struct so we can control serde on a field-by-field basis
- updated README, tests, and doc tests to match

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:32:27 -07:00
swaits 7f41676795 fix: exclude examples from crate
crates.io is limited to 10MB crates

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 22:09:28 -07:00
6 changed files with 222 additions and 99 deletions

No files matched your search

+10 -2
View File
@@ -1,9 +1,9 @@
[package] [package]
name = "vivaldi-nc" name = "vivaldi-nc"
version = "0.4.0" 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"]
@@ -11,6 +11,14 @@ documentation = "https://docs.rs/vivaldi-nc/"
readme = "README.md" readme = "README.md"
homepage = "https://sr.ht/~swaits/vivaldi-nc/" homepage = "https://sr.ht/~swaits/vivaldi-nc/"
repository = "https://git.sr.ht/~swaits/vivaldi-nc" repository = "https://git.sr.ht/~swaits/vivaldi-nc"
exclude = [
"examples/*",
]
[features]
default = ["f32"]
f32 = []
f64 = []
[dev-dependencies] [dev-dependencies]
assert_approx_eq = "1.1.0" assert_approx_eq = "1.1.0"
+46 -17
View File
@@ -110,30 +110,59 @@ 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
Run an example which loads a n-to-n latency sample from PlanetLab and iterates The repository includes an example which loads a n-to-n latency sample from
on `NetworkCoordinate`s until some low-enough mean error is reached. The output is a JSON array of elements that look like this: PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean error
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)
},
``` ```
To run the example: 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:
```bash ```bash
git clone https://git.sr.ht/~swaits/vivaldi-nc
```
Then `cd` into the local clone and run the example:
```bash
cd vivaldi-nc
cargo run --example planetlab cargo run --example planetlab
``` ```
+88 -50
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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());
} }
} }