17 Commits
Author SHA1 Message Date
swaits 634064161f bump crate version
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 21:44:47 -07:00
swaits 0d9b623f9d test: add test for NetworkCoordinate::estimate_rtt
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 21:43:35 -07:00
swaits 828efdda01 misc: formatting
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 21:37:29 -07:00
swaits 3cf2f27569 test: add property tests to HeightVector
Which also helped me fix a bug (using is_sign_negative() when we actually are okay with -0.0).

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 21:37:21 -07:00
swaits 2b8fc16537 feat: add PlanetLab example
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 20:40:06 -07:00
swaits 8f1100d64d fix: add numeric stability
Made `HeightVector` numerically safe; while `Vector` remains numerically unsafe (i.e. it can divide by 0, NaN, Inf, etc).

When HeightVector runs into a problem, it just re-randomizes itself. Since the algorithm converges relatively quickly, this seems to work fine.

Also fixed a bug so that now height will always start as a positive number (and stay that way, else get re-randomized).

Added safety to `NetworkCoordinate` which now checks that its error didn’t pull a `HeightVector` into a negative height; in which case it also re-randomizes.

Clarified the `random` construction, tweaked the DEFAULT_ERROR constant, and updated tests accordingly.

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 20:25:10 -07:00
swaits 973365a849 test: add proptest, make vec mul/div scalar safer
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 16:29:01 -07:00
swaits e2d14bcc0d doc: add link to ticket tracker
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 10:48:28 -07:00
swaits 25e0076c8d doc: add link to docs.rs in README
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 10:43:36 -07:00
swaits aba483c9e1 bump crate version
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 10:40:57 -07:00
swaits e9febc6faa clean up export, add module level docs
- public NetworkCoordinate so it’s easer to just `use vivaldi_nc::NetworkCoordinate`
- add module level docs and examples in lib.rs
- cleanup README

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 10:39:59 -07:00
swaits c83a4e654b update README, bump crate version
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 09:48:01 -07:00
swaits 2fad2e18b5 feat: add serde
Adds serialization/deserialization support via serde and serde_with.

