initial commit
Includes an initial implementation, some tests, and some documentation. Signed-off-by: Stephen Waits <steve@waits.net>
This commit is contained in:
9 files changed
+978
No files matched your search
@@ -0,0 +1 @@
|
||||
/target
|
||||
Generated
+192
@@ -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"
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "vivaldi-nc"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
authors = ["Stephen Waits <steve@waits.net>"]
|
||||
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"
|
||||
+21
@@ -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.
|
||||
@@ -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
|
||||
@@ -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<const N: usize>(Vector<f32, N>, f32);
|
||||
|
||||
//
|
||||
// **** Implementations ****
|
||||
//
|
||||
|
||||
impl<const N: usize> HeightVector<N> {
|
||||
/// 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::<f32>();
|
||||
}
|
||||
let height = rng.generate::<f32>();
|
||||
Self(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
|
||||
}
|
||||
|
||||
/// 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<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())
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
Self(Vector::<f32, N>::from(value.0), value.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Add for HeightVector<N> {
|
||||
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<const N: usize> Sub for HeightVector<N> {
|
||||
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<const N: usize> Mul<f32> for HeightVector<N> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod height_vector;
|
||||
mod vector;
|
||||
|
||||
pub mod network_coordinate;
|
||||
@@ -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<const N: usize> {
|
||||
position: HeightVector<N>,
|
||||
error: f32,
|
||||
}
|
||||
|
||||
//
|
||||
// **** Implementations ****
|
||||
//
|
||||
|
||||
impl<const N: usize> NetworkCoordinate<N> {
|
||||
/// 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<const N: usize> Default for NetworkCoordinate<N> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
position: HeightVector::<N>::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);
|
||||
}
|
||||
}
|
||||
+186
@@ -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: Float, const N: usize>([T; N]);
|
||||
|
||||
//
|
||||
// **** Implementations ****
|
||||
//
|
||||
|
||||
impl<T: Float, const N: usize> Vector<T, N> {
|
||||
/// 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<T: Float, const N: usize> Default for Vector<T, N> {
|
||||
/// A default vector is all zeroes
|
||||
fn default() -> Self {
|
||||
Self([T::zero(); N])
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Float, const N: usize> From<[T; N]> for Vector<T, N> {
|
||||
/// Create a new Vector from an array of type T, N dimensions.
|
||||
fn from(value: [T; N]) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Float, const N: usize> Add for Vector<T, N> {
|
||||
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<T: Float, const N: usize> Sub for Vector<T, N> {
|
||||
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<T: Float, const N: usize> Mul<T> for Vector<T, N> {
|
||||
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<T: Float, const N: usize> Div<T> for Vector<T, N> {
|
||||
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<T: Float, const N: usize> Index<usize> for Vector<T, N> {
|
||||
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::<f32, 3>::default();
|
||||
let b = Vector::<f32, 3>::new();
|
||||
let c = Vector::<f32, 3>::from([0.0; 3]);
|
||||
assert_eq!(a, b);
|
||||
assert_eq!(b, c);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
||||
let b = Vector::<f32, 3>::from([3.0, 2.0, 1.0]);
|
||||
let c = a + b;
|
||||
assert_eq!(c, Vector::<f32, 3>::from([4.0, 4.0, 4.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sub() {
|
||||
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
||||
let b = Vector::<f32, 3>::from([3.0, 2.0, 1.0]);
|
||||
let c = a - b;
|
||||
assert_eq!(c, Vector::<f32, 3>::from([-2.0, 0.0, 2.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mul_scalar() {
|
||||
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
||||
let b = 10.0;
|
||||
let c = a * b;
|
||||
assert_eq!(c, Vector::<f32, 3>::from([10.0, 20.0, 30.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_div_scalar() {
|
||||
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
|
||||
let b = 10.0;
|
||||
let c = a / b;
|
||||
assert_eq!(c, Vector::<f32, 3>::from([0.1, 0.2, 0.3]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_index() {
|
||||
let a = Vector::<f32, 3>::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::<f32, 3>::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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user