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>
This commit is contained in:
1 parent
3cb28947f6
commit
02c99ae0de
5 files changed
+105
-61
No files matched your search
@@ -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
|
||||
|
||||
+61
-32
@@ -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
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+13
-11
@@ -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
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user