From f29deb53351b13ebd6052a952b62ac938c43d029 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 2 Feb 2023 21:19:09 -0700 Subject: [PATCH] initial commit Includes an initial implementation, some tests, and some documentation. Signed-off-by: Stephen Waits --- .gitignore | 1 + Cargo.lock | 192 ++++++++++++++++++++++++++ Cargo.toml | 16 +++ LICENSE.txt | 21 +++ README.md | 116 ++++++++++++++++ src/height_vector.rs | 163 ++++++++++++++++++++++ src/lib.rs | 4 + src/network_coordinate.rs | 279 ++++++++++++++++++++++++++++++++++++++ src/vector.rs | 186 +++++++++++++++++++++++++ 9 files changed, 978 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 src/height_vector.rs create mode 100644 src/lib.rs create mode 100644 src/network_coordinate.rs create mode 100644 src/vector.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0752084 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,192 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "assert_approx_eq" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c07dab4369547dbe5114677b33fbbf724971019f3818172d59a97a61c774ffd" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66" + +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "vivaldi-nc" +version = "0.1.0" +dependencies = [ + "assert_approx_eq", + "nanorand", + "num-traits", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..a07b05c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "vivaldi-nc" +version = "0.1.0" +edition = "2021" +authors = ["Stephen Waits "] +description = "An implementation of Vivaldi network coordinates." +license = "MIT" +keywords = ["vivaldi", "network coordinates", "distributed", "p2p"] +categories = ["algorithms", "network-programming"] + +[dev-dependencies] +assert_approx_eq = "1.1.0" + +[dependencies] +nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]} +num-traits = "0.2.15" diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..975ab36 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Stephen Waits + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..fa570b3 --- /dev/null +++ b/README.md @@ -0,0 +1,116 @@ +# vivaldi-nc - Vivaldi Network Coordinates + +## Introduction + +Network Coordinates (NC) are a way to represent a node's position in a network's +latency space. Nodes with low latency (ping or round trip time) between each +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 +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 +nodes to understand estimated latency to any other node in the system. + +According to the [Vivaldi coordinates article on Wikipedia](https://en.wikipedia.org/wiki/Vivaldi_coordinates): + +> Vivaldi Network Coordinates establish a virtual positioning system that +> has a prime use in networking. The algorithm behind the system uses a +> distributed technique to estimate propagation times between peers in the +> network. +> +> Through this scheme, network topology awareness can be used to tune the +> network behaviour to more efficiently distribute data. For example, in a +> peer-to-peer network, more responsive identification and delivery of content +> can be achieved. In the Azureus application, Vivaldi is used to improve the +> performance of the distributed hash table that facilitates query matches. +> +> **Advantages** +> +> - Vivaldi is a fully distributed scheme, which achieves good scalability. +> - The Vivaldi algorithm is simple and easy to implement. +> +> **Drawbacks** +> +> - Vivaldi is based on the Euclidean distance model, which requires the +> predicted distances to obey the triangle inequality. However, there are +> many triangle inequality violations (TIVs) on the Internet. +> - Lack of security design, very easy for malicious nodes to conduct various +> attacks. + +*Citation*: Frank Dabek, Russ Cox, Frans Kaashoek, Robert Morris (2004). +"[Vivaldi: A Decentralized Network Coordinate System](http://conferences.sigcomm.org/sigcomm/2004/papers/p426-dabek111111.pdf)" (PDF). +Proc. of the annual conference of the Special Interest Group on Data Communication (SIGCOMM'04). + +### Addressing the Drawbacks + +Regarding the two drawbacks mentioned by the Wikiepedia article, quoted above: + +- **Euclidean distance**: The original paper proposed a height vector model + which is superior to the Euclidean distance. This crate implements the height + vector model. This provides reasonable accuracy and convergence while + remaining simple to implement. +- **Lack of security**: This is not addressed by this crate. Anyone sharing + network coordinates among nodes in a network should use appropriate + cryptography. + +## Usage + +### Getting Started + +### Features + +### Examples + +## Dependencies + +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. + +## Alternative implementations + +Several crates implemented Vivaldi NC before this one. So, why another? + +I had several design goals which the existing crates didn't satisfy: + +1. Provide the simplest interface possible. I just want to have some sort of + Coordinate struct and be able to update it, and then use it to estimate + round trip times (ie ping times). +2. Don't require the consumer to bring their own vector or linear algebra + library. The linear algebra required by this is extraordinarily simple. I + want it to just work without me needing to inject some large, mostly-unused + library. +3. Serializable/Deserializable by default. These things are useful for sending + across networks. The intent is to be able to do that without a bunch of + 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: + +- TODO: list crates +- TODO: list crates +- TODO: list crates + +## Other Algorithms + +Vivaldi is about the simplest distributed NC algorithm out there. That +simplicity combined with its reasonably good performance is a reason why it's +popular. + +But it's far from the only choice. Here are links to other NC algorithms: + +- TODO: list algorithms +- TODO: list algorithms +- TODO: list algorithms diff --git a/src/height_vector.rs b/src/height_vector.rs new file mode 100644 index 0000000..8f704e3 --- /dev/null +++ b/src/height_vector.rs @@ -0,0 +1,163 @@ +//! Implementation of a Vivaldi height vector or coordinate. +//! +//! A Vivaldi height vector consists of an N-dimensional Euclidean/Cartesian space vector and a +//! greater-than-zero height. This vector is intented to represent latency; hence the distance +//! between two Vivaldi coordinates should be close to the actual round-trip-time (i.e. ping time). +//! +//! The height component is included to help solve for the triangle inequality; the fact that three +//! points in the real world won't necessarily follow the triangle equality principle. In other +//! words, ping times aren't perfectly correlated to geographic distance. Thus, the height can be +//! considered a representation of a sort of "stem time" which is the time from a node to the core +//! internet; and the vectors can be considered locations on the core internet backbone where ping +//! times are more consistently correlated with geographic distance. +//! +//! Quoting the paper: +//! +//! > A height vector consists of a Euclidean coordinate augmented +//! > with a height. The Euclidean portion models a high-speed Internet +//! > core with latencies proportional to geographic distance, while the +//! > height models the time it takes packets to travel the access link +//! > from the node to the core. The cause of the access link latency may +//! > be queuing delay (as in the case of an oversubscribed cable line), +//! > low bandwidth (as in the case of DSL, cable modems, or telephone +//! > modems), or even the sheer length of the link (as in the case of +//! > long-distance fiber-optic cables). +//! > A packet sent from one node to another must travel the source +//! > node’s height, then travel in the Euclidean space, then travel the +//! > destination node’s height. Even if the two nodes have the same +//! > height, the distance between them is their Euclidean distance plus +//! > the two heights. This is the fundamental difference between height +//! > vectors and adding a dimension to the Euclidean space. Intuitively, +//! > packet transmission can only be done in the core, not above it. +//! > The height vector model is implemented by redefining the usual +//! > vector operations (note the + on the right hand side of the subtrac- +//! > tion equation): +//! +//! ```text +//! [x, xh] − [y, y] = [(x − y), xh + yh] +//! ∥∥∥[x, xh]∥∥∥ = ∥∥∥x∥∥∥ + xh +//! α × [x, xh] = [αx, αxh ] +//! ``` +//! +//! > Each node has a positive height element in its coordinates, so that +//! > its height can always be scaled up or down. + +use std::ops::{Add, Mul, Sub}; + +use nanorand::{Rng, WyRand}; + +use crate::vector::Vector; + +// +// **** Constants **** +// + +// +// **** Structs **** +// + +/// The `HeightVector` is a tuple containing an `N`-dimensional Cartesian vector and a `> 0` height. +/// +/// ## Generic Parameters +/// +/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector +#[derive(Copy, Clone, Debug)] +pub(crate) struct HeightVector(Vector, f32); + +// +// **** Implementations **** +// + +impl HeightVector { + /// A new height vector is a random unit vector plus a default height. + pub(crate) fn new() -> Self { + let mut rng = WyRand::new(); + let mut vec = [0.0; N]; + for i in vec.iter_mut().take(N) { + *i = rng.generate::(); + } + let height = rng.generate::(); + Self(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 + } + + /// A normalized Vivaldi height vector is just like a normalized vector: the vector scaled + /// by the inveerse of its length. + pub(crate) fn normalized(&self) -> Self { + let len = self.len(); + if len < f32::EPSILON { + // if we have a bad vector, generate a new random vector + Self::new() + } else { + // otherwise scale it by the inverse of its magnitude like any normal vector + Self(self.0 / len, self.1 / len) + } + } +} + +// +// **** Trait Implementations **** +// + +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()) + } +} + +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 { + Self(Vector::::from(value.0), value.1) + } +} + +impl Add for HeightVector { + type Output = Self; + + /// Add two Vivaldi height vectors. + fn add(self, rhs: Self) -> Self::Output { + Self(self.0 + rhs.0, self.1 + rhs.1) + } +} + +impl Sub for HeightVector { + type Output = Self; + + /// 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 { + Self(self.0 - rhs.0, self.1 + rhs.1) + } +} + +impl Mul for HeightVector { + type Output = Self; + + /// Multiply a Vivaldi height vector by a scalar. Works the same as normal vector scaling. + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs, self.1 * rhs) + } +} + +// +// **** Tests **** +// +#[cfg(test)] +mod tests { + use assert_approx_eq::assert_approx_eq; + + use super::*; + + #[test] + fn test_len() { + let a = HeightVector::<3>::from(([1.0, 2.0, 3.0], 4.0)); + assert_approx_eq!(a.len(), 7.741_657, 0.00001); + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f6a7d25 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +mod height_vector; +mod vector; + +pub mod network_coordinate; diff --git a/src/network_coordinate.rs b/src/network_coordinate.rs new file mode 100644 index 0000000..421c33f --- /dev/null +++ b/src/network_coordinate.rs @@ -0,0 +1,279 @@ +//! A Vivaldi [`NetworkCoordinate`] +//! +//! This is an implementation of Vivaldi NCs per the original paper. It implements the following +//! alogirthm (quoting from paper): +//! +//! ```text +//! // Incorporate new information: node j has been +//! // measured to be rtt ms away, has coordinates x j, +//! // and an error estimate of e j . +//! // +//! // Our own coordinates and error estimate are xi and ei. +//! // +//! // The constants ce and cc are tuning parameters. +//! +//! vivaldi(rtt, xj, ej) +//! // Sample weight balances local and remote error. (1) +//! w = ei /(ei + ej) +//! // Compute relative error of this sample. (2) +//! es = ∣∣∣‖xi − xj‖ − rtt∣∣∣/rtt +//! // Update weighted moving average of local error. (3) +//! ei = es × ce × w + ei × (1 − ce × w) +//! // Update local coordinates. (4) +//! δ = cc × w +//! xi = xi + δ × (rtt − ‖xi − xj ‖) × u(xi − xj) +//! ``` +//! + +use std::time::Duration; + +use crate::height_vector::HeightVector; + +// +// **** Constants **** +// + +// Vivaldi tuning parameters +const C_ERROR: f32 = 0.25; +const C_DELTA: f32 = 0.25; + +// initial error value +const DEFAULT_ERROR: f32 = 1000.0; + +// error should always be greater than zero +const MIN_ERROR: f32 = f32::EPSILON; + +// +// **** Structs **** +// + +#[derive(Debug)] +pub struct NetworkCoordinate { + position: HeightVector, + error: f32, +} + +// +// **** Implementations **** +// + +impl NetworkCoordinate { + /// Creates a new random [`NetworkCoordinate`] + /// + /// # Example + /// + /// ``` + /// use vivaldi_nc::network_coordinate::NetworkCoordinate; + /// + /// // create a new 3-dimensional random NC + /// let a: NetworkCoordinate<3> = NetworkCoordinate::new(); + /// + /// // print the NC + /// println!("Our new NC is: {:#?}", a); + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Given another Vivaldi [`NetworkCoordinate`], estimate the round trip time (ie ping) between them. + /// + /// This is done by computing the height vector distance between between the two coordinates. + /// Vivaldi uses this distance as a representation of estimated round trip time. + /// + /// # Parameters + /// + /// - `rhs`: the other coordinate + /// + /// # Returns + /// + /// - the estimated round trip time as a `Duration` + /// + /// # Example + /// + /// ``` + /// use vivaldi_nc::network_coordinate::NetworkCoordinate; + /// + /// // create some 2-dimensional NCs for the sake of this example. These will just be random + /// // NCs. In a real usecase these would have meaningful values. + /// let a: NetworkCoordinate<2> = NetworkCoordinate::new(); + /// let b: NetworkCoordinate<2> = NetworkCoordinate::new(); + /// + /// // get the estimated RTT, convert to milliseconds, and print + /// println!("Estimated RTT: {}", a.estimated_rtt(&b).as_millis()); + /// ``` + /// + 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) + } + + /// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round + /// trip time (aka distance) between us. + /// + /// # Parameters + /// + /// - `rhs`: the other coordinate + /// - `rtt`: the measured round trip time between `self` and `rhs` + /// + /// # Returns + /// + /// - a reference to `self` + /// + /// # Example + /// + /// ``` + /// use std::time::Duration; + /// use vivaldi_nc::network_coordinate::NetworkCoordinate; + /// + /// // We always have our own NC: + /// let mut local: NetworkCoordinate<2> = NetworkCoordinate::new(); + /// + /// // Assume we received a NC from a remote node: + /// let remote: NetworkCoordinate<2> = NetworkCoordinate::new(); + /// + /// // And we measured the RTT between us and the remote node: + /// let rtt = Duration::from_millis(100); + /// + /// // Now we can update our NC to adjust our position relative to the remote node: + /// local.update(&remote, rtt); + /// ``` + pub fn update(&mut self, rhs: &Self, rtt: Duration) -> &Self { + // rtt needs to be positive + if rtt.as_secs_f32() < 0.0 { + return self; + } + + // convert Durations into f32s as fractional milliseconds for convenience + let rtt_ms = rtt.as_secs_f32() * 1000.0; + let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f32() * 1000.0; + + // Sample weight balances local and remote error. (1) + // w = ei /(ei + ej ) + let w = self.error / (self.error + rhs.error); + + // Compute relative error of this sample. (2) + // es = ∣∣∣‖xi − xj‖ − rtt∣∣∣/rtt + let error = rtt_ms - rtt_estimated_ms; + let es = error.abs() / rtt_ms; + + // Update weighted moving average of local error. (3) + // ei = es × ce × w + ei × (1 − ce × w) + self.error = (es * C_ERROR * w + self.error * (1.0 - C_ERROR * w)).max(MIN_ERROR); + + // Update local coordinates. (4) + // δ = 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; + + // return reference to updated self + self + } +} + +// +// **** Trait Implementations **** +// + +impl Default for NetworkCoordinate { + fn default() -> Self { + Self { + position: HeightVector::::new(), + error: DEFAULT_ERROR, + } + } +} + +// +// **** Tests **** +// +#[cfg(test)] +mod tests { + use assert_approx_eq::assert_approx_eq; + + use super::*; + + #[test] + fn test_convergence() { + let mut a = NetworkCoordinate::<3>::new(); + let mut b = NetworkCoordinate::<3>::new(); + let t = Duration::from_millis(250); + (0..20).for_each(|_| { + a.update(&b, t); + b.update(&a, t); + }); + let rtt = a.estimated_rtt(&b); + assert_approx_eq!(rtt.as_secs_f32() * 1000.0, 250.0, 1.0); + } + + #[test] + fn test_mini_network() { + // define a little network with these nodes: + // + // slc has 80ms stem time to core entry Seattle + // nyc has 30ms stem time to core entry Virginia + // lax has 15ms stem time to core entry Los Angeles + // mad has 60ms stem time to core entry London + // + // we'll assume that traffic in the core moves at 50% the speed of light + // + // that gives us this grid of RTTs (in ms) for the core: + // + // | | Seattle | Virgina | Los Angeles | London | + // |-------------|---------|---------|-------------|--------| + // | Seattle | - | 52 | 20 | 102 | + // | Virginia | | - | 50 | 78 | + // | Los Angeles | | | - | 116 | + // | London | | | | - | + // + // Which gives us these routes (plus their reverse) and times (ms): + // + // SLC -> Seattle -> Virginia -> NYC = 80 + 52 + 30 = 162 + // SLC -> Seattle -> Los Angeles -> LAX = 80 + 20 + 15 = 115 + // SLC -> Seattle -> Londong -> MAD = 80 + 102 + 60 = 242 + // NYC -> Virginia -> Los Angeles -> LAX = 30 + 50 + 15 = 95 + // NYC -> Virginia -> London -> MAD = 30 + 78 + 60 = 168 + // LAX -> Los Angeles -> London -> MAD = 15 + 116 + 60 = 192 + + // create the NCs for each endpoint + let mut slc = NetworkCoordinate::<2>::new(); + let mut nyc = NetworkCoordinate::<2>::new(); + let mut lax = NetworkCoordinate::<2>::new(); + let mut mad = NetworkCoordinate::<2>::new(); + + // 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(); + println!("error = {error}"); + assert_eq!(error, 2_000.0); + + // iterate plenty of times to converge and minimize error + (0..50).for_each(|_| { + slc.update(&nyc, Duration::from_millis(162)); + nyc.update(&slc, Duration::from_millis(162)); + + slc.update(&lax, Duration::from_millis(115)); + lax.update(&slc, Duration::from_millis(115)); + + slc.update(&mad, Duration::from_millis(242)); + mad.update(&slc, Duration::from_millis(242)); + + nyc.update(&lax, Duration::from_millis(95)); + lax.update(&nyc, Duration::from_millis(95)); + + nyc.update(&mad, Duration::from_millis(168)); + mad.update(&nyc, Duration::from_millis(168)); + + lax.update(&mad, Duration::from_millis(192)); + mad.update(&lax, Duration::from_millis(192)); + }); + + // compute and test the root mean squared error + let error = + (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); + } +} diff --git a/src/vector.rs b/src/vector.rs new file mode 100644 index 0000000..cfee924 --- /dev/null +++ b/src/vector.rs @@ -0,0 +1,186 @@ +//! A minimal Euclidean/Cartesian vector implementation. +//! +//! This is the simplest possible implementation of a vector to support the needs of a +//! Vivaldi `HeightVector`. + +use num_traits::Float; + +use std::ops::{Add, Div, Index, Mul, Sub}; + +// +// **** Structs **** +// + +/// A minimal Euclidean/Cartesian vector implementation +/// +/// ## 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; N]); + +// +// **** Implementations **** +// + +impl Vector { + /// Create a new Zero vector. + pub(crate) fn new() -> Self { + Self::default() + } + + /// 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) + .sqrt() + } +} + +// +// **** Trait Implementations **** +// + +impl Default for Vector { + /// A default vector is all zeroes + fn default() -> Self { + Self([T::zero(); N]) + } +} + +impl From<[T; N]> for Vector { + /// Create a new Vector from an array of type T, N dimensions. + fn from(value: [T; N]) -> Self { + Self(value) + } +} + +impl Add for Vector { + type Output = Self; + + /// Vector+Vector addition. + fn add(self, rhs: Self) -> Self::Output { + let mut ret = [T::zero(); N]; //Self::Output::new(); + for i in 0..N { + ret[i] = self[i] + rhs[i]; + } + Self::Output::from(ret) + } +} + +impl Sub for Vector { + type Output = Self; + + /// Vector-Vector subtraction. + fn sub(self, rhs: Self) -> Self::Output { + let mut ret = [T::zero(); N]; //Self::Output::new(); + for i in 0..N { + ret[i] = self[i] - rhs[i]; + } + Self::Output::from(ret) + } +} + +impl Mul for Vector { + 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 + } +} + +impl Div for Vector { + 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 + } +} + +impl Index for Vector { + type Output = T; + + /// Nicer indexing [] for read only references. + fn index(&self, index: usize) -> &Self::Output { + &self.0[index] + } +} + +// +// **** Tests **** +// + +#[cfg(test)] +mod tests { + use super::*; + + use assert_approx_eq::assert_approx_eq; + + #[test] + fn test_default_new() { + let a = Vector::::default(); + let b = Vector::::new(); + let c = Vector::::from([0.0; 3]); + assert_eq!(a, b); + assert_eq!(b, c); + } + + #[test] + fn test_add() { + let a = Vector::::from([1.0, 2.0, 3.0]); + let b = Vector::::from([3.0, 2.0, 1.0]); + let c = a + b; + assert_eq!(c, Vector::::from([4.0, 4.0, 4.0])); + } + + #[test] + fn test_sub() { + let a = Vector::::from([1.0, 2.0, 3.0]); + let b = Vector::::from([3.0, 2.0, 1.0]); + let c = a - b; + assert_eq!(c, Vector::::from([-2.0, 0.0, 2.0])); + } + + #[test] + fn test_mul_scalar() { + let a = Vector::::from([1.0, 2.0, 3.0]); + let b = 10.0; + let c = a * b; + assert_eq!(c, Vector::::from([10.0, 20.0, 30.0])); + } + + #[test] + fn test_div_scalar() { + let a = Vector::::from([1.0, 2.0, 3.0]); + let b = 10.0; + let c = a / b; + assert_eq!(c, Vector::::from([0.1, 0.2, 0.3])); + } + + #[test] + fn test_index() { + let a = Vector::::from([1.0, 2.0, 3.0]); + assert_eq!(a[0], 1.0); + assert_eq!(a[1], 2.0); + assert_eq!(a[2], 3.0); + } + #[test] + fn test_length() { + let a = Vector::::from([1.0, 2.0, 3.0]); + assert_approx_eq!(a.len(), 3.741_65, 0.0001); + + let b = a * 10.0; + assert_approx_eq!(b.len(), 37.416_57, 0.0001); + } +}