feat: add serde

Adds serialization/deserialization support via serde and serde_with.

Also:

- remove `license-file` in Cargo.toml (superfluous)
- update README with Getting Started section
- add `serde` and `serde_with` dependencies to README
- add tests to `HeightVector`
- tweak assertion in `NetworkCoordinate`
- refactor `Vector` to be a struct witn an `inner` instead of a tuple struct so that we can use `serde_as` (from `serde_with`) to help us derive SerDe implementations for the const generic sized array
- moved trait requirements (which are getting long) into where clauses

Signed-off-by: Stephen Waits <steve@waits.net>
This commit is contained in:
swaits committed 2023-02-04 09:42:46 -07:00
1 parent 3415347072
commit 2fad2e18b5
5 files changed
+213 -34

No files matched your search

+3 -1
View File
@@ -11,11 +11,13 @@ documentation = "https://docs.rs/vivaldi-nc/"
readme = "README.md"
homepage = "https://sr.ht/~swaits/vivaldi-nc/"
repository = "https://git.sr.ht/~swaits/vivaldi-nc"
license-file = "LICENSE.txt"
[dev-dependencies]
assert_approx_eq = "1.1.0"
serde_json = "1.0.91"
[dependencies]
nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]}
num-traits = "0.2.15"
serde = {version = "1.0.152", features = ["serde_derive"]}
serde_with = "2.2.0"
+64 -13
View File
@@ -10,7 +10,7 @@ other will be close to each other. Nodes with high latency between them will be
far from each other.
This is an implementation of Vivaldi Network Coordinates, a specific NC
algorithm, with a public simple interface and few dependencies. Vivaldi
algorithm, with a simple simple interface and few dependencies. Vivaldi
coordinates are typically used in distributed networking applications, like p2p
networks. They allow each node in the network to estimate its position in a
latency space in a distributed way, with no central authority. This enables
@@ -62,6 +62,51 @@ Regarding the two drawbacks mentioned by the Wikiepedia article, quoted above:
### Getting Started
Add the crate to your project:
```bash
cargo add vivaldi-nc
```
Each node in the network should create a `NetworkCoordinate` (NC). The node
uses this structure to track its latency position in the entire network.
```rust
use vivaldi_nc::network_coordinate::NetworkCoordinate;
// create a 2-dimensional NetworkCoordinate to track my position
let mut my_position = NetworkCoordinate::<2>::new();
```
Normally 2 or 3-dimensions is plenty. Higher dimensions may add some accuracy,
but not enough to be worth the extra costs.
Each node occasionally sends its NC to other nodes. Upon receiving a NC from a
remote node, update your local node's position with the actual measured ping
time to that node.
```rust
my_position.update(&remote_position, Duration::from_millis(measured_rtt));
```
Over time your NC will get more and more accurate as it's updated against more
nodes in the network.
You can estimate your ping time with any other NC you receive, even if it was
forwarded to you indirectly.
```rust
let rtt_estimate = my_position.estimate_rtt(&remote_position);
```
That's the entire interface for creating and iteratively updating NCs.
If you want to save/restore NCs, or send/receive them over a network, you'll
want to serialize/deserialize them. `NetworkCoordinate` supports
[Serde](https://crates.io/crates/serde) by default. Many formats are supported
by Serde, including text formats like [JSON](https://github.com/serde-rs/json)
and compact binary formats like [bincode](https://crates.io/crates/bincode) and
[MessagePack](https://github.com/3Hren/msgpack-rust).
### Features
### Examples
@@ -72,13 +117,19 @@ One design goal of this crate is to minimize dependencies. When dependencies
are required, I try to be very selective about them (re: bloat and licensing).
This crate depends on:
- `nanorand`: A fast PRNG based on WyRand. It pulls in `getrandom` as a
portable source of entropy. I chose this over the more commonly used, but
heavyweight `rand` because it's significantly smaller and does just what I
need, and not much more.
- `num-traits`: A brilliant crate which makes it easier to operate on numbers
in generics; like using `Float` as a constraint on a generic type. Its
convenience outweighs its cost.
- [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand.
It pulls in `getrandom` as a portable source of entropy. I chose this over
the more commonly used, but heavyweight `rand` because it's significantly
smaller and does just what I need, and not much more.
- [`num-traits`](https://crates.io/crates/num-traits): A brilliant crate which
makes it easier to operate on numbers in generics; like using `Float` as a
constraint on a generic type. Its convenience outweighs its cost.
- [`serde`](https://crates.io/crates/serde): I think NCs are usually meant to
be shared across a network. That requires serialization/deserialization and
serde is *the* choice for that. It might be big, but it's efficient.
- [`serde_with`](https://crates.io/crates/serde_with): Because the inner
`Vector<T,N>` uses a const generic length, we use this to help derive
`Deserialize`.
## Alternative implementations
@@ -98,12 +149,12 @@ I had several design goals which the existing crates didn't satisfy:
rigamarole. Support `serde` traits by default.
4. Well documented. Well tested. This varies by the crates.
All of that said, those other implementations might work best for you. Here are
the ones I know of today:
All of that said, those other rust implementations might work best for you.
Here are the ones I know of today:
- TODO: list crates
- TODO: list crates
- TODO: list crates
- [netloc](https://github.com/dgtony/netloc)
- [violin](https://crates.io/crates/violin)
- [vivaldi](https://crates.io/crates/vivaldi)
## Other Algorithms
+66 -1
View File
@@ -45,6 +45,7 @@
use std::ops::{Add, Mul, Sub};
use nanorand::{Rng, WyRand};
use serde::{Deserialize, Serialize};
use crate::vector::Vector;
@@ -61,7 +62,7 @@ use crate::vector::Vector;
/// ## Generic Parameters
///
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
#[derive(Copy, Clone, Debug)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub(crate) struct HeightVector<const N: usize>(Vector<f32, N>, f32);
//
@@ -160,4 +161,68 @@ mod tests {
let a = HeightVector::<3>::from(([1.0, 2.0, 3.0], 4.0));
assert_approx_eq!(a.len(), 7.741_657, 0.00001);
}
#[test]
fn test_new() {
// new gives us a random unit length vec
let a = HeightVector::<3>::new();
assert_approx_eq!(a.len(), 1.0);
}
#[test]
fn test_default() {
// default() should give us a zero vec
let a = HeightVector::<3>::default();
assert_approx_eq!(a.len(), 0.0);
}
#[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);
}
#[test]
fn test_add() {
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);
}
#[test]
fn test_sub() {
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);
}
#[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);
}
#[test]
fn test_serde() {
// start with JSON, deserialize it
let s = "[{\"inner\":[1.0,2.0,3.0]},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
assert_approx_eq!(a.len(), 7.741_657, 0.001);
// serialize it into a new JSON string and make sure it matches the original
let t = serde_json::to_string(&a);
assert_eq!(t.as_ref().unwrap(), s);
}
}
+20 -2
View File
@@ -27,6 +27,8 @@
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::height_vector::HeightVector;
//
@@ -47,7 +49,7 @@ const MIN_ERROR: f32 = f32::EPSILON;
// **** Structs ****
//
#[derive(Debug)]
#[derive(Debug, Serialize, Deserialize)]
pub struct NetworkCoordinate<const N: usize> {
position: HeightVector<N>,
error: f32,
@@ -274,6 +276,22 @@ mod tests {
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
.sqrt();
println!("error = {error}");
assert!(error < 0.07);
assert!(error < 0.2);
}
#[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 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_eq!(a.error, 1.0);
assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
// serialize it into a new JSON string and make sure it matches the original
let t = serde_json::to_string(&a);
assert_eq!(t.as_ref().unwrap(), s);
}
}
+60 -17
View File
@@ -4,6 +4,8 @@
//! Vivaldi `HeightVector`.
use num_traits::Float;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use std::ops::{Add, Div, Index, Mul, Sub};
@@ -16,14 +18,24 @@ use std::ops::{Add, Div, Index, Mul, Sub};
/// ## Generic parameters:
/// - `T`: the type to use, must satisfy the `Float` trait (i.e. `f32` or `f64`)
/// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc)
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub(crate) struct Vector<T: Float, const N: usize>([T; N]);
#[serde_as]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Vector<T, const N: usize>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
#[serde_as(as = "[_; N]")]
inner: [T; N],
}
//
// **** Implementations ****
//
impl<T: Float, const N: usize> Vector<T, N> {
impl<T, const N: usize> Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
/// Create a new Zero vector.
pub(crate) fn new() -> Self {
Self::default()
@@ -32,8 +44,8 @@ impl<T: Float, const N: usize> Vector<T, N> {
/// Compute magnitude of vector.
pub(crate) fn len(&self) -> T {
(0..N)
.map(|i| self.0[i] * self.0[i])
.fold(T::zero(), |acc, x| acc + x)
.map(|i| self.inner[i] * self.inner[i])
.fold(T::zero(), |sum_sq, x| sum_sq + x)
.sqrt()
}
}
@@ -42,21 +54,32 @@ impl<T: Float, const N: usize> Vector<T, N> {
// **** Trait Implementations ****
//
impl<T: Float, const N: usize> Default for Vector<T, N> {
impl<T, const N: usize> Default for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
/// A default vector is all zeroes
fn default() -> Self {
Self([T::zero(); N])
Self {
inner: [T::zero(); N],
}
}
}
impl<T: Float, const N: usize> From<[T; N]> for Vector<T, N> {
impl<T, const N: usize> From<[T; N]> for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
/// Create a new Vector from an array of type T, N dimensions.
fn from(value: [T; N]) -> Self {
Self(value)
Self { inner: value }
}
}
impl<T: Float, const N: usize> Add for Vector<T, N> {
impl<T, const N: usize> Add for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
type Output = Self;
/// Vector+Vector addition.
@@ -69,7 +92,10 @@ impl<T: Float, const N: usize> Add for Vector<T, N> {
}
}
impl<T: Float, const N: usize> Sub for Vector<T, N> {
impl<T, const N: usize> Sub for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
type Output = Self;
/// Vector-Vector subtraction.
@@ -82,38 +108,47 @@ impl<T: Float, const N: usize> Sub for Vector<T, N> {
}
}
impl<T: Float, const N: usize> Mul<T> for Vector<T, N> {
impl<T, const N: usize> Mul<T> for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
type Output = Self;
/// Vector*scalar multiplication.
fn mul(self, rhs: T) -> Self::Output {
let mut ret = Self::Output::new();
for i in 0..N {
ret.0[i] = self.0[i] * rhs
ret.inner[i] = self.inner[i] * rhs
}
ret
}
}
impl<T: Float, const N: usize> Div<T> for Vector<T, N> {
impl<T, const N: usize> Div<T> for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
type Output = Self;
/// Vector/scalar division.
fn div(self, rhs: T) -> Self::Output {
let mut ret = Self::Output::new();
for i in 0..N {
ret.0[i] = self.0[i] / rhs
ret.inner[i] = self.inner[i] / rhs
}
ret
}
}
impl<T: Float, const N: usize> Index<usize> for Vector<T, N> {
impl<T, const N: usize> Index<usize> for Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
type Output = T;
/// Nicer indexing [] for read only references.
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
&self.inner[index]
}
}
@@ -183,4 +218,12 @@ mod tests {
let b = a * 10.0;
assert_approx_eq!(b.len(), 37.416_57, 0.0001);
}
#[test]
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!(s.is_ok());
}
}