4 Commits
Author SHA1 Message Date
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 139 additions and 67 deletions

No files matched your search

+4 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "vivaldi-nc"
version = "0.4.0"
version = "0.5.0"
edition = "2021"
authors = ["Stephen Waits <steve@waits.net>"]
description = "Vivaldi network coordinates with a simple interface."
@@ -11,6 +11,9 @@ documentation = "https://docs.rs/vivaldi-nc/"
readme = "README.md"
homepage = "https://sr.ht/~swaits/vivaldi-nc/"
repository = "https://git.sr.ht/~swaits/vivaldi-nc"
exclude = [
"examples/*",
]
[dev-dependencies]
assert_approx_eq = "1.1.0"
+37 -17
View File
@@ -112,28 +112,48 @@ usage examples.
### Examples
Run an example which loads a n-to-n latency sample from 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:
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
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
{
"position": [
{
"inner": [ # Euclidean position (3D in this case):
9.249476, # first dimension; "x"
23.747225, # second dimension; "x"
-6.473008 # third dimension; "x"
]
},
72.10599 # Height component (estimate of stem latency in milliseconds)
],
"error": 2.9166403 # Estimated error (lower is better)
},
{
"position": [
5.563593,
-2.332495,
7.3957834
],
"height": 55.56651,
"error": 3.241348
}
```
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
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
```
+61 -32
View File
@@ -63,7 +63,11 @@ use crate::vector::Vector;
///
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
#[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<f32, N>,
height: f32,
}
//
// **** Implementations ****
@@ -83,13 +87,17 @@ impl<const N: usize> HeightVector<N> {
*i = rng.generate::<f32>();
}
let height = rng.generate::<f32>().abs();
Self(Vector::<f32, N>::from(vec), height).normalized()
Self {
position: Vector::<f32, N>::from(vec),
height,
}
.normalized()
}
/// The magnitude of a Vivaldi height vector is defined as the magnitude of the vector plus the
/// height value.
pub(crate) fn len(&self) -> f32 {
self.0.len() + self.1
self.position.len() + self.height
}
/// A normalized Vivaldi height vector is just like a normalized vector: the vector scaled
@@ -101,7 +109,10 @@ impl<const N: usize> HeightVector<N> {
Self::new()
} else {
// 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 +129,10 @@ impl<const N: usize> HeightVector<N> {
/// In this case, valid means the height is positive, and none of the components are NaN or
/// Inf.
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 +143,20 @@ impl<const N: usize> 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.
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> {
/// Convert from a `([f32; N], f32)` (vector, height) to a Vivaldi height vector type.
fn from(value: ([f32; N], f32)) -> Self {
let ret = Self(Vector::<f32, N>::from(value.0), value.1);
let ret = Self {
position: Vector::<f32, N>::from(value.0),
height: value.1,
};
if ret.is_valid() {
ret
} else {
@@ -150,7 +170,10 @@ impl<const N: usize> Add for HeightVector<N> {
/// Add two Vivaldi height vectors.
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() {
ret
} else {
@@ -165,7 +188,10 @@ impl<const N: usize> Sub for HeightVector<N> {
/// 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.
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() {
ret
} else {
@@ -179,7 +205,10 @@ impl<const N: usize> Mul<f32> for HeightVector<N> {
/// Multiply a Vivaldi height vector by a scalar. Works the same as normal vector scaling.
fn mul(self, rhs: f32) -> 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() {
ret
} else {
@@ -220,9 +249,9 @@ mod tests {
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
let c = a+b;
assert_eq!(c.0[0], fx0 + fx1);
assert_eq!(c.0[1], fy0 + fy1);
assert_eq!(c.1, fh0 + fh1);
assert_eq!(c.position[0], fx0 + fx1);
assert_eq!(c.position[1], fy0 + fy1);
assert_eq!(c.height, fh0 + fh1);
}
#[test]
@@ -234,9 +263,9 @@ mod tests {
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
let c = a-b;
assert_eq!(c.0[0], fx0 - fx1);
assert_eq!(c.0[1], fy0 - fy1);
assert_eq!(c.1, fh0 + fh1);
assert_eq!(c.position[0], fx0 - fx1);
assert_eq!(c.position[1], fy0 - fy1);
assert_eq!(c.height, fh0 + fh1);
}
#[test]
@@ -249,9 +278,9 @@ mod tests {
if fm < 0.0 {
assert_approx_eq!(b.len(), 1.0)
} else {
assert_eq!(b.0[0], fx * fm);
assert_eq!(b.0[1], fy * fm);
assert_eq!(b.1, fh * fm);
assert_eq!(b.position[0], fx * fm);
assert_eq!(b.position[1], fy * fm);
assert_eq!(b.height, fh * fm);
}
}
}
@@ -279,9 +308,9 @@ mod tests {
#[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);
assert_eq!(a.position[0], 1.0);
assert_eq!(a.position[1], 2.0);
assert_eq!(a.height, 3.0);
}
#[test]
@@ -289,9 +318,9 @@ mod tests {
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);
assert_eq!(c.position[0], 4.0);
assert_eq!(c.position[1], 4.0);
assert_eq!(c.height, 4.0);
}
#[test]
@@ -299,23 +328,23 @@ mod tests {
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);
assert_eq!(c.position[0], -2.0);
assert_eq!(c.position[1], 0.0);
assert_eq!(c.height, 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);
assert_eq!(a.position[0], 10.0);
assert_eq!(a.position[1], 20.0);
assert_eq!(a.height, 30.0);
}
#[test]
fn test_serde() {
// 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();
// make sure it's the right length and works like we expect a normal NC
+22 -5
View File
@@ -51,7 +51,7 @@
//!
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
//! // 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();
//!
//! // mock up a measured RTT between us and that node (normally the nodes would coordinate
@@ -69,16 +69,19 @@
//! 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.
//!
//! 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
//! 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
//! // this from a remote node)
//! let received_msg = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
//! let remote_nc: NetworkCoordinate<3> = serde_json::from_str(received_msg).unwrap();
//! let received_msg = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
//! let remote_nc: NetworkCoordinate3D = serde_json::from_str(received_msg).unwrap();
//!
//! // estimate the RTT to this remote node without measuring it directly
//! let rtt_estimated = my_nc.estimated_rtt(&remote_nc);
@@ -94,3 +97,17 @@ mod vector;
// publish our interface
pub mod network_coordinate;
pub use network_coordinate::NetworkCoordinate;
// 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>;
+13 -11
View File
@@ -51,7 +51,8 @@ const MIN_ERROR: f32 = f32::EPSILON;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NetworkCoordinate<const N: usize> {
position: HeightVector<N>,
#[serde(flatten)]
heightvec: HeightVector<N>,
error: f32,
}
@@ -106,7 +107,7 @@ impl<const N: usize> NetworkCoordinate<N> {
///
pub fn estimated_rtt(&self, rhs: &Self) -> Duration {
// 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)
Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0)
}
/// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round
@@ -166,11 +167,12 @@ impl<const N: usize> NetworkCoordinate<N> {
// δ = cc × w
let delta = C_DELTA * w;
// 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
// error
if self.position.is_invalid() {
if self.heightvec.is_invalid() {
*self = Self::new()
}
@@ -193,7 +195,7 @@ impl<const N: usize> Default for NetworkCoordinate<N> {
/// A default `NetworkCoordinate` has a random position and DEFAULT_ERROR
fn default() -> Self {
Self {
position: HeightVector::<N>::random(),
heightvec: HeightVector::<N>::random(),
error: DEFAULT_ERROR,
}
}
@@ -258,8 +260,8 @@ mod tests {
// verify the initial error
let error =
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
.sqrt();
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
.sqrt();
assert_eq!(error, 400.0);
// iterate plenty of times to converge and minimize error
@@ -292,11 +294,11 @@ mod tests {
#[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 s = "{\"position\":[1.5,0.5,2.0],\"height\":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_approx_eq!(a.heightvec.len(), 2.649_509, 0.001);
assert_eq!(a.error, 1.0);
assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
@@ -308,9 +310,9 @@ mod tests {
#[test]
fn test_estimated_rtt() {
// 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 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 estimate = a.estimated_rtt(&b);
+2 -1
View File
@@ -24,6 +24,7 @@ pub(crate) struct Vector<T, const N: usize>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
#[serde(rename = "position")]
#[serde_as(as = "[_; N]")]
inner: [T; N],
}
@@ -279,7 +280,7 @@ mod tests {
fn test_serde() {
let a = Vector::<f32, 3>::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_eq!(s.as_ref().unwrap(), "{\"position\":[1.0,2.0,3.0]}");
assert!(s.is_ok());
}
}