A Discrete-Event Network Simulator
API
Loading...
Searching...
No Matches
symmetric-adjacency-matrix.h
Go to the documentation of this file.
1/* Copyright (c) 2025 CTTC
2 *
3 * SPDX-License-Identifier: GPL-2.0-only
4 *
5 * Author: Gabriel Ferreira <gabrielcarvfer@gmail.com>
6 */
7
8#ifndef NS3_SYMMETRIC_ADJACENCY_MATRIX_H
9#define NS3_SYMMETRIC_ADJACENCY_MATRIX_H
10
11#include <algorithm>
12#include <vector>
13
14namespace ns3
15{
16
17/**
18 * @brief A class representing a symmetric adjacency matrix.
19 *
20 * Since the matrix is symmetric, we save up on memory by
21 * storing only the lower left triangle, including the main
22 * diagonal.
23 *
24 * In pseudocode, the matrix is stored as a vector m_matrix, where
25 * each new row is accessed via an offset precomputed in m_rowOffsets.
26 * We also keep track of the number of rows in m_rows.
27 *
28 * A 4x4 matrix would be represented as follows:
29 *
30 * @code
31 * m_matrix= [
32 * 0
33 * 1 2
34 * 3 4 5
35 * 6 7 8 9
36 * ];
37 * m_rowOffsets = [0, 1, 3, 6];
38 * m_rows = 4;
39 * @endcode
40 *
41 * To add a new row (`AddRow()`) in the adjacency matrix (equivalent to an additional node in a
42 bidirected graph),
43 * we need to first add a new offset, then increment the number of rows and finally resize the
44 vector.
45 *
46 * @code
47 * m_rowOffsets.push_back(m_matrix.size());
48 * m_rows++;
49 * m_matrix.resize(m_matrix.size()+m_rows);
50 * @endcode
51 *
52 * The resulting state would be:
53 *
54 * @code
55 * m_rowOffsets = [0, 1, 3, 6, 10];
56 * m_rows = 5;
57 * m_matrix= [
58 * 0
59 * 1 2
60 * 3 4 5
61 * 6 7 8 9
62 * 10 11 12 13 14
63 * ];
64 * @endcode
65 *
66 * In this previous example, the elements of the matrix are
67 * the offset of the values from the beginning of the vector.
68 *
69 * In practice, this matrix could store the state between a given
70 * pair of a link between two nodes. The state could be a boolean
71 * value, in case just tracking valid/invalid,
72 * connected/disconnected link, or numerical types to store
73 * weights, which can be used for routing algorithms.
74 *
75 * The `adjacency-matrix-example` illustrates the usage of the adjacency matrix
76 * in a routing example.
77 *
78 * First we set up the matrix with capacity for 10 nodes.
79 * All values are initialized to maximum, to indicate a disconnected node.
80 *
81 * @code
82 * constexpr float maxFloat = std::numeric_limits<float>::max();
83 * // Create routing weight matrix for 10 nodes and initialize weights to infinity (disconnected)
84 * ns3::SymmetricAdjacencyMatrix<float> routeWeights(10, maxFloat);
85 * @endcode
86 *
87 * We can then map graph nodes into the table rows
88 * @code
89 * // Node | Corresponding matrix row
90 * // A | 0
91 * // B | 1
92 * // C | 2
93 * // D | 3
94 * // E | 4
95 * // F | 5
96 * // G | 6
97 * // H | 7
98 * // I | 8
99 * // J | 9
100 * @endcode
101 *
102 * Then proceed to populate the matrix to reflect the graph
103 *
104 * @code
105 * // A------5-------B-------------14-------C
106 * // \ \ /1|
107 * // \ 3 J |
108 * // \ \ /1 | 7
109 * // 4 E-2-F--4---G--3--H |
110 * // \ 8 / \ |
111 * // D-------- 10--I
112 *
113 * // Distance from nodes to other nodes
114 * routeWeights.SetValue(0, 1, 5); // A-B=5
115 * routeWeights.SetValue(1, 2, 14); // B-C=14
116 * routeWeights.SetValue(0, 3, 4); // A-D=4
117 * routeWeights.SetValue(1, 5, 3); // B-F=3
118 * routeWeights.SetValue(2, 9, 1); // C-J=1
119 * routeWeights.SetValue(9, 7, 1); // J-H=1
120 * routeWeights.SetValue(2, 8, 7); // C-I=7
121 * routeWeights.SetValue(3, 4, 8); // D-E=8
122 * routeWeights.SetValue(4, 5, 2); // E-F=2
123 * routeWeights.SetValue(5, 6, 4); // F-G=4
124 * routeWeights.SetValue(6, 7, 3); // G-H=3
125 * routeWeights.SetValue(7, 8, 10); // H-I=10
126 * @endcode
127 *
128 * Then we set the weights from the nodes to themselves as 0
129 * @code
130 * for (std::size_t i=0; i < routeWeights.GetRows(); i++)
131 * {
132 * routeWeights.SetValue(i, i, 0);
133 * }
134 * @endcode
135 *
136 * Create the known shortest paths
137 * @code
138 * std::map<std::pair<int, int>, std::vector<int>> routeMap;
139 * for (std::size_t i = 0; i < routeWeights.GetRows(); i++)
140 * {
141 * for (std::size_t j = 0; j < routeWeights.GetRows(); j++)
142 * {
143 * if (routeWeights.GetValue(i, j) != maxFloat)
144 * {
145 * if (i != j)
146 * {
147 * routeMap[{i, j}] = {(int)i, (int)j};
148 * }
149 * else
150 * {
151 * routeMap[{i, j}] = {(int)i};
152 * }
153 * }
154 * }
155 * }
156 * @endcode
157 *
158 * And we finally can proceed to assemble paths between nodes
159 * and store them in a routing table. In this case, by brute-force
160 *
161 * @code
162 * for (std::size_t bridgeNode = 0; bridgeNode < routeWeights.GetRows(); bridgeNode++)
163 * {
164 * for (std::size_t srcNode = 0; srcNode < routeWeights.GetRows(); srcNode++)
165 * {
166 * for (std::size_t dstNode = 0; dstNode < routeWeights.GetRows(); dstNode++)
167 * {
168 * auto weightA = routeWeights.GetValue(srcNode, bridgeNode);
169 * auto weightB = routeWeights.GetValue(bridgeNode, dstNode);
170 * // If there is a path between A and bridge, plus bridge and B
171 * if (std::max(weightA, weightB) == maxFloat)
172 * {
173 * continue;
174 * }
175 * // Check if sum of weights is lower than existing path
176 * auto weightAB = routeWeights.GetValue(srcNode, dstNode);
177 * if (weightA + weightB < weightAB)
178 * {
179 * // If it is, update adjacency matrix with the new weight of the shortest
180 * // path
181 * routeWeights.SetValue(srcNode, dstNode, weightA + weightB);
182 *
183 * // Retrieve the partial routes A->bridge and bridge->C,
184 * // and assemble the new route A->bridge->C
185 * const auto srcToBridgeRoute = routeMap.at({srcNode, bridgeNode});
186 * const auto bridgeToDstRoute = routeMap.at({bridgeNode, dstNode});
187 * std::vector<int> dst;
188 * dst.insert(dst.end(), srcToBridgeRoute.begin(), srcToBridgeRoute.end());
189 * dst.insert(dst.end(), bridgeToDstRoute.begin() + 1, bridgeToDstRoute.end());
190 * routeMap[{srcNode, dstNode}] = dst;
191 *
192 * // We also include the reverse path, since the graph is bidirectional
193 * std::vector<int> invDst(dst.rbegin(), dst.rend());
194 * routeMap[{dstNode, srcNode}] = invDst;
195 * }
196 * }
197 * }
198 * }
199 * @endcode
200 *
201 * After this, we have both the complete route, weight of the route, and the weights for each hop in
202 * the route.
203 *
204 * We can print all this information for a given route between nodes srcNodeOpt and
205 * dstNodeOpt with
206 *
207 * @code
208 * std::cout << "route between " << (char)(srcNodeOpt + 'A') << " and "
209 * << (char)(dstNodeOpt + 'A') << " (length "
210 * << routeWeights.GetValue(srcNodeOpt, dstNodeOpt) << "):";
211 * auto lastNodeNumber = srcNodeOpt;
212 * for (auto nodeNumber : routeMap.at({srcNodeOpt, dstNodeOpt}))
213 * {
214 * std::cout << "--" << routeWeights.GetValue(lastNodeNumber, nodeNumber) << "-->"
215 * << (char)('A' + nodeNumber);
216 * lastNodeNumber = nodeNumber;
217 * }
218 * @endcode
219 *
220 * Which, for example, between nodes A and I, would print
221 *
222 * @verbatim
223 route between A and I (length 24):--0-->A--5-->B--3-->F--4-->G--3-->H--1-->J--1-->C--7-->I
224 @endverbatim
225 *
226 * In case one of the links is disconnected, the weights of the adjacency matrix can be reset
227 * with SetValueAdjacent(disconnectedNode, maxFloat).
228 *
229 * Note that, in this implementation, all the routes containing the node need to be removed from
230 * routeMap, and the search needs to be re-executed.
231 */
232template <typename T>
234{
235 public:
236 /**
237 * Default constructor
238 * @param [in] numRows The number of rows in the matrix
239 * @param [in] value The default initialization value of matrix
240 */
241 SymmetricAdjacencyMatrix(std::size_t numRows = 0, T value = {})
242 {
243 m_rows = numRows;
244 m_matrix.resize(m_rows * (m_rows + 1) / 2);
245 std::fill(m_matrix.begin(), m_matrix.end(), value);
246 for (std::size_t i = 0; i < numRows; i++)
247 {
248 m_rowOffsets.push_back(i * (i + 1) / 2);
249 }
250 }
251
252 /**
253 * @brief Retrieve the value of matrix (row, column) node
254 * @param [in] row The row of the matrix to retrieve the value
255 * @param [in] column The column of the matrix to retrieve the value
256 * @return value retrieved from matrix (row, column) or matrix (column, row)
257 */
258 T GetValue(std::size_t row, std::size_t column)
259 {
260 // Highest id should be always row, since we have only half matrix
261 const auto maxIndex = std::max(row, column);
262 const auto minIndex = std::min(row, column);
263 return m_matrix.at(m_rowOffsets.at(maxIndex) + minIndex);
264 }
265
266 /**
267 * @brief Set the value of matrix (row, column) node
268 * @param [in] row The row of the matrix to set the value
269 * @param [in] column The column of the matrix to set the value
270 * @param [in] value value to be assigned to matrix (row, column) or matrix (column, row)
271 */
272 void SetValue(std::size_t row, std::size_t column, T value)
273 {
274 // Highest id should be always row, since we have only half matrix
275 const auto maxIndex = std::max(row, column);
276 const auto minIndex = std::min(row, column);
277 m_matrix.at(m_rowOffsets.at(maxIndex) + minIndex) = value;
278 }
279
280 /**
281 * @brief Set the value of adjacent nodes of a given node (all columns of a given row, and its
282 * reflection)
283 * @param [in] row The row of the matrix to set the value
284 * @param [in] value Value to be assigned to matrix (row, column) or matrix (column, row)
285 */
286 void SetValueAdjacent(std::size_t row, T value)
287 {
288 // Since we only store the lower-left half of the adjacency matrix,
289 // we need to set the adjacent values in both rows and columns involving this row id
290
291 // First set the columns of row m_id
292 for (std::size_t i = 0; i < row; i++)
293 {
294 m_matrix.at(m_rowOffsets.at(row) + i) = value;
295 }
296 // Then set the column m_id of rows >= m_id
297 for (std::size_t i = row; i < m_rows; i++)
298 {
299 m_matrix.at(m_rowOffsets.at(i) + row) = value;
300 }
301 }
302
303 /**
304 * @brief Add new row to the adjacency matrix
305 */
306 void AddRow()
307 {
308 m_rowOffsets.push_back(m_matrix.size());
309 m_rows++;
310 m_matrix.resize(m_matrix.size() + m_rows);
311 }
312
313 /**
314 * @brief Retrieve number of rows in the adjacency matrix
315 * @return the number of rows
316 */
317 std::size_t GetRows()
318 {
319 return m_rows;
320 }
321
322 private:
323 std::size_t m_rows; //!< Number of rows in matrix
324 std::vector<T>
325 m_matrix; //!< The adjacency matrix. For efficiency purposes, we store only lower
326 //!< left half, including the main diagonal. It also is stored as a vector
327 //!< not to introduce gaps between different rows or items (in case T = bool)
328 std::vector<std::size_t> m_rowOffsets; //!< Precomputed row starting offsets of m_matrix
329};
330
331} // namespace ns3
332#endif // NS3_SYMMETRIC_ADJACENCY_MATRIX_H
std::size_t GetRows()
Retrieve number of rows in the adjacency matrix.
std::vector< T > m_matrix
The adjacency matrix.
void SetValue(std::size_t row, std::size_t column, T value)
Set the value of matrix (row, column) node.
T GetValue(std::size_t row, std::size_t column)
Retrieve the value of matrix (row, column) node.
SymmetricAdjacencyMatrix(std::size_t numRows=0, T value={})
Default constructor.
void AddRow()
Add new row to the adjacency matrix.
std::vector< std::size_t > m_rowOffsets
Precomputed row starting offsets of m_matrix.
void SetValueAdjacent(std::size_t row, T value)
Set the value of adjacent nodes of a given node (all columns of a given row, and its reflection).
std::size_t m_rows
Number of rows in matrix.
Every class exported by the ns3 library is enclosed in the ns3 namespace.