1 module vivaldi.node;
2 
3 import vivaldi.coordinate;
4 
5 /**
6  * A node is a point in the coordinate system with an estimated
7  * position.
8  *
9  * Node positions may either be represented Vivaldi coordinate, or by
10  * "hybrid" coordinates. In the hybrid model, an additional
11  * non-Euclidean adjustment term is added to each coordinate.
12  *
13  * Adjustment terms improve the performance of the Euclidean
14  * embedding, and therefore can match the performance of
15  * high-dimension embeddings with fewer dimensions (plus the
16  * adjustment term).
17  *
18  * See "On Suitability of Euclidean Embedding forHost-based Network
19  * Coordinate System" by Lee et al.
20  *
21  * Params:
22  *      T = The type of an instantiation of Coordinate.
23  *      window = The number of samples used to compute each adjustment term.
24  */
25 struct Node(T, ushort window = 0)
26 {
27     /**
28      * Given a round-trip time observation for another node at
29      * `other`, updates the estimated position of this Coordinate.
30      */
31     void update(const ref Node other, const double rtt) nothrow @safe @nogc {
32         static if (window > 0) {
33             import std.algorithm : sum;
34 
35             coordinate.update(other.coordinate, rtt, adjustment, other.adjustment);
36             const dist = coordinate.distanceTo(other.coordinate);
37 
38             // NOTE: Rather than choosing landmarks as described in
39             // "On Suitability", sample all nodes. In a passive
40             // system, this is feasible.
41             samples[index] = rtt - dist;
42             index = (index + 1) % window;
43 
44             adjustment = sum(samples[]) / (2.0 * window);
45         } else {
46             coordinate.update(other.coordinate, rtt);
47         }
48     }
49 
50     /**
51      * Returns the distance to `other` in estimated round-trip time.
52      */
53     double distanceTo(const ref Node other) nothrow @safe @nogc {
54         auto dist = coordinate.distanceTo(other.coordinate);
55 
56         static if (window > 0) {
57             import std.algorithm : max;
58             dist = max(dist, dist + adjustment + other.adjustment);
59         }
60 
61         return dist;
62     }
63 
64     /**
65      * The Vivaldi coordinate of this Node.
66      */
67     T coordinate;
68 
69 private:
70 
71     static if (window > 0) {
72         /**
73          * The adjustment term in a hybrid coordinate system. See "On
74          * Suitability" Sec. VI-A.
75          */
76         double adjustment = 0.0;
77 
78         /**
79          * The insertion index for the next sample in the sample window.
80          */
81         size_t index = 0;
82 
83         /**
84          * Samples is a ringbuffer of error terms used to compute an adjustment.
85          */
86         double[window] samples = 0.0;
87     }
88 }
89 
90 @("no adjustment")
91 nothrow @safe @nogc unittest {
92     alias C4 = Coordinate!4;
93 
94     auto a = Node!C4();
95     auto b = Node!C4();
96 
97     a.update(b, 0.2);
98     assert(a.distanceTo(b) > 0);
99 }
100 
101 @("adjustment")
102 nothrow @safe @nogc unittest {
103     version (DigitalMars) {
104         import std.math : isClose;
105     } else version (LDC) {
106         import std.math : approxEqual;
107         alias isClose = approxEqual;
108     }
109 
110     alias C3 = Coordinate!(3, 1.5, 0);
111 
112     auto a = Node!(C3, 20)();
113     auto b = Node!(C3, 20)();
114 
115     a.coordinate.vector = [ -0.5, 1.3, 2.4 ];
116     b.coordinate.vector = [ 1.2, -2.3, 3.4 ];
117 
118     assert(a.distanceTo(a) == 0);
119     assert(a.distanceTo(b) == b.distanceTo(a));
120     assert(isClose(a.distanceTo(b), 4.104875150354758));
121 
122     a.adjustment = -1.0e6;
123     assert(isClose(a.distanceTo(b), 4.104875150354758));
124 
125     a.adjustment = 0.1;
126     b.adjustment = 0.2;
127 
128     assert(isClose(a.distanceTo(b), 4.104875150354758 + 0.3));
129 }