initial commit

Includes an initial implementation, some tests, and some documentation.

Signed-off-by: Stephen Waits <steve@waits.net>
This commit is contained in:
swaits committed 2023-02-02 21:19:09 -07:00
commit f29deb5335
9 files changed
+978

No files matched your search

+1
View File
@@ -0,0 +1 @@
/target
Generated
+192
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+116
View File
@@ -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
+163
View File
@@ -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);
}
}
+4
View File
@@ -0,0 +1,4 @@
mod height_vector;
mod vector;
pub mod network_coordinate;
+279
View File
@@ -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
View File
@@ -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);
}
}