1 module vivaldi.coordinate; 2 3 import std.math : isFinite; 4 5 /** 6 * A helper for validating a double lies within [0.0, 1.0). 7 */ 8 private bool isValid01(double v)() { 9 return v >= 0.0 && v < 1.0; 10 } 11 12 /** 13 * Coordinate represents a point in a Vivaldi network coordinate 14 * system. 15 * 16 * A coordinate has two components: a Euclidean and a non-Euclidean 17 * component. 18 * 19 * The Euclidean component represents the distance across the core of 20 * the Internet, where link distance typically reflects geographic 21 * distance. 22 * 23 * The non-Euclidean component is a "height" offset, which represents 24 * the distance across an access link to the core Internet. Link 25 * distance on access links is typically constrained by bandwidth and 26 * congestion, rather than geography. 27 * 28 * See "Vivaldi: A Decentralized Network Coordinate System" by Dabek, 29 * et al. 30 * 31 * Params: 32 * dims = Dimensionality of the coordinate system. 33 * maxError = Limit to the error any observation may induce. 34 * minHeight = The minimum height of any Coordinate. 35 * ce = A tuning factor which impacts the maximum impact an observation 36 * can have on a Coordinate. 37 * cc = ditto 38 * rho = A tuning factor for the effect of gravity exerted by the origin to 39 * control drift. See "Network Coordinates in the Wild" by 40 * Ledlie, et al. for more information. 41 */ 42 struct Coordinate(size_t dims, 43 double maxError = 1.5, 44 double minHeight = 1.0e-5, 45 double ce = 0.25, 46 double cc = 0.25, 47 double rho = 150.0) 48 if (dims > 0 && isValid01!ce && isValid01!cc && isFinite(rho) && rho > 0.0) 49 { 50 51 /** 52 * Given a round-trip time observation for another node at 53 * `other`, updates the estimated position of this Coordinate. 54 * 55 * The adjustment parameters are used for hybrid coordinates. See 56 * Node. 57 */ 58 void update(const ref Coordinate other, 59 double rtt, 60 const double localAdjustment = 0.0, 61 const double remoteAdjustment = 0.0) 62 nothrow @safe @nogc { 63 64 import std.algorithm : max, min; 65 import std.math : abs, pow; 66 67 assert(isFinite(rtt)); 68 69 double dist = distanceTo(other); 70 dist = max(dist, dist + localAdjustment + remoteAdjustment); 71 72 // Protect against div-by-zero. 73 rtt = max(rtt, double.min_normal); 74 75 // This term is the relative error of this sample. 76 const err = abs(dist - rtt) / rtt; 77 78 // Weight is used to push in proportion to the error: large 79 // error -> large force. 80 const weight = error / max(error + other.error, double.min_normal); 81 82 error = min(err * ce * weight + error * (1.0 - ce * weight), maxError); 83 84 const delta = cc * weight; 85 86 double force = delta * (rtt - dist); 87 88 // Apply the force exerted by the other node. 89 applyForce(other, force); 90 91 scope origin = Coordinate(); 92 93 dist = distanceTo(origin); 94 dist = max(dist, dist + localAdjustment); 95 96 // Gravity toward the origin exerts a pulling force which is a 97 // small fraction of the expected diameter of the network. 98 // "Network Coordinates in the Wild", Sec. 7.2 99 force = -1.0 * pow(dist / rho, 2.0); 100 101 // Apply the force of gravity exerted by the origin. 102 applyForce(origin, force); 103 } 104 105 /** 106 * Returns the Vivaldi distance to `other` in estimated round-trip time. 107 * 108 * To include adjustments in a hybrid coordinate system, see Node. 109 */ 110 double distanceTo(const ref Coordinate other) const pure nothrow @safe @nogc { 111 double[dims] diff = vector[] - other.vector[]; 112 return magnitude(diff) + height + other.height; 113 } 114 115 invariant { 116 /// 117 static bool valid(const double d) pure nothrow @safe @nogc { 118 import std.math : isInfinity, isNaN; 119 return !isInfinity(d) && !isNaN(d); 120 } 121 122 foreach (i; vector) { 123 assert(valid(i)); 124 } 125 126 assert(valid(error)); 127 assert(valid(height)); 128 } 129 130 /** 131 * `vector` is the Euclidean component of the coordinate which 132 * represents the distance from the origin in the Internet core in 133 * seconds. 134 */ 135 double[dims] vector = 0.0; 136 137 /** 138 * `height` is the non-Euclidean component of the coordinate, 139 * which represents the distance along an access link to the 140 * Internet core in seconds. 141 */ 142 double height = minHeight; 143 144 /** 145 * `error` is a unitless measure of confidence that this 146 * Coordinate represents the true distance from the origin. 147 */ 148 double error = maxError; 149 150 /** 151 * Applies a `force` in seconds against this coordinate from the 152 * direction of `other`. 153 * 154 * If force is a positive value, this coordinate will be pushed 155 * away from other. If negative, this coordinate will be pulled 156 * closer to other. 157 */ 158 private void applyForce(const ref Coordinate other, const double force) 159 nothrow @safe @nogc { 160 import std.algorithm : max; 161 162 double[dims] unit; 163 const mag = unitvector(vector, other.vector, unit); 164 165 unit[] *= force; 166 vector[] += unit[]; 167 168 if (mag > double.min_normal) { 169 height = max((height + other.height) * force / mag + height, minHeight); 170 } 171 } 172 173 } 174 175 @("defaults") 176 nothrow @safe @nogc unittest { 177 auto coord = Coordinate!4(); 178 179 assert(coord.vector == [ 0.0, 0.0, 0.0, 0.0 ]); 180 assert(magnitude(coord.vector) == 0.0); 181 assert(coord.error == 1.5); 182 assert(coord.height == 1.0e-5); 183 } 184 185 @("update") 186 nothrow @safe @nogc unittest { 187 auto c = Coordinate!4(); 188 189 assert(c.vector == [ 0.0, 0.0, 0.0, 0.0 ]); 190 191 // Place another node above and nearby; update with a high RTT. 192 auto other = Coordinate!4(); 193 other.vector[2] = 0.001; 194 195 c.update(other, 0.2); 196 197 // The coordinate should be pushed away along the correct axis. 198 assert(c.vector[0] == 0.0); 199 assert(c.vector[1] == 0.0); 200 assert(c.vector[2] < 0.0); 201 assert(c.vector[3] == 0.0); 202 } 203 204 @("constant factors") 205 unittest { 206 // Vivaldi ce term 207 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.0 - double.epsilon))); 208 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 1.0 + double.epsilon))); 209 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, double.nan))); 210 211 // Vivaldi cc term 212 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, 0.0 - double.epsilon))); 213 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, 1.0 + double.epsilon))); 214 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, double.nan))); 215 216 // Gravitational constant 217 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, 0.25, 0.0))); 218 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, 0.25, 0.0 - double.epsilon))); 219 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, 0.25, double.nan))); 220 assert(!__traits(compiles, Coordinate!(4, 1.5, 1.0e-5, 0.25, 0.25, double.infinity))); 221 } 222 223 @("zero rtt") 224 nothrow @safe @nogc unittest { 225 auto c = Coordinate!4(); 226 auto other = Coordinate!4(); 227 228 c.update(other, 0); 229 230 // A zero RTT pushes away regardless. 231 assert(c.distanceTo(other) > 0); 232 233 // The error term should not blow out. 234 assert(c.error == 1.5); 235 } 236 237 @("finite rtt") 238 unittest { 239 import core.exception : AssertError; 240 import std.exception; 241 242 auto a = Coordinate!4(); 243 auto b = Coordinate!4(); 244 245 assertThrown!AssertError(a.update(b, double.infinity)); 246 assertThrown!AssertError(a.update(b, -double.infinity)); 247 assertThrown!AssertError(a.update(b, double.nan)); 248 } 249 250 @("zero error") 251 unittest { 252 auto c = Coordinate!4(); 253 auto other = Coordinate!4(); 254 255 // This test the invariant that total error does not 256 // divide-by-zero and cause the coordinate to be invalid. 257 c.error = 0; 258 other.error = 0; 259 260 c.update(other, 0.1); 261 262 assert(c.error == 0); 263 assert(c.distanceTo(other) > 0); 264 } 265 266 @("distanceTo") 267 nothrow @safe @nogc unittest { 268 version (DigitalMars) { 269 import std.math : isClose; 270 } else version (LDC) { 271 import std.math : approxEqual; 272 alias isClose = approxEqual; 273 } 274 275 auto c1 = Coordinate!(3, 1.5, 0)(); 276 c1.vector = [ -0.5, 1.3, 2.4 ]; 277 278 auto c2 = Coordinate!(3, 1.5, 0)(); 279 c2.vector = [ 1.2, -2.3, 3.4 ]; 280 281 assert(c1.distanceTo(c1) == 0); 282 assert(c1.distanceTo(c2) == c2.distanceTo(c1)); 283 assert(isClose(c1.distanceTo(c2), 4.104875150354758)); 284 285 c1.height = 0.7; 286 c2.height = 0.1; 287 assert(isClose(c1.distanceTo(c2), 4.104875150354758 + 0.8)); 288 } 289 290 @("applyForce zero height") 291 nothrow @safe @nogc unittest { 292 version (DigitalMars) { 293 import std.math : isClose; 294 } else version (LDC) { 295 import std.math : approxEqual; 296 alias isClose = approxEqual; 297 } 298 299 auto origin = Coordinate!(3, 1.5, 0)(); 300 301 auto above = Coordinate!(3, 1.5, 0)(); 302 above.vector = [ 0.0, 0.0, 2.9 ]; 303 304 auto c = origin; 305 c.applyForce(above, 5.3); 306 assert(c.vector == [ 0.0, 0.0, -5.3 ]); 307 308 auto right = Coordinate!(3, 1.5, 0)(); 309 right.vector = [ 3.4, 0.0, -5.3 ]; 310 c.applyForce(right, 2.0); 311 assert(c.vector == [ -2.0, 0.0, -5.3 ]); 312 313 c = origin; 314 c.applyForce(origin, 1.0); 315 assert(isClose(origin.distanceTo(c), 1.0)); 316 } 317 318 @("applyForce default height") 319 @safe unittest { 320 import std.format; 321 322 version (DigitalMars) { 323 import std.math : isClose; 324 } else version (LDC) { 325 import std.math : approxEqual; 326 alias isClose = approxEqual; 327 } 328 329 auto origin = Coordinate!3(); 330 auto c = origin; 331 332 auto above = Coordinate!3(); 333 above.vector = [ 0.0, 0.0, 2.9 ]; 334 above.height = 0.0; 335 336 c.applyForce(above, 5.3); 337 assert(c.vector == [ 0.0, 0.0, -5.3 ]); 338 assert(isClose(c.height, 1.0e-5 + 5.3 * 1.0e-5 / 2.9)); 339 340 c = origin; 341 c.applyForce(above, -5.3); 342 assert(c.vector == [ 0.0, 0.0, 5.3 ]); 343 assert(isClose(c.height, 1.0e-5)); 344 } 345 346 // FIXME: scale to prevent overflow 347 private double magnitude(size_t D)(const double[D] vec) pure nothrow @safe @nogc 348 if (D > 0) 349 { 350 import std.algorithm : map, sum; 351 352 version (DigitalMars) { 353 import std.math.algebraic : sqrt; 354 } else version (LDC) { 355 import std.math : sqrt; 356 } 357 358 return sqrt(sum(vec[].map!(a => a * a))); 359 } 360 361 @("magnitude") 362 nothrow @nogc @safe unittest { 363 version (DigitalMars) { 364 import std.math : isClose; 365 } else version (LDC) { 366 import std.math : approxEqual; 367 alias isClose = approxEqual; 368 } 369 370 assert(magnitude([ 0.0, 0.0, 0.0, 0.0 ]) == 0.0); 371 assert(isClose(magnitude([ 1.0, -2.0, 3.0 ]), 3.7416573867739413)); 372 assert(!__traits(compiles, magnitude([]))); 373 } 374 375 /** 376 * Returns a unit vector pointing at `dest` from `src` in `ret` and 377 * the distance between the two inputs. 378 */ 379 private double unitvector(size_t D)(const double[D] dest, 380 const double[D] src, 381 ref double[D] ret) nothrow @safe @nogc 382 if (D > 0) 383 { 384 import std.random : uniform01; 385 386 double mag; 387 388 ret[] = dest[] - src[]; 389 390 mag = magnitude(ret); 391 // Push if the two vectors aren't too close. 392 if (mag > double.min_normal) { 393 ret[] *= 1.0 / mag; 394 return mag; 395 } 396 397 // Push in a random direction if they _are_ close. 398 // 399 // cf. "Two nodes occupying the same location will have a spring 400 // pushing them away from each other in some arbitrary direction." 401 foreach (ref n; ret) { 402 n = uniform01() - 0.5; 403 } 404 405 mag = magnitude(ret); 406 if (mag > double.min_normal) { 407 ret[] *= 1.0 / mag; 408 return 0.0; 409 } 410 411 // Well, that didn't work out... push along the first dimension; 412 // it is the only dimension all coordinates have. 413 ret[] = 0.0; 414 ret[0] = 1.0; 415 return 0.0; 416 } 417 418 @("unitvector") 419 nothrow @safe @nogc unittest { 420 version (DigitalMars) { 421 import std.math : isClose; 422 } else version (LDC) { 423 import std.math : approxEqual; 424 alias isClose = approxEqual; 425 } 426 427 assert(!__traits(compiles, unitvector([]))); 428 429 double[3] a = [ 1.0, 2.0, 3.0 ]; 430 double[3] b = [ 0.5, 0.6, 0.7 ]; 431 432 double[3] result; 433 434 { 435 auto mag = unitvector(a, b, result); 436 assert(isClose(magnitude(result), 1.0)); 437 438 double[3] diff = a[] - b[]; 439 assert(isClose(mag, magnitude(diff))); 440 441 double[3] expected = [0.18257418583505536, 442 0.511207720338155, 443 0.8398412548412546]; 444 445 foreach (i, v; result) { 446 assert(isClose(v, expected[i])); 447 } 448 } 449 450 auto mag = unitvector(a, a, result); 451 assert(isClose(magnitude(result), 1.0)); 452 assert(isClose(mag, 0.0)); 453 }