diff --git a/README.md b/README.md index e079e7b..a01339c 100644 --- a/README.md +++ b/README.md @@ -114,24 +114,36 @@ usage examples. 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 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 - { - "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 +} ``` +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 diff --git a/src/height_vector.rs b/src/height_vector.rs index a3a720e..db44c4c 100644 --- a/src/height_vector.rs +++ b/src/height_vector.rs @@ -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(Vector, f32); +pub(crate) struct HeightVector { + #[serde(flatten)] + position: Vector, + height: f32, +} // // **** Implementations **** @@ -83,13 +87,17 @@ impl HeightVector { *i = rng.generate::(); } let height = rng.generate::().abs(); - Self(Vector::::from(vec), height).normalized() + Self { + position: Vector::::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 HeightVector { 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 HeightVector { /// 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 HeightVector { impl Default for HeightVector { /// 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 From<([f32; N], f32)> for HeightVector { /// 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::::from(value.0), value.1); + let ret = Self { + position: Vector::::from(value.0), + height: value.1, + }; if ret.is_valid() { ret } else { @@ -150,7 +170,10 @@ impl Add for HeightVector { /// 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 Sub for HeightVector { /// 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 Mul for HeightVector { /// 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 diff --git a/src/lib.rs b/src/lib.rs index f478808..b7f30df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 @@ -77,7 +77,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(); //! //! // estimate the RTT to this remote node without measuring it directly diff --git a/src/network_coordinate.rs b/src/network_coordinate.rs index 47c76c2..10654b3 100644 --- a/src/network_coordinate.rs +++ b/src/network_coordinate.rs @@ -51,7 +51,8 @@ const MIN_ERROR: f32 = f32::EPSILON; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct NetworkCoordinate { - position: HeightVector, + #[serde(flatten)] + heightvec: HeightVector, error: f32, } @@ -106,7 +107,7 @@ impl NetworkCoordinate { /// 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 NetworkCoordinate { // δ = 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 Default for NetworkCoordinate { /// A default `NetworkCoordinate` has a random position and DEFAULT_ERROR fn default() -> Self { Self { - position: HeightVector::::random(), + heightvec: HeightVector::::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); diff --git a/src/vector.rs b/src/vector.rs index 9adc8e4..2ef4db6 100644 --- a/src/vector.rs +++ b/src/vector.rs @@ -24,6 +24,7 @@ pub(crate) struct Vector 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::::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()); } }