Also:

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

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 09:42:46 -07:00
swaits 3415347072 remove Cargo.lock and add to .gitignore
Because this is a library, Cargo.lock should not be committed.

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-04 09:37:40 -07:00
swaits 5611618126 prep Cargo.toml for publication, add badges
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-02 22:33:15 -07:00
swaits c2399c5b8f add build badge to readme
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-02 21:56:18 -07:00
swaits f35680738c Add build script for git.sr.ht
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-02 21:48:35 -07:00
15 changed files with 268 additions and 1129 deletions

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
image: archlinux
packages:
- rustup
sources:
- https://git.sr.ht/~swaits/vivaldi-nc
tasks:
- setup: |
rustup toolchain install nightly stable
cd vivaldi-nc/
rustup run stable cargo fetch
- stable: |
rustup default stable
cd vivaldi-nc/
cargo build --verbose
cargo test --verbose
- nightly: |
rustup default nightly
cd vivaldi-nc/
cargo build --verbose ||:
cargo test --verbose ||:
- docs: |
cd vivaldi-nc/
rustup run stable cargo doc --no-deps
rustup run nightly cargo doc --no-deps ||:
-2
View File
@@ -1,4 +1,2 @@
/flamegraph.svg
/target
Cargo.lock
/tarpaulin-report.html
-142
View File
@@ -1,142 +0,0 @@
# Changelog
All notable changes to this project will be documented in this file.
## [0.6.12] - 2023-11-01
### Miscellaneous Tasks
- Update dependencies
## [0.6.11]
### Miscellaneous Tasks
- Finally add CHANGELOG.md
## [0.6.4]
### Miscellaneous Tasks
- Add MSRV (1.60)
- Upgrade dependencies
## [0.6.3]
### Styling
- Format Cargo.toml
- More Cargo.toml formatting
- Move clippy allows to specific lines of code
### Testing
- Add (and fix) more deny lints
- Add audit, deny, outdated, and udeps to CI
- Fix cargo audit advisory db location
- Make accepted licenses explicit
## [0.6.2]
### Bug Fixes
- Allow clippy lints on specific tests
### Refactor
- Change std::time::Duration to std::core::Duration
- Change unwrap()s to expects() and deny in clippy
- Clean up by using cfg-if where possible
### Testing
- Use prop_assert* macros where appropriate
### Dev
- Add husky for pre-commit hook generation
## [0.6.1]
### Refactor
- Switch to array-init
## [0.6.0]
### Testing
- Add more stuff to CI
- Add examples to CI
- Get to 100% coverage, clean up cargo features
## [0.5.4]
### Performance
- Add benchmark, optimize
## [0.5.3]
### Documentation
- Updates to NetworkCoordinate
## [0.5.2]
### Features
- F32/f64 cargo features added
## [0.5.1]
### Documentation
- Udpate crate description
- Minor updates re: type aliases, crate to 0.5.1
## [0.5.0]
### Features
- Improve ergonomics for serialization format
- Type aliases for 2D/3D NetworkCoordinates
## [0.4.1]
### Bug Fixes
- Exclude examples from crate
## [0.4.0]
### Bug Fixes
- Add numeric stability
### Features
- Add PlanetLab example
### Testing
- Add proptest, make vec mul/div scalar safer
- Add property tests to HeightVector
- Add test for NetworkCoordinate::estimate_rtt
### Misc
- Formatting
## [0.3.1]
### Documentation
- Add link to docs.rs in README
- Add link to ticket tracker
### Features
- Add serde
<!-- generated by git-cliff -->
+10 -37
View File
@@ -1,51 +1,24 @@
[package]
name = "vivaldi-nc"
version = "0.6.13"
version = "0.4.0"
edition = "2021"
authors = ["Stephen Waits <steve@waits.net>"]
description = "Vivaldi network coordinates for fast, distributed latency estimates in multinode networks, with a clean and simple interface."
description = "Vivaldi network coordinates with a simple interface."
license = "MIT"
keywords = ["vivaldi", "network", "coordinates", "distributed", "p2p"]
categories = ["algorithms", "network-programming"]
documentation = "https://docs.rs/vivaldi-nc/"
readme = "README.md"
homepage = "https://git.swaits.com/swaits/vivaldi-nc"
repository = "https://git.swaits.com/swaits/vivaldi-nc"
exclude = ["examples/*"]
rust-version = "1.65"
[features]
default = []
f32 = []
[profile.bench]
debug = true
[profile.release]
debug = true
[[bench]]
name = "million-updates"
harness = false
homepage = "https://sr.ht/~swaits/vivaldi-nc/"
repository = "https://git.sr.ht/~swaits/vivaldi-nc"
[dev-dependencies]
assert_approx_eq = "1.1.0"
criterion = "0.5.1"
cargo-husky = { version = "1.5.0", features = [
"run-for-all",
"precommit-hook",
"run-cargo-test",
"run-cargo-check",
"run-cargo-clippy",
"run-cargo-fmt",
] }
proptest = "1.4.0"
serde_json = "1.0.114"
proptest = "1.0.0"
serde_json = "1.0.91"
[dependencies]
array-init = "2.1.0"
cfg-if = "1.0.0"
num-traits = "0.2.18"
rand = "0.8.5"
serde = { version = "1.0.197", features = ["serde_derive"] }
serde_with = "3.6.1"
nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]}
num-traits = "0.2.15"
serde = {version = "1.0.152", features = ["serde_derive"]}
serde_with = "2.2.0"
+42 -97
View File
@@ -1,22 +1,6 @@
# vivaldi-nc - Vivaldi Network Coordinates
![Rust Version](https://img.shields.io/badge/rustc-1.60+-blue.svg) [![docs.rs](https://img.shields.io/docsrs/vivaldi-nc)](https://docs.rs/vivaldi-nc/) [![CI](https://git.swaits.com/swaits/vivaldi-nc/actions/workflows/ci.yml/badge.svg)](https://git.swaits.com/swaits/vivaldi-nc/actions) [![Crates.io](https://img.shields.io/crates/v/vivaldi-nc)](https://crates.io/crates/vivaldi-nc) ![Crates.io](https://img.shields.io/crates/d/vivaldi-nc) ![Crates.io](https://img.shields.io/crates/l/vivaldi-nc)
<!--toc:start-->
- [vivaldi-nc - Vivaldi Network Coordinates](#vivaldi-nc-vivaldi-network-coordinates)
- [Introduction](#introduction)
- [Addressing the Drawbacks](#addressing-the-drawbacks)
- [Usage](#usage)
- [Getting Started](#getting-started)
- [Cargo Features](#cargo-features)
- [Examples](#examples)
- [Dependencies](#dependencies)
- [Design Goals & Alternatives](#design-goals-alternatives)
- [Other Vivaldi NC Implementations](#other-vivaldi-nc-implementations)
- [Other NC Algorithms](#other-nc-algorithms)
- [Getting Help or Contributing](#getting-help-or-contributing)
- [License](#license)
<!--toc:end-->
[![docs.rs](https://img.shields.io/docsrs/vivaldi-nc)](https://docs.rs/vivaldi-nc/) [![builds.sr.ht status](https://builds.sr.ht/~swaits/vivaldi-nc.svg)](https://builds.sr.ht/~swaits/vivaldi-nc?) [![Crates.io](https://img.shields.io/crates/v/vivaldi-nc)](https://crates.io/crates/vivaldi-nc) ![Crates.io](https://img.shields.io/crates/d/vivaldi-nc) ![Crates.io](https://img.shields.io/crates/l/vivaldi-nc)
## Introduction
@@ -38,20 +22,20 @@ According to the [Vivaldi coordinates article on Wikipedia](https://en.wikipedia
> 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.
>
> 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.
@@ -126,61 +110,30 @@ and compact binary formats like [bincode](https://crates.io/crates/bincode) and
See the [module documentation](https://docs.rs/vivaldi-nc) for more detailed
usage examples.
### Cargo Features
By default, the internal data structures and operations all use `f64`, which is
slightly faster on modern architectures. If you want to use `f32` instead, you
can enable it as a cargo feature:
```toml
[dependencies]
vivaldi-nc = { version = "(version)", features = ["f32"] }
```
### Examples
The repository includes an example which loads a 490 node N-to-N latency sample
from PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean
error is reached. The output is a JSON array of elements which contain the NC's
position, its height (or stem latency estimate), and its estimation of error
(lower is better) that look like this:
Run an example which loads a n-to-n latency sample from PlanetLab and iterates
on `NetworkCoordinate`s until some low-enough mean error is reached. The output is a JSON array of elements that look like this:
```json
{
"position": [
5.563593,
-2.332495,
7.3957834
],
"height": 55.56651,
"error": 3.241348
}
{
"position": [
{
"inner": [ # Euclidean position (3D in this case):
9.249476, # first dimension; "x"
23.747225, # second dimension; "x"
-6.473008 # third dimension; "x"
]
},
72.10599 # Height component (estimate of stem latency in milliseconds)
],
"error": 2.9166403 # Estimated error (lower is better)
},
```
Adding some detail to these three fields:
- `position`: Estimated position of this node (or `NetworkCoordinate`) on the
network core. Think of this like the node's endpoint into the Internet
backbone. This is a cartesian coordinate in a multi-dimensional latency space
(in this case 3D), measured in milliseconds.
- `height`: Estimated stem time, which is the time from the node itself to the
backbone. So, for example this might represent the latency between your
node's actual location, like your home, and the point at which it gets into
the highest-speed, core part of the Internet. Height helps adjust for
[triangle inequality violations](https://en.wikipedia.org/wiki/Triangle_inequality),
which are common on the Internet.
- `error`: Estimated error of the current `position`, and `height`. Lower is
better.
To run the example, first clone the repository locally:
To run the example:
```bash
git clone https://git.swaits.com/swaits/vivaldi-nc
```
Then `cd` into the local clone and run the example:
```bash
cd vivaldi-nc
cargo run --example planetlab
```
@@ -190,11 +143,10 @@ 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:
- [`rand`](https://crates.io/crates/rand): I started with `nanorand`; however,
this is faster because of `rand::thread_rng()`; whereas with `nanorand` we
ended up needing to reimplement that same functionality through something
like `lazy_static!` plus `Mutex`, which is not worth it for a few percentage
points of performance.
- [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand.
It pulls in `getrandom` as a portable source of entropy. I chose this over
the more commonly used, but heavyweight `rand` because it's significantly
smaller and does just what I need, and not much more.
- [`num-traits`](https://crates.io/crates/num-traits): A brilliant crate which
makes it easier to operate on numbers in generics; like using `Float` as a
constraint on a generic type. Its convenience outweighs its cost.
@@ -204,19 +156,12 @@ This crate depends on:
- [`serde_with`](https://crates.io/crates/serde_with): Because the inner
`Vector<T,N>` uses a const generic length, we use this to help derive
`Deserialize`.
- [`array-init`](https://crates.io/crates/array-init): Makes the array
operations for the internal `Vector` so much nicer. Once we have
`feature(array_zip)` stabilized, we could use that instead. Until then, this
works well with zero performance impact.
- [`cfg-if`](https://crates.io/crates/cfg-if): Compile-time only macro which
makes using `cfg` parameters much nicer in code.
## Design Goals & Alternatives
Several crates implemented Vivaldi NC before this one. So, why another?
I had several design goals which the existing crates didn't satisfy, in order
of priority:
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
@@ -228,10 +173,7 @@ of priority:
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 in the existing crates.
5. Performance. While it's a low priority, it should be reasonable. The library
currently does about 10,000 `NetworkCoordinate::update()` calls per
millisecond on my Macbook Pro (Intel i7-9750H (12) @ 2.60GHz).
4. Well documented. Well tested. This varies by the crates.
### Other Vivaldi NC Implementations
@@ -258,12 +200,15 @@ find many more.
## Getting Help or Contributing
To get help, discuss this crate, or report a bug, [open an
issue](https://git.swaits.com/swaits/vivaldi-nc/issues).
To get help or discuss this crate, [submit a
ticket](https://todo.sr.ht/~swaits/vivaldi-nc) or post on
[`vivaldi-nc-discuss@`](https://lists.sr.ht/~swaits/vivaldi-nc-discuss).
If you want to contribute code enhancements or fixes directly, fork the
repository on [git.swaits.com](https://git.swaits.com/swaits/vivaldi-nc) and
open a pull request.
Discussion related to development or patch submissions should go to
[`vivaldi-nc-devel@`](https://lists.sr.ht/~swaits/vivaldi-nc-devel).
Patchsets should use [git-send-email](https://git-send-email.io/) or the [sr.ht
UI](https://git.sr.ht/~swaits/vivaldi-nc/send-email) (easiest IMO).
Patch submitters implicitly agree that all contributions they submit fall under
the MIT license.
@@ -271,19 +216,19 @@ the MIT license.
## License
> 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
-52
View File
@@ -1,52 +0,0 @@
use core::time::Duration;
use criterion::{criterion_group, criterion_main, Criterion};
use vivaldi_nc::{NetworkCoordinate2D, NetworkCoordinate3D};
const NUM_NODES: usize = 1_000;
fn million_vivaldi_2d_updates() {
// create NUM_NODES NCs
let mut nc: Vec<NetworkCoordinate2D> =
(0..NUM_NODES).map(|_| NetworkCoordinate2D::new()).collect();
// loop, update each item for every other item
for i in 0..NUM_NODES {
let nc_i = nc[i].clone();
for (j, nc_j) in nc.iter_mut().enumerate().take(NUM_NODES) {
if i == j {
continue;
}
let rtt = if i > j { i - j } else { j - i };
// let nc_j = nc[j].clone();
nc_j.update(&nc_i, Duration::from_millis(rtt.try_into().unwrap()));
}
}
}
fn million_vivaldi_3d_updates() {
// create NUM_NODES NCs
let mut nc: Vec<NetworkCoordinate3D> =
(0..NUM_NODES).map(|_| NetworkCoordinate3D::new()).collect();
// loop, update each item for every other item
for i in 0..NUM_NODES {
let nc_i = nc[i].clone();
for (j, nc_j) in nc.iter_mut().enumerate().take(NUM_NODES) {
if i == j {
continue;
}
let rtt = if i > j { i - j } else { j - i };
// let nc_j = nc[j].clone();
nc_j.update(&nc_i, Duration::from_millis(rtt.try_into().unwrap()));
}
}
}
fn run_benchmarks(c: &mut Criterion) {
c.bench_function("million 2D updates", |b| b.iter(million_vivaldi_2d_updates));
c.bench_function("million 3D updates", |b| b.iter(million_vivaldi_3d_updates));
}
criterion_group!(benches, run_benchmarks);
criterion_main!(benches);
-83
View File
@@ -1,83 +0,0 @@
# git-cliff ~ default configuration file
# https://git-cliff.org/docs/configuration
#
# Lines starting with "#" are comments.
# Configuration options are organized into tables and keys.
# See documentation for more information on available options.
[changelog]
# changelog header
header = """
# Changelog\n
All notable changes to this project will be documented in this file.\n
"""
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# postprocessors
postprocessors = [
# { pattern = '<REPO>', replace = "https://github.com/orhun/git-cliff" }, # replace repository URL
]
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = true
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](<REPO>/issues/${2}))"}, # replace issue numbers
]
# regex for parsing and grouping commits
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^doc", group = "Documentation" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactor" },
{ message = "^style", group = "Styling" },
{ message = "^test", group = "Testing" },
{ message = "^chore\\(release\\): prepare for", skip = true },
{ message = "^chore\\(deps\\)", skip = true },
{ message = "^chore\\(pr\\)", skip = true },
{ message = "^chore\\(pull\\)", skip = true },
{ message = "^chore|ci", group = "Miscellaneous Tasks" },
{ body = ".*security", group = "Security" },
{ message = "^revert", group = "Revert" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = "v[0-9].*"
# regex for skipping tags
skip_tags = "v0.1.0-beta.1"
# regex for ignoring tags
ignore_tags = ""
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "oldest"
# limit the number of commits included in the changelog.
# limit_commits = 42
-270
View File
@@ -1,270 +0,0 @@
# This template contains all of the possible sections and their default values
# Note that all fields that take a lint level have these possible values:
# * deny - An error will be produced and the check will fail
# * warn - A warning will be produced, but the check will not fail
# * allow - No warning or error will be produced, though in some cases a note
# will be
# The values provided in this template are the default values that will be used
# when any section or field is not specified in your own configuration
# Root options
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
# dependency, such as, for example, the `nix` crate only being used via the
# `target_family = "unix"` configuration, that only having windows targets in
# this list would mean the nix crate, as well as any of its exclusive
# dependencies not shared by any other crates, would be ignored, as the target
# list here is effectively saying which targets you are building for.
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
# is pruned from the graph, all of its dependencies will also be pruned unless
# they are connected to another crate in the graph that hasn't been pruned,
# so it should be used with care. The identifiers are [Package ID Specifications]
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
#exclude = []
# If true, metadata will be collected with `--all-features`. Note that this can't
# be toggled off if true, if you want to conditionally enable `--all-features` it
# is recommended to pass `--all-features` on the cmd line instead
all-features = false
# If true, metadata will be collected with `--no-default-features`. The same
# caveat with `all-features` applies
no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
# of features from the crate(s) to all of the graph roots can be far too verbose.
# This option can be overridden via `--feature-depth` on the cmd line
feature-depth = 1
# This section is considered when running `cargo deny check advisories`
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "warn"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
#"RUSTSEC-0000-0000",
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
# See Git Authentication for more information about setting up git authentication.
#git-fetch-with-cli = true
# This section is considered when running `cargo deny check licenses`
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
allow = [
"Apache-2.0",
"BSD-3-Clause",
"MIT",
"Unicode-DFS-2016",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "deny"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
# [possible values: any between 0.0 and 1.0].
confidence-threshold = 0.8
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
# aren't accepted for every possible crate as with the normal allow list
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
#[[licenses.clarify]]
# The name of the crate the clarification applies to
#name = "ring"
# The optional version constraint for the crate
#version = "*"
# The SPDX expression for the license requirements of the crate
#expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
# the license expression. If the contents match, the clarification will be used
# when running the license check, otherwise the clarification will be ignored
# and the crate will be checked normally, which may produce warnings or errors
# depending on the rest of your configuration
#license-files = [
# Each entry is a crate relative path, and the (opaque) hash of its contents
#{ path = "LICENSE", hash = 0xbd0eed23 }
#]
[licenses.private]
# If true, ignores workspace crates that aren't published, or are only
# published to private registries.
# To see how to mark a crate as unpublished (to the official registry),
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
ignore = false
# One or more private registries that you might publish crates to, if a crate
# is only published to private registries, and ignore is true, the crate will
# not have its license(s) checked
registries = [
#"https://sekretz.com/registry
]
# This section is considered when running `cargo deny check bans`.
# More documentation about the 'bans' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
[bans]
# Lint level for when multiple versions of the same crate are detected
multiple-versions = "warn"
# Lint level for when a crate version requirement is `*`
wildcards = "allow"
# The graph highlighting used when creating dotgraphs for crates
# with multiple versions
# * lowest-version - The path to the lowest versioned duplicate is highlighted
# * simplest-path - The path to the version with the fewest edges is highlighted
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overriden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overriden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
]
# List of features to allow/deny
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#name = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
#allow = [
# "rustls",
# "__rustls",
# "__tls",
# "hyper-rustls",
# "rustls",
# "rustls-pemfile",
# "rustls-tls-webpki-roots",
# "tokio-rustls",
# "webpki-roots",
#]
# If true, the allowed features must exactly match the enabled feature set. If
# this is set there is no point setting `deny`
#exact = true
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
]
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
[sources]
# Lint level for what to happen when a crate from a crate registry that is not
# in the allow list is encountered
unknown-registry = "warn"
# Lint level for what to happen when a crate from a git repository that is not
# in the allow list is encountered
unknown-git = "warn"
# List of URLs for allowed crate registries. Defaults to the crates.io index
# if not specified. If it is specified but empty, no registries are allowed.
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
# List of URLs for allowed Git repositories
allow-git = []
[sources.allow-org]
# 1 or more github.com organizations to allow git sources for
github = [""]
# 1 or more gitlab.com organizations to allow git sources for
gitlab = [""]
# 1 or more bitbucket.org organizations to allow git sources for
bitbucket = [""]
+5 -17
View File
@@ -2,25 +2,13 @@
//
// for info on the format, see `NetLatency-Data/README.md`
//
// **** Features ****
//
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
#[cfg(feature = "f32")]
type FloatType = f32;
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
#[cfg(not(feature = "f32"))]
type FloatType = f64;
use std::{
fs::File,
io::{BufReader, Read},
time::Duration,
};
use rand::Rng;
use nanorand::{Rng, WyRand};
use vivaldi_nc::NetworkCoordinate;
// this will return a Vec<Vec<Duration>>
@@ -78,10 +66,10 @@ fn main() {
// 4. adjust local NC
let mut error = 0.0;
for _ in 0..30_000 {
let mut rng = rand::thread_rng();
let mut rng = WyRand::new();
// 1. choose a local and remote NC
let i_local = rng.gen_range(0..NUM_NODES);
let i_remote = rng.gen_range(0..NUM_NODES);
let i_local = rng.generate::<usize>() % NUM_NODES;
let i_remote = rng.generate::<usize>() % NUM_NODES;
if i_local == i_remote {
continue;
}
@@ -97,7 +85,7 @@ fn main() {
nc[i_local].update(&nc_remote, rtt_measured);
// complete, now let's sum up the error
error = nc.iter().map(|n| n.error()).sum::<FloatType>() / NUM_NODES as FloatType;
error = nc.iter().map(|n| n.error()).sum::<f32>() / NUM_NODES as f32;
assert!(error.is_finite());
if error < 5.0 {
break;
-2
View File
@@ -8,5 +8,3 @@ cc 4a5cb6ca844d5306203ae00a99375c83a20ddf4ca427f0ae0b541697efa3d9b7 # shrinks to
cc 38ed1e2c313317809f5469d856d2c1c61e4851c64a156cd1bdd0757fa7c77128 # shrinks to x = 0.0, y = -0.0, h = 0.0, m = -0.0
cc b7d1b5e9fe3e33066b583c4532865373179972e9b5786ecc62438f23bc939e87 # shrinks to x = -0.0, y = 0.0, h = -3.448118e-33, m = -0.0
cc 3c4fc4f21c82ffe5bcbc740ca9ab598e0894226acb86a3046a3a24cbbcdbf8e6 # shrinks to x = 0.0, y = 0.0, h = -0.0
cc d1a551d7eb92bf69e3d0cd0b677baac5aaa5a95d6a016fccf67cf57af71c8b0c # shrinks to x = -5.098216e26, y = 0.0, h = 0.0
cc 7fa6f2f56f2398bae6804c5f0c0f3f47a7304e505c34f3addb676546cefd97f2 # shrinks to x = -0.0, y = 1.500389e38, h = 2.828499e38
+68 -158
View File
@@ -42,27 +42,13 @@
//! > Each node has a positive height element in its coordinates, so that
//! > its height can always be scaled up or down.
use core::ops::{Add, Mul, Sub};
use std::ops::{Add, Mul, Sub};
use rand::prelude::*;
use nanorand::{Rng, WyRand};
use serde::{Deserialize, Serialize};
use crate::vector::Vector;
//
// **** Features ****
//
cfg_if::cfg_if! {
if #[cfg(feature = "f32")] {
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
type FloatType = f32;
} else {
/// `FloatType` is a type alias for either `f32` or `f64` depending on cargo features
type FloatType = f64;
}
}
//
// **** Constants ****
//
@@ -77,58 +63,45 @@ cfg_if::cfg_if! {
///
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct HeightVector<const N: usize> {
/// `position` is the Euclidean coordinate part of the `HeightVector`, representing a position
/// in the network's latency space
#[serde(flatten)]
position: Vector<FloatType, N>,
/// `height` is a representation of a node's stemp time, or its latency to get into the network
/// core (or backbone). It's a way to account for triangle inequality violations.
/// in the network's latency space
height: FloatType,
}
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
pub(crate) fn new() -> Self {
Self::random()
}
/// A new height vector is a random unit vector
pub(crate) fn random() -> Self {
let mut rng = thread_rng();
let mut rng = WyRand::new();
let mut vec = [0.0; N];
for i in vec.iter_mut().take(N) {
*i = rng.gen::<FloatType>() - 0.5;
*i = rng.generate::<f32>();
}
let height = rng.gen::<FloatType>().abs();
Self {
position: Vector::<FloatType, N>::from(vec),
height,
}
.normalized()
let height = rng.generate::<f32>().abs();
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) -> FloatType {
self.position.len() + self.height
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();
// scale it by the inverse of its magnitude like any normal vector
let ret = Self {
position: self.position / len,
height: self.height / len,
};
if ret.is_valid() {
ret
} else {
if len < f32::EPSILON {
// if we have a bad vector, generate a new random vector
Self::random()
Self::new()
} else {
// otherwise scale it by the inverse of its magnitude like any normal vector
Self(self.0 / len, self.1 / len)
}
}
@@ -145,10 +118,7 @@ impl<const N: usize> HeightVector<N> {
/// In this case, valid means the height is positive, and none of the components are NaN or
/// Inf.
pub(crate) fn is_invalid(&self) -> bool {
self.position.is_invalid()
|| self.height.is_nan()
|| self.height.is_infinite()
|| self.height < 0.0
self.0.is_invalid() || self.1.is_nan() || self.1.is_infinite() || self.1 < 0.0
}
}
@@ -159,20 +129,14 @@ impl<const N: usize> HeightVector<N> {
impl<const N: usize> Default for HeightVector<N> {
/// Default value for a Vivaldi height vector is just the defaults of its children types.
fn default() -> Self {
Self {
position: Vector::default(),
height: Default::default(),
}
Self(Default::default(), Default::default())
}
}
impl<const N: usize> From<([FloatType; N], FloatType)> for HeightVector<N> {
/// Convert from a `([FloatType; N], FloatType)` (vector, height) to a Vivaldi height vector type.
fn from(value: ([FloatType; N], FloatType)) -> Self {
let ret = Self {
position: Vector::<FloatType, N>::from(value.0),
height: value.1,
};
impl<const N: usize> From<([f32; N], f32)> for HeightVector<N> {
/// Convert from a `([f32; N], f32)` (vector, height) to a Vivaldi height vector type.
fn from(value: ([f32; N], f32)) -> Self {
let ret = Self(Vector::<f32, N>::from(value.0), value.1);
if ret.is_valid() {
ret
} else {
@@ -186,10 +150,7 @@ impl<const N: usize> Add for HeightVector<N> {
/// Add two Vivaldi height vectors.
fn add(self, rhs: Self) -> Self::Output {
let ret = Self {
position: self.position + rhs.position,
height: self.height + rhs.height,
};
let ret = Self(self.0 + rhs.0, self.1 + rhs.1);
if ret.is_valid() {
ret
} else {
@@ -204,10 +165,7 @@ impl<const N: usize> Sub for HeightVector<N> {
/// Subtract two Vivaldi height vectors. Note that this is the difference in the vectors
/// and the summation of the heights, as defined by Vivaldi's author.
fn sub(self, rhs: Self) -> Self::Output {
let ret = Self {
position: self.position - rhs.position,
height: self.height + rhs.height,
};
let ret = Self(self.0 - rhs.0, self.1 + rhs.1);
if ret.is_valid() {
ret
} else {
@@ -216,15 +174,12 @@ impl<const N: usize> Sub for HeightVector<N> {
}
}
impl<const N: usize> Mul<FloatType> for HeightVector<N> {
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: FloatType) -> Self::Output {
let ret = Self {
position: self.position * rhs,
height: self.height * rhs,
};
fn mul(self, rhs: f32) -> Self::Output {
let ret = Self(self.0 * rhs, self.1 * rhs);
if ret.is_valid() {
ret
} else {
@@ -236,79 +191,67 @@ impl<const N: usize> Mul<FloatType> for HeightVector<N> {
//
// **** Tests ****
//
#[allow(clippy::ignored_unit_patterns)]
#[cfg(test)]
mod tests {
use super::*;
use assert_approx_eq::assert_approx_eq;
use proptest::prelude::*;
fn approx_eq(a: FloatType, b: FloatType, margin: FloatType) -> bool {
(a - b).abs() <= margin
}
proptest! {
#[test]
fn proptest_len(x: FloatType, y: FloatType, h: FloatType) {
let len = x.hypot(y) + h.abs();
fn proptest_len(x: f32, y: f32, h: f32) {
let len = ((x*x) + (y*y)).sqrt() + h.abs();
let a = HeightVector::<2>::from(([x,y],h));
if x.is_nan() || x.is_infinite() || y.is_nan() || y.is_infinite() || h.is_nan() || h.is_infinite() || h < 0.0 {
// we should've gottne a random univ vector here
prop_assert!(approx_eq(a.len(), 1.0, 0.0001));
assert_approx_eq!(a.len(), 1.0);
} else {
// `HeightVector` we created from proptest values should be valid and have a length
if len.is_nan() || len.is_infinite() {
prop_assert!(a.len().is_nan() || a.len().is_infinite());
} else {
prop_assert!(approx_eq(a.len(), len, 0.0001));
}
assert_eq!(a.len(), len);
}
}
#[test]
#[allow(clippy::cast_lossless, clippy::cast_precision_loss)]
fn proptest_add(x0 in -1_000_000_000..1_000_000_000i32, y0 in -1_000_000_000..1_000_000_000i32, h0 in 0..1_000_000_000i32, x1 in -1_000_000_000..1_000_000_000i32, y1 in -1_000_000_000..1_000_000_000i32, h1 in 0..1_000_000i32) {
// convert our integer range inputs to FloatType
let (fx0,fy0,fh0) = (x0 as FloatType / 1_000.0, y0 as FloatType / 1_000.0, h0 as FloatType / 1_000.0);
let (fx1,fy1,fh1) = (x1 as FloatType / 1_000.0, y1 as FloatType / 1_000.0, h1 as FloatType / 1_000.0);
// convert our integer range inputs to f32s
let (fx0,fy0,fh0) = (x0 as f32 / 1_000.0, y0 as f32 / 1_000.0, h0 as f32 / 1_000.0);
let (fx1,fy1,fh1) = (x1 as f32 / 1_000.0, y1 as f32 / 1_000.0, h1 as f32 / 1_000.0);
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
let c = a+b;
prop_assert!(approx_eq(c.position[0], fx0 + fx1, 0.0001));
prop_assert!(approx_eq(c.position[1], fy0 + fy1, 0.0001));
prop_assert!(approx_eq(c.height, fh0 + fh1, 0.0001));
assert_eq!(c.0[0], fx0 + fx1);
assert_eq!(c.0[1], fy0 + fy1);
assert_eq!(c.1, fh0 + fh1);
}
#[test]
#[allow(clippy::cast_lossless, clippy::cast_precision_loss)]
fn proptest_sub(x0 in -1_000_000_000..1_000_000_000i32, y0 in -1_000_000_000..1_000_000_000i32, h0 in 0..1_000_000_000i32, x1 in -1_000_000_000..1_000_000_000i32, y1 in -1_000_000_000..1_000_000_000i32, h1 in 0..1_000_000i32) {
// convert our integer range inputs to FloatType
let (fx0,fy0,fh0) = (x0 as FloatType / 1_000.0, y0 as FloatType / 1_000.0, h0 as FloatType / 1_000.0);
let (fx1,fy1,fh1) = (x1 as FloatType / 1_000.0, y1 as FloatType / 1_000.0, h1 as FloatType / 1_000.0);
// convert our integer range inputs to f32s
let (fx0,fy0,fh0) = (x0 as f32 / 1_000.0, y0 as f32 / 1_000.0, h0 as f32 / 1_000.0);
let (fx1,fy1,fh1) = (x1 as f32 / 1_000.0, y1 as f32 / 1_000.0, h1 as f32 / 1_000.0);
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
let c = a-b;
prop_assert!(approx_eq(c.position[0], fx0 - fx1, 0.0001));
prop_assert!(approx_eq(c.position[1], fy0 - fy1, 0.0001));
prop_assert!(approx_eq(c.height, fh0 + fh1, 0.0001));
assert_eq!(c.0[0], fx0 - fx1);
assert_eq!(c.0[1], fy0 - fy1);
assert_eq!(c.1, fh0 + fh1);
}
#[test]
#[allow(clippy::cast_lossless, clippy::cast_precision_loss)]
fn proptest_mul(x in -1_000_000_000..1_000_000_000i32, y in -1_000_000_000..1_000_000_000i32, h in 0..1_000_000_000i32, m in -1_000_000_000..1_000_000_000i32) {
// convert our integer range inputs to FloatType
let (fx,fy,fh,fm) = (x as FloatType / 1_000.0, y as FloatType / 1_000.0, h as FloatType / 1_000.0, m as FloatType / 1_000.0);
// convert our integer range inputs to f32s
let (fx,fy,fh,fm) = (x as f32 / 1_000.0, y as f32 / 1_000.0, h as f32 / 1_000.0, m as f32 / 1_000.0);
let a = HeightVector::<2>::from(([fx, fy], fh));
let b = a * fm;
if fm < 0.0 {
prop_assert!(approx_eq(b.len(), 1.0, 0.0001));
assert_approx_eq!(b.len(), 1.0)
} else {
prop_assert!(approx_eq(b.position[0], fx * fm, 0.0001));
prop_assert!(approx_eq(b.position[1], fy * fm, 0.0001));
prop_assert!(approx_eq(b.height, fh * fm, 0.0001));
assert_eq!(b.0[0], fx * fm);
assert_eq!(b.0[1], fy * fm);
assert_eq!(b.1, fh * fm);
}
}
}
@@ -322,7 +265,7 @@ mod tests {
#[test]
fn test_new() {
// new gives us a random unit length vec
let a = HeightVector::<3>::random();
let a = HeightVector::<3>::new();
assert_approx_eq!(a.len(), 1.0);
}
@@ -336,9 +279,9 @@ mod tests {
#[test]
fn test_from() {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
assert_approx_eq!(a.position[0], 1.0);
assert_approx_eq!(a.position[1], 2.0);
assert_approx_eq!(a.height, 3.0);
assert_eq!(a.0[0], 1.0);
assert_eq!(a.0[1], 2.0);
assert_eq!(a.1, 3.0);
}
#[test]
@@ -346,9 +289,9 @@ mod tests {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
let c = a + b;
assert_approx_eq!(c.position[0], 4.0);
assert_approx_eq!(c.position[1], 4.0);
assert_approx_eq!(c.height, 4.0);
assert_eq!(c.0[0], 4.0);
assert_eq!(c.0[1], 4.0);
assert_eq!(c.1, 4.0);
}
#[test]
@@ -356,63 +299,30 @@ mod tests {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
let c = a - b;
assert_approx_eq!(c.position[0], -2.0);
assert_approx_eq!(c.position[1], 0.0);
assert_approx_eq!(c.height, 4.0);
assert_eq!(c.0[0], -2.0);
assert_eq!(c.0[1], 0.0);
assert_eq!(c.1, 4.0);
}
#[test]
fn test_mul_scalar() {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0;
assert_approx_eq!(a.position[0], 10.0);
assert_approx_eq!(a.position[1], 20.0);
assert_approx_eq!(a.height, 30.0);
assert_eq!(a.0[0], 10.0);
assert_eq!(a.0[1], 20.0);
assert_eq!(a.1, 30.0);
}
#[test]
fn test_serde() {
// start with JSON, deserialize it
let s = "{\"position\":[1.0,2.0,3.0],\"height\":4.0}";
let a: HeightVector<3> =
serde_json::from_str(s).expect("deserialization failed during test");
let s = "[{\"inner\":[1.0,2.0,3.0]},4.0]";
let a: HeightVector<3> = serde_json::from_str(s).unwrap();
// make sure it's the right length and works like we expect a normal NC
assert_approx_eq!(a.len(), 7.741_657, 0.001);
// serialize it into a new JSON string and make sure it matches the original
let t = serde_json::to_string(&a);
assert_eq!(t.as_ref().expect("serialization failed during test"), s);
}
#[test]
fn test_sub_invalid() {
let valid = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let mut invalid = HeightVector::<2>::from(([1.0, 2.0], 3.0));
// force invalidity here - because `::from()` actually also catches this
invalid.height = 1.0 / 0.0;
let result = valid - invalid;
assert_approx_eq!(result.len(), 1.0);
}
#[test]
fn test_add_invalid() {
let valid = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let mut invalid = HeightVector::<2>::from(([1.0 / 0.0, 2.0], 3.0));
// make sure `from()` caught and replaced the invalid vec
assert_approx_eq!(invalid.len(), 1.0);
// force invalidity here - because `::from()` actually also catches this
invalid.height = 1.0 / 0.0;
let result = valid + invalid;
assert_approx_eq!(result.len(), 1.0);
}
#[test]
fn test_zero_norm() {
let a = HeightVector::<2>::from(([0.0, 0.0], 0.0));
let b = a.normalized();
let len = b.len();
assert_approx_eq!(len, 1.0);
assert_eq!(t.as_ref().unwrap(), s);
}
}
+7 -44
View File
@@ -1,5 +1,4 @@
//! Vivaldi network coordinates for fast, distributed latency estimates in multinode networks, with
//! a clean and simple interface.
//! Vivaldi Network Coordinates with a simple interface
//!
//! 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
@@ -20,8 +19,7 @@
//!
//! # Usage
//!
//! This crate exports a single struct, [`NetworkCoordinate`] and two type aliases
//! ([`NetworkCoordinate2D`] and [`NetworkCoordinate3D`]). Typical use of Vivaldi NCs for a
//! This crate exports a single struct, [`NetworkCoordinate`]. Typical use of Vivaldi NCs for a
//! distributed network works like this:
//!
//! 1. Each node in the network has its own instance of [`NetworkCoordinate`]. *See "Note on
@@ -53,7 +51,7 @@
//!
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
//! // this from a remote node)
//! let received_msg = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
//! let received_msg = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
//! let remote_nc: NetworkCoordinate<3> = serde_json::from_str(received_msg).unwrap();
//!
//! // mock up a measured RTT between us and that node (normally the nodes would coordinate
@@ -71,19 +69,16 @@
//! forwarded to us by someone else. And in this case, we aren't measuring the actual RTT. Instead,
//! we're using the NCs to estimate RTT.
//!
//! This example also uses the type alias [`NetworkCoordinate3D`] instead of its equivalent
//! [`NetworkCoordinate<3>`].
//!
//! ```
//! use vivaldi_nc::NetworkCoordinate3D;
//! use vivaldi_nc::NetworkCoordinate;
//!
//! // create our local NC as a 3-dimensional Vivaldi coordinate
//! let mut my_nc = NetworkCoordinate3D::new();
//! let mut my_nc = NetworkCoordinate::<3>::new();
//!
//! // mock up a remote NC for the sake of this example (normally the node would actually receive
//! // this from a remote node)
//! let received_msg = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
//! let remote_nc: NetworkCoordinate3D = serde_json::from_str(received_msg).unwrap();
//! let received_msg = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
//! let remote_nc: NetworkCoordinate<3> = serde_json::from_str(received_msg).unwrap();
//!
//! // estimate the RTT to this remote node without measuring it directly
//! let rtt_estimated = my_nc.estimated_rtt(&remote_nc);
@@ -93,41 +88,9 @@
//! ```
//!
#![deny(
clippy::all,
clippy::correctness,
clippy::style,
clippy::suspicious,
clippy::complexity,
clippy::perf,
clippy::cargo,
clippy::pedantic,
clippy::nursery,
clippy::unwrap_used
)]
#![deny(
anonymous_parameters,
missing_copy_implementations,
missing_debug_implementations,
missing_docs,
nonstandard_style,
rust_2018_idioms,
single_use_lifetimes,
trivial_casts,
trivial_numeric_casts,
// unreachable_pub, // disagrees with clippy::nurclippy::nursery as of 13-02-2023
unused_extern_crates,
unused_qualifications,
variant_size_differences
)]
#![allow(clippy::type_repetition_in_bounds)]
#![allow(single_use_lifetimes)]
mod height_vector;
mod vector;
// publish our interface
pub mod network_coordinate;
pub use network_coordinate::NetworkCoordinate;
pub use network_coordinate::NetworkCoordinate2D;
pub use network_coordinate::NetworkCoordinate3D;
+61 -142
View File
@@ -1,78 +1,60 @@
//! Main interface module for Vivaldi network coordinates.
//! 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)
//! ```
//!
//! For usage explanation and examples, please see the main [`crate`] documentation.
use core::time::Duration;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::height_vector::HeightVector;
//
// **** Features ****
//
#[cfg(feature = "f32")]
type FloatType = f32;
#[cfg(not(feature = "f32"))]
type FloatType = f64;
//
// **** Constants ****
//
// Vivaldi tuning parameters
const C_ERROR: FloatType = 0.25;
const C_DELTA: FloatType = 0.25;
const C_ERROR: f32 = 0.25;
const C_DELTA: f32 = 0.25;
// initial error value
const DEFAULT_ERROR: FloatType = 200.0;
const DEFAULT_ERROR: f32 = 200.0;
// error should always be greater than zero
const MIN_ERROR: FloatType = FloatType::EPSILON;
const MIN_ERROR: f32 = f32::EPSILON;
//
// **** Structs ****
//
/// A `NetworkCoordinate<N>` is the main interface to a Vivaldi network coordinate.
///
/// # Generic Parameters
///
/// - `N`: Const generic for number of dimensions. For example, `NetworkCoordinate<3>` is a
/// 3-Dimentionsal Euclidean coordinate plus a height. Should be a positive number greater than
/// zero.
///
/// **Note:** Dimensions other than 2D or 3D are usually not useful. If you want to use one of
/// those dimensions, you can use type aliases ([`NetworkCoordinate2D`] or [`NetworkCoordinate3D`])
/// which are a little more ergonomic than using the generic here.
///
/// # Examples
///
/// For an explanation and examples of usage, please see the main [`crate`] documentation.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NetworkCoordinate<const N: usize> {
#[serde(flatten)]
heightvec: HeightVector<N>,
error: FloatType,
position: HeightVector<N>,
error: f32,
}
// type aliases for convenience
/// A 2D [`NetworkCoordinate`]. Includes a 2D Euclidean position and a height.
///
/// This type alias is just for convenience. It's functionally equivalent to
/// `NetworkCoordinate<2>`. For more information, see [`NetworkCoordinate`].
#[allow(clippy::module_name_repetitions)]
pub type NetworkCoordinate2D = NetworkCoordinate<2>;
/// A 3D [`NetworkCoordinate`]. Includes a 3D Euclidean position and a height.
///
/// This type alias is just for convenience. It's functionally equivalent to
/// `NetworkCoordinate<3>`. For more information, see [`NetworkCoordinate`].
#[allow(clippy::module_name_repetitions)]
pub type NetworkCoordinate3D = NetworkCoordinate<3>;
//
// **** Implementations ****
//
@@ -91,7 +73,6 @@ impl<const N: usize> NetworkCoordinate<N> {
/// // print the NC
/// println!("Our new NC is: {:#?}", a);
/// ```
#[must_use]
pub fn new() -> Self {
Self::default()
}
@@ -123,16 +104,9 @@ impl<const N: usize> NetworkCoordinate<N> {
/// println!("Estimated RTT: {}", a.estimated_rtt(&b).as_millis());
/// ```
///
#[must_use]
pub fn estimated_rtt(&self, rhs: &Self) -> Duration {
// estimated rss is euclidean distance between the two plus the sum of the heights
cfg_if::cfg_if! {
if #[cfg(feature = "f32")] {
Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0)
} else {
Duration::from_secs_f64((self.heightvec - rhs.heightvec).len() / 1000.0)
}
}
Duration::from_secs_f32((self.position - rhs.position).len() / 1000.0)
}
/// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round
@@ -150,7 +124,7 @@ impl<const N: usize> NetworkCoordinate<N> {
/// # Example
///
/// ```
/// use core::time::Duration;
/// use std::time::Duration;
/// use vivaldi_nc::NetworkCoordinate;
///
/// // We always have our own NC:
@@ -165,52 +139,15 @@ impl<const N: usize> NetworkCoordinate<N> {
/// // Now we can update our NC to adjust our position relative to the remote node:
/// local.update(&remote, rtt);
/// ```
///
/// # Algorithm
///
/// 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 xj,
/// // and an error estimate of ej .
/// //
/// // 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)
/// ```
///
pub fn update(&mut self, rhs: &Self, rtt: Duration) -> &Self {
// convert Durations into FloatType as fractional milliseconds for convenience
cfg_if::cfg_if! {
if #[cfg(feature = "f32")] {
let rtt_ms = rtt.as_secs_f32() * 1000.0;
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f32() * 1000.0;
} else {
let rtt_ms = rtt.as_secs_f64() * 1000.0;
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f64() * 1000.0;
}
// rtt needs to be positive
if rtt.as_secs_f32() < 0.0 {
return self;
}
// rtt needs to be positive
if rtt_ms < 0.0 {
// Note: `rtt` is guaranteed to be positive because `Duration` enforces it.
// If this panics, something changed where `Duration` now allows for negative
// values.
unreachable!();
}
// 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 )
@@ -223,27 +160,18 @@ impl<const N: usize> NetworkCoordinate<N> {
// 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);
// NOTE: using `mul_add()` which is a little safer (avoid overflows)
self.error = (es * C_ERROR)
.mul_add(w, self.error * C_ERROR.mul_add(-w, 1.0))
.max(MIN_ERROR);
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.heightvec =
self.heightvec + (self.heightvec - rhs.heightvec).normalized() * delta * error;
self.position = self.position + (self.position - rhs.position).normalized() * delta * error;
// if we ended up with an invalid coordinate, return a new random coordinate with default
// error
if self.heightvec.is_invalid() {
*self = Self::new();
// We should never get here because the call to `normalized()` above should catch any
// invalid `heightvec`
unreachable!();
if self.position.is_invalid() {
*self = Self::new()
}
// return reference to updated self
@@ -252,8 +180,7 @@ impl<const N: usize> NetworkCoordinate<N> {
/// getter for error value - useful for consumers to understand the estimated accuracty of this
/// `NetworkCoordinate`
#[must_use]
pub const fn error(&self) -> FloatType {
pub fn error(&self) -> f32 {
self.error
}
}
@@ -263,10 +190,10 @@ impl<const N: usize> NetworkCoordinate<N> {
//
impl<const N: usize> Default for NetworkCoordinate<N> {
/// A default `NetworkCoordinate` has a random position and `DEFAULT_ERROR`
/// A default `NetworkCoordinate` has a random position and DEFAULT_ERROR
fn default() -> Self {
Self {
heightvec: HeightVector::<N>::random(),
position: HeightVector::<N>::random(),
error: DEFAULT_ERROR,
}
}
@@ -330,8 +257,10 @@ mod tests {
let mut mad = NetworkCoordinate::<2>::new();
// verify the initial error
let error = slc.error.hypot(nyc.error.hypot(lax.error.hypot(mad.error)));
assert_approx_eq!(error, 400.0);
let error =
(slc.error.powf(2.0) + nyc.error.powf(2.0) + lax.error.powf(2.0) + mad.error.powf(2.0))
.sqrt();
assert_eq!(error, 400.0);
// iterate plenty of times to converge and minimize error
(0..20).for_each(|_| {
@@ -356,45 +285,35 @@ mod tests {
// compute and test the root mean squared error
let error = slc.error + nyc.error + lax.error + mad.error;
println!("error = {error}");
assert!(error < 5.0);
}
#[test]
fn test_serde() {
// start with JSON, deserialize it
let s = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
let a: NetworkCoordinate<3> =
serde_json::from_str(s).expect("deserialization failed during test");
let s = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},0.1],\"error\":1.0}";
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
// make sure it's the right length and works like we expect a normal NC
assert_approx_eq!(a.heightvec.len(), 2.649_509, 0.001);
assert_approx_eq!(a.error, 1.0);
assert_approx_eq!(a.position.len(), 2.649_509, 0.001);
assert_eq!(a.error, 1.0);
assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
// serialize it into a new JSON string and make sure it matches the original
let t = serde_json::to_string(&a);
assert_eq!(t.as_ref().expect("serialization failed during test"), s);
assert_eq!(t.as_ref().unwrap(), s);
}
#[test]
fn test_estimated_rtt() {
// start with JSON, deserialize it
let s = "{\"position\":[1.5,0.5,2.0],\"height\":25.0,\"error\":1.0}";
let a: NetworkCoordinate<3> =
serde_json::from_str(s).expect("deserialization failed during test");
let s = "{\"position\":[-1.5,-0.5,-2.0],\"height\":50.0,\"error\":1.0}";
let b: NetworkCoordinate<3> =
serde_json::from_str(s).expect("deserialization failed during test");
let s = "{\"position\":[{\"inner\":[1.5,0.5,2.0]},25.0],\"error\":1.0}";
let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
let s = "{\"position\":[{\"inner\":[-1.5,-0.5,-2.0]},50.0],\"error\":1.0}";
let b: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
let estimate = a.estimated_rtt(&b);
assert_approx_eq!(estimate.as_secs_f32(), 0.080_099);
}
#[test]
fn test_error_getter() {
let s = "{\"position\":[1.5,0.5,2.0],\"height\":25.0,\"error\":1.0}";
let a: NetworkCoordinate<3> =
serde_json::from_str(s).expect("deserialization failed during test");
assert_approx_eq!(a.error(), 1.0);
}
}
+51 -61
View File
@@ -3,12 +3,11 @@
//! This is the simplest possible implementation of a vector to support the needs of a
//! Vivaldi `HeightVector`.
use array_init::array_init;
use num_traits::Float;
use serde::{Deserialize, Serialize};
use serde_with::serde_as;
use core::ops::{Add, Div, Index, Mul, Sub};
use std::ops::{Add, Div, Index, Mul, Sub};
//
// **** Structs ****
@@ -21,11 +20,10 @@ use core::ops::{Add, Div, Index, Mul, Sub};
/// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc)
#[serde_as]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Vector<T, const N: usize>
pub(crate) struct Vector<T, const N: usize>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
#[serde(rename = "position")]
#[serde_as(as = "[_; N]")]
inner: [T; N],
}
@@ -38,10 +36,17 @@ impl<T, const N: usize> Vector<T, N>
where
T: Float + Serialize + for<'d> Deserialize<'d>,
{
/// Create a new Zero vector.
pub(crate) fn new() -> Self {
Self::default()
}
/// Compute magnitude of vector.
pub(crate) fn len(&self) -> T {
// use hypot() instead of sqrt() of sum of squares to avoid overflows
self.inner.iter().fold(T::zero(), |acc, x| acc.hypot(*x))
(0..N)
.map(|i| self.inner[i] * self.inner[i])
.fold(T::zero(), |sum_sq, x| sum_sq + x)
.sqrt()
}
/// Checks whether the `Vector` is invalid.
@@ -86,9 +91,11 @@ where
/// Vector+Vector addition.
fn add(self, rhs: Self) -> Self::Output {
Self::Output {
inner: array_init(|i| self[i] + rhs[i]),
let mut ret = [T::zero(); N]; //Self::Output::new();
for i in 0..N {
ret[i] = self[i] + rhs[i];
}
Self::Output::from(ret)
}
}
@@ -100,9 +107,11 @@ where
/// Vector-Vector subtraction.
fn sub(self, rhs: Self) -> Self::Output {
Self::Output {
inner: array_init(|i| self[i] - rhs[i]),
let mut ret = [T::zero(); N]; //Self::Output::new();
for i in 0..N {
ret[i] = self[i] - rhs[i];
}
Self::Output::from(ret)
}
}
@@ -114,9 +123,11 @@ where
/// Vector*scalar multiplication.
fn mul(self, rhs: T) -> Self::Output {
Self::Output {
inner: array_init(|i| self[i] * rhs),
let mut ret = Self::Output::new();
for i in 0..N {
ret.inner[i] = self.inner[i] * rhs;
}
ret
}
}
@@ -128,9 +139,11 @@ where
/// Vector/scalar division.
fn div(self, rhs: T) -> Self::Output {
Self::Output {
inner: array_init(|i| self[i] / rhs),
let mut ret = Self::Output::new();
for i in 0..N {
ret.inner[i] = self.inner[i] / rhs;
}
ret
}
}
@@ -150,7 +163,6 @@ where
// **** Tests ****
//
#[allow(clippy::ignored_unit_patterns)]
#[cfg(test)]
mod tests {
use super::*;
@@ -158,45 +170,25 @@ mod tests {
use assert_approx_eq::assert_approx_eq;
use proptest::prelude::*;
fn approx_eq(a: f32, b: f32, margin: f32) -> bool {
(a - b).abs() <= margin
}
proptest! {
#[test]
fn proptest_new(x: f32, y: f32, z: f32){
let v = Vector::<f32, 3>::from([x,y,z]);
prop_assert!(approx_eq(v[0], x, 0.0001));
prop_assert!(approx_eq(v[1], y, 0.0001));
prop_assert!(approx_eq(v[2], z, 0.0001));
prop_assert!(v[0].is_finite());
prop_assert!(v[1].is_finite());
prop_assert!(v[1].is_finite());
assert_eq!(v[0], x);
assert_eq!(v[1], y);
assert_eq!(v[2], z);
assert!(v[0].is_finite());
assert!(v[1].is_finite());
assert!(v[1].is_finite());
}
#[test]
fn proptest_mul(x: f32, y: f32, z: f32, m: f32) {
let v = Vector::<f32, 3>::from([x,y,z]);
let w = v * m;
let (x,y,z) = (v[0]*m, v[1]*m, v[2]*m);
if x.is_nan() || x.is_infinite() {
prop_assert!(w[0].is_nan() || w[0].is_infinite());
} else {
prop_assert!(approx_eq(w[0], x, 0.0001));
}
if y.is_nan() || y.is_infinite() {
prop_assert!(w[1].is_nan() || w[1].is_infinite());
} else {
prop_assert!(approx_eq(w[1], y, 0.0001));
}
if z.is_nan() || z.is_infinite() {
prop_assert!(w[2].is_nan() || w[2].is_infinite());
} else {
prop_assert!(approx_eq(w[2], z, 0.0001));
}
assert_eq!(v[0] * m, w[0]);
assert_eq!(v[1] * m, w[1]);
assert_eq!(v[2] * m, w[2]);
}
#[test]
@@ -205,22 +197,23 @@ mod tests {
let w = v / d;
let (x,y,z) = (v[0]/d, v[1]/d, v[2]/d);
if x.is_nan() || x.is_infinite() {
prop_assert!(w[0].is_nan() ||w[0].is_infinite());
if x.is_nan() {
assert!(w[0].is_nan());
} else {
prop_assert!(approx_eq(w[0], x, 0.0001));
assert_eq!(v[0] / d, x);
}
if y.is_nan() || y.is_infinite() {
prop_assert!(w[1].is_nan() || w[1].is_infinite());
if y.is_nan() {
assert!(w[1].is_nan());
} else {
prop_assert!(approx_eq(w[1], y, 0.0001));
assert_eq!(v[1] / d, y);
}
if z.is_nan() || z.is_infinite() {
prop_assert!(w[2].is_nan() || w[2].is_infinite());
if z.is_nan() {
assert!(w[2].is_nan());
} else {
prop_assert!(approx_eq(w[2], z, 0.0001));
assert_eq!(v[2] / d, z);
}
}
}
@@ -228,7 +221,7 @@ mod tests {
#[test]
fn test_default_new() {
let a = Vector::<f32, 3>::default();
let b = Vector::<f32, 3>::from([1.0; 3]) * 0.0;
let b = Vector::<f32, 3>::new();
let c = Vector::<f32, 3>::from([0.0; 3]);
assert_eq!(a, b);
assert_eq!(b, c);
@@ -269,9 +262,9 @@ mod tests {
#[test]
fn test_index() {
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
assert_approx_eq!(a[0], 1.0);
assert_approx_eq!(a[1], 2.0);
assert_approx_eq!(a[2], 3.0);
assert_eq!(a[0], 1.0);
assert_eq!(a[1], 2.0);
assert_eq!(a[2], 3.0);
}
#[test]
fn test_length() {
@@ -286,10 +279,7 @@ mod tests {
fn test_serde() {
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
let s = serde_json::to_string(&a);
assert_eq!(
s.as_ref().expect("serialization failed in test"),
"{\"position\":[1.0,2.0,3.0]}"
);
assert_eq!(s.as_ref().unwrap(), "{\"inner\":[1.0,2.0,3.0]}");
assert!(s.is_ok());
}
}
-22
View File
@@ -1,22 +0,0 @@
// This is just for basic interface and happy path testing. It doesn't do anything other than make
// sure the expected public interface exists and works.
use array_init::array_init;
use core::time::Duration;
use vivaldi_nc::NetworkCoordinate;
const NUM_NODES: usize = 100;
#[test]
fn basic_usage() {
let mut nc: [NetworkCoordinate<2>; NUM_NODES] = array_init(|_| NetworkCoordinate::<2>::new());
for i in 0..NUM_NODES {
for j in 0..NUM_NODES {
if i == j {
continue;
}
let other = nc[j].clone();
nc[i].update(&other, Duration::from_millis(100));
}
}
}