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