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:
swaits committed 2023-02-08 15:32:27 -07:00
1 parent 3cb28947f6
commit 02c99ae0de
5 files changed
+105 -61

No files matched your search

+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
+2 -2
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
@@ -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
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());
}
}