24 Commits
Author SHA1 Message Date
swaits c3b3ff1e4d feat: f32/f64 cargo features added
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 17:01:19 -07:00
swaits 2addbc68b5 doc: minor updates re: type aliases, crate to 0.5.1
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:59:15 -07:00
swaits 6294d1e520 doc: udpate crate description 2023-02-08 15:48:46 -07:00
swaits 2f8769012b bump crate to 0.5.0
Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:40:47 -07:00
swaits 57891ed230 feat: type aliases for 2D/3D NetworkCoordinates
This just adds a few type aliases for convenience, and slightly better ergonomics for the most common usage (2D or 3D).

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:40:23 -07:00
swaits fce4946369 feat: improve ergonomics for serialization format
Goal: To make the serialization format more sensible instead of it being a reflection of our internal (ie private) data model.

- mostly handled with #[serde()] macros (renaming, flattening)
- renamed NetworkCoordinate::position to heightvec which is a better name, even if not seen in the published interface
- changed HeightVector from a tuple struct to a named struct so we can control serde on a field-by-field basis
- updated README, tests, and doc tests to match

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-08 15:32:27 -07:00
swaits 7f41676795 fix: exclude examples from crate
crates.io is limited to 10MB crates

Signed-off-by: Stephen Waits <steve@waits.net>
2023-02-07 22:09:28 -07:00
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 240 additions and 978 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 /target
Cargo.lock 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 -->
+14 -33
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "vivaldi-nc" name = "vivaldi-nc"
version = "0.6.13" version = "0.5.2"
edition = "2021" edition = "2021"
authors = ["Stephen Waits <steve@waits.net>"] 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 for fast, distributed latency estimates in multinode networks, with a clean and simple interface."
@@ -9,43 +9,24 @@ keywords = ["vivaldi", "network", "coordinates", "distributed", "p2p"]
categories = ["algorithms", "network-programming"] categories = ["algorithms", "network-programming"]
documentation = "https://docs.rs/vivaldi-nc/" documentation = "https://docs.rs/vivaldi-nc/"
readme = "README.md" readme = "README.md"
homepage = "https://git.swaits.com/swaits/vivaldi-nc" homepage = "https://sr.ht/~swaits/vivaldi-nc/"
repository = "https://git.swaits.com/swaits/vivaldi-nc" repository = "https://git.sr.ht/~swaits/vivaldi-nc"
exclude = ["examples/*"] exclude = [
rust-version = "1.65" "examples/*",
]
[features] [features]
default = [] default = ["f32"]
f32 = [] f32 = []
f64 = []
[profile.bench]
debug = true
[profile.release]
debug = true
[[bench]]
name = "million-updates"
harness = false
[dev-dependencies] [dev-dependencies]
assert_approx_eq = "1.1.0" assert_approx_eq = "1.1.0"
criterion = "0.5.1" proptest = "1.0.0"
cargo-husky = { version = "1.5.0", features = [ serde_json = "1.0.91"
"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"
[dependencies] [dependencies]
array-init = "2.1.0" nanorand = {version = "0.7.0", features = ["wyrand", "getrandom"]}
cfg-if = "1.0.0" num-traits = "0.2.15"
num-traits = "0.2.18" serde = {version = "1.0.152", features = ["serde_derive"]}
rand = "0.8.5" serde_with = "2.2.0"
serde = { version = "1.0.197", features = ["serde_derive"] }
serde_with = "3.6.1"
+31 -57
View File
@@ -1,22 +1,6 @@
# vivaldi-nc - Vivaldi Network Coordinates # 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) [![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)
<!--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-->
## Introduction ## 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 > has a prime use in networking. The algorithm behind the system uses a
> distributed technique to estimate propagation times between peers in the > distributed technique to estimate propagation times between peers in the
> network. > network.
> >
> Through this scheme, network topology awareness can be used to tune the > Through this scheme, network topology awareness can be used to tune the
> network behaviour to more efficiently distribute data. For example, in a > network behaviour to more efficiently distribute data. For example, in a
> peer-to-peer network, more responsive identification and delivery of content > peer-to-peer network, more responsive identification and delivery of content
> can be achieved. In the Azureus application, Vivaldi is used to improve the > 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** > **Advantages**
> >
> - Vivaldi is a fully distributed scheme, which achieves good scalability. > - Vivaldi is a fully distributed scheme, which achieves good scalability.
> - The Vivaldi algorithm is simple and easy to implement. > - The Vivaldi algorithm is simple and easy to implement.
> >
> **Drawbacks** > **Drawbacks**
> >
> - Vivaldi is based on the Euclidean distance model, which requires the > - Vivaldi is based on the Euclidean distance model, which requires the
> predicted distances to obey the triangle inequality. However, there are > predicted distances to obey the triangle inequality. However, there are
> many triangle inequality violations (TIVs) on the Internet. > many triangle inequality violations (TIVs) on the Internet.
@@ -128,20 +112,18 @@ usage examples.
### Cargo Features ### Cargo Features
By default, the internal data structures and operations all use `f64`, which is By default, the internal data structures and operations all use `f32`. If you want to use `f64`, you can enable it as a cargo feature:
slightly faster on modern architectures. If you want to use `f32` instead, you
can enable it as a cargo feature:
```toml ```toml
[dependencies] [dependencies]
vivaldi-nc = { version = "(version)", features = ["f32"] } vivaldi-nc = { version = "0.5.2", features = ["f64"] }
``` ```
### Examples ### Examples
The repository includes an example which loads a 490 node N-to-N latency sample The repository includes an example which loads a n-to-n latency sample from
from PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean PlanetLab and iterates on `NetworkCoordinate`s until some low-enough mean error
error is reached. The output is a JSON array of elements which contain the NC's 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 position, its height (or stem latency estimate), and its estimation of error
(lower is better) that look like this: (lower is better) that look like this:
@@ -174,7 +156,7 @@ Adding some detail to these three fields:
To run the example, first clone the repository locally: To run the example, first clone the repository locally:
```bash ```bash
git clone https://git.swaits.com/swaits/vivaldi-nc git clone https://git.sr.ht/~swaits/vivaldi-nc
``` ```
Then `cd` into the local clone and run the example: Then `cd` into the local clone and run the example:
@@ -190,11 +172,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). are required, I try to be very selective about them (re: bloat and licensing).
This crate depends on: This crate depends on:
- [`rand`](https://crates.io/crates/rand): I started with `nanorand`; however, - [`nanorand`](https://crates.io/crates/nanorand): A fast PRNG based on WyRand.
this is faster because of `rand::thread_rng()`; whereas with `nanorand` we It pulls in `getrandom` as a portable source of entropy. I chose this over
ended up needing to reimplement that same functionality through something the more commonly used, but heavyweight `rand` because it's significantly
like `lazy_static!` plus `Mutex`, which is not worth it for a few percentage smaller and does just what I need, and not much more.
points of performance.
- [`num-traits`](https://crates.io/crates/num-traits): A brilliant crate which - [`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 makes it easier to operate on numbers in generics; like using `Float` as a
constraint on a generic type. Its convenience outweighs its cost. constraint on a generic type. Its convenience outweighs its cost.
@@ -204,19 +185,12 @@ This crate depends on:
- [`serde_with`](https://crates.io/crates/serde_with): Because the inner - [`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 `Vector<T,N>` uses a const generic length, we use this to help derive
`Deserialize`. `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 ## Design Goals & Alternatives
Several crates implemented Vivaldi NC before this one. So, why another? 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 I had several design goals which the existing crates didn't satisfy:
of priority:
1. Provide the *simplest interface possible*. I just want to have some sort of 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 Coordinate struct and be able to update it, and then use it to estimate
@@ -228,10 +202,7 @@ of priority:
3. Serializable/Deserializable by default. These things are useful for sending 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 across networks. The intent is to be able to do that without a bunch of
rigamarole. Support `serde` traits by default. rigamarole. Support `serde` traits by default.
4. Well documented. Well tested. This varies in the existing crates. 4. Well documented. Well tested. This varies by the 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).
### Other Vivaldi NC Implementations ### Other Vivaldi NC Implementations
@@ -258,12 +229,15 @@ find many more.
## Getting Help or Contributing ## Getting Help or Contributing
To get help, discuss this crate, or report a bug, [open an To get help or discuss this crate, [submit a
issue](https://git.swaits.com/swaits/vivaldi-nc/issues). 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 Discussion related to development or patch submissions should go to
repository on [git.swaits.com](https://git.swaits.com/swaits/vivaldi-nc) and [`vivaldi-nc-devel@`](https://lists.sr.ht/~swaits/vivaldi-nc-devel).
open a pull request.
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 Patch submitters implicitly agree that all contributions they submit fall under
the MIT license. the MIT license.
@@ -271,19 +245,19 @@ the MIT license.
## License ## License
> MIT License > MIT License
> >
> Copyright (c) 2023 Stephen Waits > Copyright (c) 2023 Stephen Waits
> >
> Permission is hereby granted, free of charge, to any person obtaining a copy > Permission is hereby granted, free of charge, to any person obtaining a copy
> of this software and associated documentation files (the "Software"), to deal > of this software and associated documentation files (the "Software"), to deal
> in the Software without restriction, including without limitation the rights > in the Software without restriction, including without limitation the rights
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
> copies of the Software, and to permit persons to whom the Software is > copies of the Software, and to permit persons to whom the Software is
> furnished to do so, subject to the following conditions: > furnished to do so, subject to the following conditions:
> >
> The above copyright notice and this permission notice shall be included in all > The above copyright notice and this permission notice shall be included in all
> copies or substantial portions of the Software. > copies or substantial portions of the Software.
> >
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > 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` // 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::{ use std::{
fs::File, fs::File,
io::{BufReader, Read}, io::{BufReader, Read},
time::Duration, time::Duration,
}; };
use rand::Rng; use nanorand::{Rng, WyRand};
use vivaldi_nc::NetworkCoordinate; use vivaldi_nc::NetworkCoordinate;
// this will return a Vec<Vec<Duration>> // this will return a Vec<Vec<Duration>>
@@ -78,10 +66,10 @@ fn main() {
// 4. adjust local NC // 4. adjust local NC
let mut error = 0.0; let mut error = 0.0;
for _ in 0..30_000 { for _ in 0..30_000 {
let mut rng = rand::thread_rng(); let mut rng = WyRand::new();
// 1. choose a local and remote NC // 1. choose a local and remote NC
let i_local = rng.gen_range(0..NUM_NODES); let i_local = rng.generate::<usize>() % NUM_NODES;
let i_remote = rng.gen_range(0..NUM_NODES); let i_remote = rng.generate::<usize>() % NUM_NODES;
if i_local == i_remote { if i_local == i_remote {
continue; continue;
} }
@@ -97,7 +85,7 @@ fn main() {
nc[i_local].update(&nc_remote, rtt_measured); nc[i_local].update(&nc_remote, rtt_measured);
// complete, now let's sum up the error // 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()); assert!(error.is_finite());
if error < 5.0 { if error < 5.0 {
break; 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 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 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 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
+52 -104
View File
@@ -42,9 +42,9 @@
//! > Each node has a positive height element in its coordinates, so that //! > Each node has a positive height element in its coordinates, so that
//! > its height can always be scaled up or down. //! > 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 serde::{Deserialize, Serialize};
use crate::vector::Vector; use crate::vector::Vector;
@@ -52,16 +52,11 @@ use crate::vector::Vector;
// //
// **** Features **** // **** Features ****
// //
#[cfg(feature = "f32")]
type FloatType = f32;
cfg_if::cfg_if! { #[cfg(feature = "f64")]
if #[cfg(feature = "f32")] { type FloatType = f64;
/// `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 **** // **** Constants ****
@@ -77,15 +72,9 @@ cfg_if::cfg_if! {
/// ///
/// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector /// - `N`: the dimensionality of the vector portion (i.e. non-height) of the Vivaldi height vector
#[derive(Copy, Clone, Debug, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, Serialize, Deserialize)]
pub struct HeightVector<const N: usize> { pub(crate) 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)] #[serde(flatten)]
position: Vector<FloatType, N>, 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, height: FloatType,
} }
@@ -94,14 +83,19 @@ pub struct HeightVector<const N: usize> {
// //
impl<const N: usize> HeightVector<N> { 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 /// A new height vector is a random unit vector
pub(crate) fn random() -> Self { pub(crate) fn random() -> Self {
let mut rng = thread_rng(); let mut rng = WyRand::new();
let mut vec = [0.0; N]; let mut vec = [0.0; N];
for i in vec.iter_mut().take(N) { for i in vec.iter_mut().take(N) {
*i = rng.gen::<FloatType>() - 0.5; *i = rng.generate::<FloatType>();
} }
let height = rng.gen::<FloatType>().abs(); let height = rng.generate::<FloatType>().abs();
Self { Self {
position: Vector::<FloatType, N>::from(vec), position: Vector::<FloatType, N>::from(vec),
height, height,
@@ -119,16 +113,15 @@ impl<const N: usize> HeightVector<N> {
/// by the inveerse of its length. /// by the inveerse of its length.
pub(crate) fn normalized(&self) -> Self { pub(crate) fn normalized(&self) -> Self {
let len = self.len(); let len = self.len();
// scale it by the inverse of its magnitude like any normal vector if len < FloatType::EPSILON {
let ret = Self {
position: self.position / len,
height: self.height / len,
};
if ret.is_valid() {
ret
} else {
// if we have a bad vector, generate a new random vector // 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 {
position: self.position / len,
height: self.height / len,
}
} }
} }
@@ -160,7 +153,7 @@ impl<const N: usize> Default for HeightVector<N> {
/// Default value for a Vivaldi height vector is just the defaults of its children types. /// Default value for a Vivaldi height vector is just the defaults of its children types.
fn default() -> Self { fn default() -> Self {
Self { Self {
position: Vector::default(), position: Default::default(),
height: Default::default(), height: Default::default(),
} }
} }
@@ -236,37 +229,27 @@ impl<const N: usize> Mul<FloatType> for HeightVector<N> {
// //
// **** Tests **** // **** Tests ****
// //
#[allow(clippy::ignored_unit_patterns)]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use assert_approx_eq::assert_approx_eq; use assert_approx_eq::assert_approx_eq;
use proptest::prelude::*; use proptest::prelude::*;
fn approx_eq(a: FloatType, b: FloatType, margin: FloatType) -> bool {
(a - b).abs() <= margin
}
proptest! { proptest! {
#[test] #[test]
fn proptest_len(x: FloatType, y: FloatType, h: FloatType) { fn proptest_len(x: FloatType, y: FloatType, h: FloatType) {
let len = x.hypot(y) + h.abs(); let len = ((x*x) + (y*y)).sqrt() + h.abs();
let a = HeightVector::<2>::from(([x,y],h)); 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 { 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 // 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 { } else {
// `HeightVector` we created from proptest values should be valid and have a length // `HeightVector` we created from proptest values should be valid and have a length
if len.is_nan() || len.is_infinite() { assert_eq!(a.len(), len);
prop_assert!(a.len().is_nan() || a.len().is_infinite());
} else {
prop_assert!(approx_eq(a.len(), len, 0.0001));
}
} }
} }
#[test] #[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) { 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 // 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 (fx0,fy0,fh0) = (x0 as FloatType / 1_000.0, y0 as FloatType / 1_000.0, h0 as FloatType / 1_000.0);
@@ -275,13 +258,12 @@ mod tests {
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 )); let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 )); let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
let c = a+b; let c = a+b;
prop_assert!(approx_eq(c.position[0], fx0 + fx1, 0.0001)); assert_eq!(c.position[0], fx0 + fx1);
prop_assert!(approx_eq(c.position[1], fy0 + fy1, 0.0001)); assert_eq!(c.position[1], fy0 + fy1);
prop_assert!(approx_eq(c.height, fh0 + fh1, 0.0001)); assert_eq!(c.height, fh0 + fh1);
} }
#[test] #[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) { 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 // 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 (fx0,fy0,fh0) = (x0 as FloatType / 1_000.0, y0 as FloatType / 1_000.0, h0 as FloatType / 1_000.0);
@@ -290,13 +272,12 @@ mod tests {
let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 )); let a = HeightVector::<2>::from(([fx0 ,fy0 ],fh0 ));
let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 )); let b = HeightVector::<2>::from(([fx1 ,fy1 ],fh1 ));
let c = a-b; let c = a-b;
prop_assert!(approx_eq(c.position[0], fx0 - fx1, 0.0001)); assert_eq!(c.position[0], fx0 - fx1);
prop_assert!(approx_eq(c.position[1], fy0 - fy1, 0.0001)); assert_eq!(c.position[1], fy0 - fy1);
prop_assert!(approx_eq(c.height, fh0 + fh1, 0.0001)); assert_eq!(c.height, fh0 + fh1);
} }
#[test] #[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) { 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 // 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); 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);
@@ -304,11 +285,11 @@ mod tests {
let a = HeightVector::<2>::from(([fx, fy], fh)); let a = HeightVector::<2>::from(([fx, fy], fh));
let b = a * fm; let b = a * fm;
if fm < 0.0 { if fm < 0.0 {
prop_assert!(approx_eq(b.len(), 1.0, 0.0001)); assert_approx_eq!(b.len(), 1.0)
} else { } else {
prop_assert!(approx_eq(b.position[0], fx * fm, 0.0001)); assert_eq!(b.position[0], fx * fm);
prop_assert!(approx_eq(b.position[1], fy * fm, 0.0001)); assert_eq!(b.position[1], fy * fm);
prop_assert!(approx_eq(b.height, fh * fm, 0.0001)); assert_eq!(b.height, fh * fm);
} }
} }
} }
@@ -322,7 +303,7 @@ mod tests {
#[test] #[test]
fn test_new() { fn test_new() {
// new gives us a random unit length vec // 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); assert_approx_eq!(a.len(), 1.0);
} }
@@ -336,9 +317,9 @@ mod tests {
#[test] #[test]
fn test_from() { fn test_from() {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)); let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
assert_approx_eq!(a.position[0], 1.0); assert_eq!(a.position[0], 1.0);
assert_approx_eq!(a.position[1], 2.0); assert_eq!(a.position[1], 2.0);
assert_approx_eq!(a.height, 3.0); assert_eq!(a.height, 3.0);
} }
#[test] #[test]
@@ -346,9 +327,9 @@ mod tests {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)); let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0)); let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
let c = a + b; let c = a + b;
assert_approx_eq!(c.position[0], 4.0); assert_eq!(c.position[0], 4.0);
assert_approx_eq!(c.position[1], 4.0); assert_eq!(c.position[1], 4.0);
assert_approx_eq!(c.height, 4.0); assert_eq!(c.height, 4.0);
} }
#[test] #[test]
@@ -356,63 +337,30 @@ mod tests {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)); let a = HeightVector::<2>::from(([1.0, 2.0], 3.0));
let b = HeightVector::<2>::from(([3.0, 2.0], 1.0)); let b = HeightVector::<2>::from(([3.0, 2.0], 1.0));
let c = a - b; let c = a - b;
assert_approx_eq!(c.position[0], -2.0); assert_eq!(c.position[0], -2.0);
assert_approx_eq!(c.position[1], 0.0); assert_eq!(c.position[1], 0.0);
assert_approx_eq!(c.height, 4.0); assert_eq!(c.height, 4.0);
} }
#[test] #[test]
fn test_mul_scalar() { fn test_mul_scalar() {
let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0; let a = HeightVector::<2>::from(([1.0, 2.0], 3.0)) * 10.0;
assert_approx_eq!(a.position[0], 10.0); assert_eq!(a.position[0], 10.0);
assert_approx_eq!(a.position[1], 20.0); assert_eq!(a.position[1], 20.0);
assert_approx_eq!(a.height, 30.0); assert_eq!(a.height, 30.0);
} }
#[test] #[test]
fn test_serde() { fn test_serde() {
// start with JSON, deserialize it // start with JSON, deserialize it
let s = "{\"position\":[1.0,2.0,3.0],\"height\":4.0}"; let s = "{\"position\":[1.0,2.0,3.0],\"height\":4.0}";
let a: HeightVector<3> = let a: HeightVector<3> = serde_json::from_str(s).unwrap();
serde_json::from_str(s).expect("deserialization failed during test");
// make sure it's the right length and works like we expect a normal NC // 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); assert_approx_eq!(a.len(), 7.741_657, 0.001);
// serialize it into a new JSON string and make sure it matches the original // serialize it into a new JSON string and make sure it matches the original
let t = serde_json::to_string(&a); 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_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);
} }
} }
+4 -30
View File
@@ -93,39 +93,13 @@
//! ``` //! ```
//! //!
#![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 height_vector;
mod vector; mod vector;
// make sure we don't have conflicting features
#[cfg(all(feature = "f32", feature = "f64"))]
compile_error!("feature \"f32\" and feature \"f64\" cannot be enabled at the same time");
// publish our interface // publish our interface
pub mod network_coordinate; pub mod network_coordinate;
pub use network_coordinate::NetworkCoordinate; pub use network_coordinate::NetworkCoordinate;
+59 -104
View File
@@ -1,8 +1,31 @@
//! 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 serde::{Deserialize, Serialize};
@@ -14,7 +37,7 @@ use crate::height_vector::HeightVector;
#[cfg(feature = "f32")] #[cfg(feature = "f32")]
type FloatType = f32; type FloatType = f32;
#[cfg(not(feature = "f32"))] #[cfg(feature = "f64")]
type FloatType = f64; type FloatType = f64;
// //
@@ -35,21 +58,6 @@ const MIN_ERROR: FloatType = FloatType::EPSILON;
// **** Structs **** // **** 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)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NetworkCoordinate<const N: usize> { pub struct NetworkCoordinate<const N: usize> {
#[serde(flatten)] #[serde(flatten)]
@@ -62,15 +70,13 @@ pub struct NetworkCoordinate<const N: usize> {
/// A 2D [`NetworkCoordinate`]. Includes a 2D Euclidean position and a height. /// A 2D [`NetworkCoordinate`]. Includes a 2D Euclidean position and a height.
/// ///
/// This type alias is just for convenience. It's functionally equivalent to /// This type alias is just for convenience. It's functionally equivalent to
/// `NetworkCoordinate<2>`. For more information, see [`NetworkCoordinate`]. /// `NetworkCoordinate<2>`.
#[allow(clippy::module_name_repetitions)]
pub type NetworkCoordinate2D = NetworkCoordinate<2>; pub type NetworkCoordinate2D = NetworkCoordinate<2>;
/// A 3D [`NetworkCoordinate`]. Includes a 3D Euclidean position and a height. /// A 3D [`NetworkCoordinate`]. Includes a 3D Euclidean position and a height.
/// ///
/// This type alias is just for convenience. It's functionally equivalent to /// This type alias is just for convenience. It's functionally equivalent to
/// `NetworkCoordinate<3>`. For more information, see [`NetworkCoordinate`]. /// `NetworkCoordinate<3>`.
#[allow(clippy::module_name_repetitions)]
pub type NetworkCoordinate3D = NetworkCoordinate<3>; pub type NetworkCoordinate3D = NetworkCoordinate<3>;
// //
@@ -91,7 +97,6 @@ impl<const N: usize> NetworkCoordinate<N> {
/// // print the NC /// // print the NC
/// println!("Our new NC is: {:#?}", a); /// println!("Our new NC is: {:#?}", a);
/// ``` /// ```
#[must_use]
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
@@ -123,16 +128,13 @@ impl<const N: usize> NetworkCoordinate<N> {
/// println!("Estimated RTT: {}", a.estimated_rtt(&b).as_millis()); /// println!("Estimated RTT: {}", a.estimated_rtt(&b).as_millis());
/// ``` /// ```
/// ///
#[must_use]
pub fn estimated_rtt(&self, rhs: &Self) -> Duration { pub fn estimated_rtt(&self, rhs: &Self) -> Duration {
// estimated rss is euclidean distance between the two plus the sum of the heights // estimated rss is euclidean distance between the two plus the sum of the heights
cfg_if::cfg_if! { #[cfg(feature = "f32")]
if #[cfg(feature = "f32")] { return Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0);
Duration::from_secs_f32((self.heightvec - rhs.heightvec).len() / 1000.0)
} else { #[cfg(feature = "f64")]
Duration::from_secs_f64((self.heightvec - rhs.heightvec).len() / 1000.0) return Duration::from_secs_f64((self.heightvec - rhs.heightvec).len() / 1000.0);
}
}
} }
/// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round /// Given another Vivaldi [`NetworkCoordinate`], adjust our coordinateto better represent the actual round
@@ -150,7 +152,7 @@ impl<const N: usize> NetworkCoordinate<N> {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use core::time::Duration; /// use std::time::Duration;
/// use vivaldi_nc::NetworkCoordinate; /// use vivaldi_nc::NetworkCoordinate;
/// ///
/// // We always have our own NC: /// // We always have our own NC:
@@ -165,51 +167,21 @@ impl<const N: usize> NetworkCoordinate<N> {
/// // Now we can update our NC to adjust our position relative to the remote node: /// // Now we can update our NC to adjust our position relative to the remote node:
/// local.update(&remote, rtt); /// 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 { pub fn update(&mut self, rhs: &Self, rtt: Duration) -> &Self {
// convert Durations into FloatType as fractional milliseconds for convenience // convert Durations into FloatType as fractional milliseconds for convenience
cfg_if::cfg_if! { #[cfg(feature = "f32")]
if #[cfg(feature = "f32")] { let rtt_ms = rtt.as_secs_f32() * 1000.0;
let rtt_ms = rtt.as_secs_f32() * 1000.0; #[cfg(feature = "f32")]
let rtt_estimated_ms = self.estimated_rtt(rhs).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; #[cfg(feature = "f64")]
let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f64() * 1000.0; let rtt_ms = rtt.as_secs_f64() * 1000.0;
} #[cfg(feature = "f64")]
} let rtt_estimated_ms = self.estimated_rtt(rhs).as_secs_f64() * 1000.0;
// rtt needs to be positive // rtt needs to be positive
if rtt_ms < 0.0 { if rtt_ms < 0.0 {
// Note: `rtt` is guaranteed to be positive because `Duration` enforces it. return self;
// If this panics, something changed where `Duration` now allows for negative
// values.
unreachable!();
} }
// Sample weight balances local and remote error. (1) // Sample weight balances local and remote error. (1)
@@ -223,11 +195,7 @@ impl<const N: usize> NetworkCoordinate<N> {
// Update weighted moving average of local error. (3) // Update weighted moving average of local error. (3)
// ei = es × ce × w + ei × (1 − ce × w) // ei = es × ce × w + ei × (1 − ce × w)
// self.error = (es * C_ERROR * w + self.error * (1.0 - C_ERROR * w)).max(MIN_ERROR); 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);
// Update local coordinates. (4) // Update local coordinates. (4)
// δ = cc × w // δ = cc × w
@@ -239,11 +207,7 @@ impl<const N: usize> NetworkCoordinate<N> {
// if we ended up with an invalid coordinate, return a new random coordinate with default // if we ended up with an invalid coordinate, return a new random coordinate with default
// error // error
if self.heightvec.is_invalid() { if self.heightvec.is_invalid() {
*self = Self::new(); *self = Self::new()
// We should never get here because the call to `normalized()` above should catch any
// invalid `heightvec`
unreachable!();
} }
// return reference to updated self // return reference to updated self
@@ -252,8 +216,7 @@ impl<const N: usize> NetworkCoordinate<N> {
/// getter for error value - useful for consumers to understand the estimated accuracty of this /// getter for error value - useful for consumers to understand the estimated accuracty of this
/// `NetworkCoordinate` /// `NetworkCoordinate`
#[must_use] pub fn error(&self) -> FloatType {
pub const fn error(&self) -> FloatType {
self.error self.error
} }
} }
@@ -263,7 +226,7 @@ impl<const N: usize> NetworkCoordinate<N> {
// //
impl<const N: usize> Default for 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 { fn default() -> Self {
Self { Self {
heightvec: HeightVector::<N>::random(), heightvec: HeightVector::<N>::random(),
@@ -330,8 +293,10 @@ mod tests {
let mut mad = NetworkCoordinate::<2>::new(); let mut mad = NetworkCoordinate::<2>::new();
// verify the initial error // verify the initial error
let error = slc.error.hypot(nyc.error.hypot(lax.error.hypot(mad.error))); let error =
assert_approx_eq!(error, 400.0); (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 // iterate plenty of times to converge and minimize error
(0..20).for_each(|_| { (0..20).for_each(|_| {
@@ -356,6 +321,7 @@ mod tests {
// compute and test the root mean squared error // compute and test the root mean squared error
let error = slc.error + nyc.error + lax.error + mad.error; let error = slc.error + nyc.error + lax.error + mad.error;
println!("error = {error}");
assert!(error < 5.0); assert!(error < 5.0);
} }
@@ -363,38 +329,27 @@ mod tests {
fn test_serde() { fn test_serde() {
// start with JSON, deserialize it // start with JSON, deserialize it
let s = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}"; let s = "{\"position\":[1.5,0.5,2.0],\"height\":0.1,\"error\":1.0}";
let a: NetworkCoordinate<3> = let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
serde_json::from_str(s).expect("deserialization failed during test");
// make sure it's the right length and works like we expect a normal NC // 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.heightvec.len(), 2.649_509, 0.001);
assert_approx_eq!(a.error, 1.0); assert_eq!(a.error, 1.0);
assert_eq!(a.estimated_rtt(&a).as_millis(), 0); assert_eq!(a.estimated_rtt(&a).as_millis(), 0);
// serialize it into a new JSON string and make sure it matches the original // serialize it into a new JSON string and make sure it matches the original
let t = serde_json::to_string(&a); 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] #[test]
fn test_estimated_rtt() { fn test_estimated_rtt() {
// start with JSON, deserialize it // start with JSON, deserialize it
let s = "{\"position\":[1.5,0.5,2.0],\"height\":25.0,\"error\":1.0}"; let s = "{\"position\":[1.5,0.5,2.0],\"height\":25.0,\"error\":1.0}";
let a: NetworkCoordinate<3> = let a: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
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 s = "{\"position\":[-1.5,-0.5,-2.0],\"height\":50.0,\"error\":1.0}";
let b: NetworkCoordinate<3> = let b: NetworkCoordinate<3> = serde_json::from_str(s).unwrap();
serde_json::from_str(s).expect("deserialization failed during test");
let estimate = a.estimated_rtt(&b); let estimate = a.estimated_rtt(&b);
assert_approx_eq!(estimate.as_secs_f32(), 0.080_099); 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 -60
View File
@@ -3,12 +3,11 @@
//! This is the simplest possible implementation of a vector to support the needs of a //! This is the simplest possible implementation of a vector to support the needs of a
//! Vivaldi `HeightVector`. //! Vivaldi `HeightVector`.
use array_init::array_init;
use num_traits::Float; use num_traits::Float;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::serde_as; use serde_with::serde_as;
use core::ops::{Add, Div, Index, Mul, Sub}; use std::ops::{Add, Div, Index, Mul, Sub};
// //
// **** Structs **** // **** Structs ****
@@ -21,7 +20,7 @@ use core::ops::{Add, Div, Index, Mul, Sub};
/// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc) /// - `N`: the number of dimensions (i.e. 2 for 2D vectors, 3 for 3D, etc)
#[serde_as] #[serde_as]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Vector<T, const N: usize> pub(crate) struct Vector<T, const N: usize>
where where
T: Float + Serialize + for<'d> Deserialize<'d>, T: Float + Serialize + for<'d> Deserialize<'d>,
{ {
@@ -38,10 +37,17 @@ impl<T, const N: usize> Vector<T, N>
where where
T: Float + Serialize + for<'d> Deserialize<'d>, T: Float + Serialize + for<'d> Deserialize<'d>,
{ {
/// Create a new Zero vector.
pub(crate) fn new() -> Self {
Self::default()
}
/// Compute magnitude of vector. /// Compute magnitude of vector.
pub(crate) fn len(&self) -> T { pub(crate) fn len(&self) -> T {
// use hypot() instead of sqrt() of sum of squares to avoid overflows (0..N)
self.inner.iter().fold(T::zero(), |acc, x| acc.hypot(*x)) .map(|i| self.inner[i] * self.inner[i])
.fold(T::zero(), |sum_sq, x| sum_sq + x)
.sqrt()
} }
/// Checks whether the `Vector` is invalid. /// Checks whether the `Vector` is invalid.
@@ -86,9 +92,11 @@ where
/// Vector+Vector addition. /// Vector+Vector addition.
fn add(self, rhs: Self) -> Self::Output { fn add(self, rhs: Self) -> Self::Output {
Self::Output { let mut ret = [T::zero(); N]; //Self::Output::new();
inner: array_init(|i| self[i] + rhs[i]), for i in 0..N {
ret[i] = self[i] + rhs[i];
} }
Self::Output::from(ret)
} }
} }
@@ -100,9 +108,11 @@ where
/// Vector-Vector subtraction. /// Vector-Vector subtraction.
fn sub(self, rhs: Self) -> Self::Output { fn sub(self, rhs: Self) -> Self::Output {
Self::Output { let mut ret = [T::zero(); N]; //Self::Output::new();
inner: array_init(|i| self[i] - rhs[i]), for i in 0..N {
ret[i] = self[i] - rhs[i];
} }
Self::Output::from(ret)
} }
} }
@@ -114,9 +124,11 @@ where
/// Vector*scalar multiplication. /// Vector*scalar multiplication.
fn mul(self, rhs: T) -> Self::Output { fn mul(self, rhs: T) -> Self::Output {
Self::Output { let mut ret = Self::Output::new();
inner: array_init(|i| self[i] * rhs), for i in 0..N {
ret.inner[i] = self.inner[i] * rhs;
} }
ret
} }
} }
@@ -128,9 +140,11 @@ where
/// Vector/scalar division. /// Vector/scalar division.
fn div(self, rhs: T) -> Self::Output { fn div(self, rhs: T) -> Self::Output {
Self::Output { let mut ret = Self::Output::new();
inner: array_init(|i| self[i] / rhs), for i in 0..N {
ret.inner[i] = self.inner[i] / rhs;
} }
ret
} }
} }
@@ -150,7 +164,6 @@ where
// **** Tests **** // **** Tests ****
// //
#[allow(clippy::ignored_unit_patterns)]
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -158,45 +171,25 @@ mod tests {
use assert_approx_eq::assert_approx_eq; use assert_approx_eq::assert_approx_eq;
use proptest::prelude::*; use proptest::prelude::*;
fn approx_eq(a: f32, b: f32, margin: f32) -> bool {
(a - b).abs() <= margin
}
proptest! { proptest! {
#[test] #[test]
fn proptest_new(x: f32, y: f32, z: f32){ fn proptest_new(x: f32, y: f32, z: f32){
let v = Vector::<f32, 3>::from([x,y,z]); let v = Vector::<f32, 3>::from([x,y,z]);
prop_assert!(approx_eq(v[0], x, 0.0001)); assert_eq!(v[0], x);
prop_assert!(approx_eq(v[1], y, 0.0001)); assert_eq!(v[1], y);
prop_assert!(approx_eq(v[2], z, 0.0001)); assert_eq!(v[2], z);
prop_assert!(v[0].is_finite()); assert!(v[0].is_finite());
prop_assert!(v[1].is_finite()); assert!(v[1].is_finite());
prop_assert!(v[1].is_finite()); assert!(v[1].is_finite());
} }
#[test] #[test]
fn proptest_mul(x: f32, y: f32, z: f32, m: f32) { fn proptest_mul(x: f32, y: f32, z: f32, m: f32) {
let v = Vector::<f32, 3>::from([x,y,z]); let v = Vector::<f32, 3>::from([x,y,z]);
let w = v * m; let w = v * m;
assert_eq!(v[0] * m, w[0]);
let (x,y,z) = (v[0]*m, v[1]*m, v[2]*m); assert_eq!(v[1] * m, w[1]);
if x.is_nan() || x.is_infinite() { assert_eq!(v[2] * m, w[2]);
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));
}
} }
#[test] #[test]
@@ -205,22 +198,23 @@ mod tests {
let w = v / d; let w = v / d;
let (x,y,z) = (v[0]/d, v[1]/d, v[2]/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 { } else {
prop_assert!(approx_eq(w[0], x, 0.0001)); assert_eq!(v[0] / d, x);
} }
if y.is_nan() || y.is_infinite() { if y.is_nan() {
prop_assert!(w[1].is_nan() || w[1].is_infinite()); assert!(w[1].is_nan());
} else { } else {
prop_assert!(approx_eq(w[1], y, 0.0001)); assert_eq!(v[1] / d, y);
} }
if z.is_nan() || z.is_infinite() { if z.is_nan() {
prop_assert!(w[2].is_nan() || w[2].is_infinite()); assert!(w[2].is_nan());
} else { } else {
prop_assert!(approx_eq(w[2], z, 0.0001)); assert_eq!(v[2] / d, z);
} }
} }
} }
@@ -228,7 +222,7 @@ mod tests {
#[test] #[test]
fn test_default_new() { fn test_default_new() {
let a = Vector::<f32, 3>::default(); 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]); let c = Vector::<f32, 3>::from([0.0; 3]);
assert_eq!(a, b); assert_eq!(a, b);
assert_eq!(b, c); assert_eq!(b, c);
@@ -269,9 +263,9 @@ mod tests {
#[test] #[test]
fn test_index() { fn test_index() {
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]); let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
assert_approx_eq!(a[0], 1.0); assert_eq!(a[0], 1.0);
assert_approx_eq!(a[1], 2.0); assert_eq!(a[1], 2.0);
assert_approx_eq!(a[2], 3.0); assert_eq!(a[2], 3.0);
} }
#[test] #[test]
fn test_length() { fn test_length() {
@@ -286,10 +280,7 @@ mod tests {
fn test_serde() { fn test_serde() {
let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]); let a = Vector::<f32, 3>::from([1.0, 2.0, 3.0]);
let s = serde_json::to_string(&a); let s = serde_json::to_string(&a);
assert_eq!( assert_eq!(s.as_ref().unwrap(), "{\"position\":[1.0,2.0,3.0]}");
s.as_ref().expect("serialization failed in test"),
"{\"position\":[1.0,2.0,3.0]}"
);
assert!(s.is_ok()); 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));
}
}
}