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 }