Icarus
Vehicle Simulation as a Transformable Computational Graph, built on Vulcan and Janus
Loading...
Searching...
No Matches
TopologyAnalyzer.hpp
Go to the documentation of this file.
1#pragma once
2
10
11#include <icarus/core/Error.hpp>
14
15#include <algorithm>
16#include <queue>
17#include <set>
18#include <sstream>
19#include <string>
20#include <unordered_map>
21#include <unordered_set>
22#include <vector>
23
24namespace icarus {
25
29struct CycleInfo {
30 std::vector<std::string> components;
31 std::vector<std::string> signals;
32
33 [[nodiscard]] std::string ToString() const {
34 std::ostringstream oss;
35 oss << "Cycle detected: ";
36 for (size_t i = 0; i < components.size(); ++i) {
37 oss << components[i];
38 if (i < components.size() - 1) {
39 oss << " -> ";
40 }
41 }
42 if (!components.empty()) {
43 oss << " -> " << components[0]; // Close the cycle
44 }
45 return oss.str();
46 }
47};
48
53 std::vector<std::string> execution_order;
54 std::vector<CycleInfo> cycles;
55 bool has_cycles = false;
56
57 [[nodiscard]] bool IsValid() const { return !has_cycles; }
58};
59
70 public:
78 void AddEdge(const std::string &producer, const std::string &consumer,
79 const std::string &signal_name = "") {
80 // Register both nodes
81 if (adjacency_.find(producer) == adjacency_.end()) {
82 adjacency_[producer] = {};
83 in_degree_[producer] = 0;
84 }
85 if (adjacency_.find(consumer) == adjacency_.end()) {
86 adjacency_[consumer] = {};
87 in_degree_[consumer] = 0;
88 }
89
90 // Add edge (avoid duplicates)
91 auto &edges = adjacency_[producer];
92 bool already_exists = false;
93 for (const auto &e : edges) {
94 if (e.target == consumer) {
95 already_exists = true;
96 break;
97 }
98 }
99
100 if (!already_exists) {
101 edges.push_back({consumer, signal_name});
102 in_degree_[consumer]++;
103 }
104 }
105
113 void AddNode(const std::string &component) {
114 if (adjacency_.find(component) == adjacency_.end()) {
115 adjacency_[component] = {};
116 in_degree_[component] = 0;
117 }
118 }
119
123 [[nodiscard]] std::vector<std::string> GetNodes() const {
124 std::vector<std::string> nodes;
125 nodes.reserve(adjacency_.size());
126 for (const auto &kv : adjacency_) {
127 nodes.push_back(kv.first);
128 }
129 return nodes;
130 }
131
137 [[nodiscard]] TopologyResult TopologicalSort() const {
138 TopologyResult result;
139
140 // Copy in-degrees for mutation
141 std::unordered_map<std::string, int> in_deg = in_degree_;
142
143 // Queue of nodes with no incoming edges
144 std::queue<std::string> ready;
145 for (const auto &kv : in_deg) {
146 if (kv.second == 0) {
147 ready.push(kv.first);
148 }
149 }
150
151 // Process nodes
152 while (!ready.empty()) {
153 std::string node = ready.front();
154 ready.pop();
155 result.execution_order.push_back(node);
156
157 // Reduce in-degree of neighbors
158 auto it = adjacency_.find(node);
159 if (it != adjacency_.end()) {
160 for (const auto &edge : it->second) {
161 in_deg[edge.target]--;
162 if (in_deg[edge.target] == 0) {
163 ready.push(edge.target);
164 }
165 }
166 }
167 }
168
169 // Check for cycles
170 if (result.execution_order.size() != adjacency_.size()) {
171 result.has_cycles = true;
172 result.cycles = DetectCycles();
173 }
174
175 return result;
176 }
177
183 [[nodiscard]] std::vector<CycleInfo> DetectCycles() const {
184 std::vector<CycleInfo> cycles;
185
186 // Track visited state: 0=unvisited, 1=in-stack, 2=done
187 std::unordered_map<std::string, int> state;
188 for (const auto &kv : adjacency_) {
189 state[kv.first] = 0;
190 }
191
192 std::vector<std::string> path;
193 std::vector<std::string> path_signals;
194
195 for (const auto &kv : adjacency_) {
196 if (state[kv.first] == 0) {
197 DetectCyclesDFS(kv.first, state, path, path_signals, cycles);
198 }
199 }
200
201 return cycles;
202 }
203
207 [[nodiscard]] size_t Size() const { return adjacency_.size(); }
208
212 [[nodiscard]] bool Empty() const { return adjacency_.empty(); }
213
217 [[nodiscard]] std::vector<std::string> GetDependents(const std::string &component) const {
218 std::vector<std::string> deps;
219 auto it = adjacency_.find(component);
220 if (it != adjacency_.end()) {
221 for (const auto &edge : it->second) {
222 deps.push_back(edge.target);
223 }
224 }
225 return deps;
226 }
227
231 [[nodiscard]] std::vector<std::string> GetDependencies(const std::string &component) const {
232 std::vector<std::string> deps;
233 for (const auto &kv : adjacency_) {
234 for (const auto &edge : kv.second) {
235 if (edge.target == component) {
236 deps.push_back(kv.first);
237 break;
238 }
239 }
240 }
241 return deps;
242 }
243
244 private:
245 struct Edge {
246 std::string target;
247 std::string signal; // For diagnostics
248 };
249
250 std::unordered_map<std::string, std::vector<Edge>> adjacency_;
251 std::unordered_map<std::string, int> in_degree_;
252
253 void DetectCyclesDFS(const std::string &node, std::unordered_map<std::string, int> &state,
254 std::vector<std::string> &path, std::vector<std::string> &path_signals,
255 std::vector<CycleInfo> &cycles) const {
256 state[node] = 1; // In stack
257 path.push_back(node);
258
259 auto it = adjacency_.find(node);
260 if (it != adjacency_.end()) {
261 for (const auto &edge : it->second) {
262 if (state[edge.target] == 1) {
263 // Found cycle - extract it
264 CycleInfo cycle;
265 bool in_cycle = false;
266 for (size_t i = 0; i < path.size(); ++i) {
267 if (path[i] == edge.target) {
268 in_cycle = true;
269 }
270 if (in_cycle) {
271 cycle.components.push_back(path[i]);
272 if (i < path_signals.size()) {
273 cycle.signals.push_back(path_signals[i]);
274 }
275 }
276 }
277 cycle.signals.push_back(edge.signal);
278 cycles.push_back(cycle);
279 } else if (state[edge.target] == 0) {
280 path_signals.push_back(edge.signal);
281 DetectCyclesDFS(edge.target, state, path, path_signals, cycles);
282 path_signals.pop_back();
283 }
284 }
285 }
286
287 state[node] = 2; // Done
288 path.pop_back();
289 }
290};
291
296 public:
306 [[nodiscard]] static DependencyGraph
307 BuildGraph(const SimulatorConfig &config, const std::vector<signal::SignalRoute> &routes = {}) {
308 DependencyGraph graph;
309
310 // Build list of known component names for matching
311 std::vector<std::string> sorted_names;
312 for (const auto &comp : config.components) {
313 std::string name = comp.FullPath();
314 sorted_names.push_back(name);
315 graph.AddNode(name);
316 }
317
318 // Sort by descending length to ensure we match the longest prefix first.
319 // If lengths are equal, sort lexicographically for determinism.
320 std::sort(sorted_names.begin(), sorted_names.end(),
321 [](const std::string &a, const std::string &b) {
322 if (a.size() != b.size()) {
323 return a.size() > b.size();
324 }
325 return a < b;
326 });
327
328 // Use provided routes or config routes
329 const auto &route_list = routes.empty() ? config.routes : routes;
330
331 // Helper to find component name from signal path
332 // Matches the longest known component prefix
333 auto find_component = [&](const std::string &signal_path) -> std::string {
334 // Try each component and see if signal starts with it
335 for (const auto &comp_name : sorted_names) {
336 if (signal_path.size() > comp_name.size() &&
337 signal_path.compare(0, comp_name.size(), comp_name) == 0 &&
338 signal_path[comp_name.size()] == '.') {
339 return comp_name;
340 }
341 }
342 // Fallback: take first segment
343 size_t dot = signal_path.find('.');
344 return (dot != std::string::npos) ? signal_path.substr(0, dot) : signal_path;
345 };
346
347 // Add edges from routes
348 for (const auto &route : route_list) {
349 std::string producer = find_component(route.output_path);
350 std::string consumer = find_component(route.input_path);
351
352 if (!producer.empty() && !consumer.empty() && producer != consumer) {
353 graph.AddEdge(producer, consumer, route.output_path);
354 }
355 }
356
357 return graph;
358 }
359
369 const SimulatorConfig &config,
371 DependencyGraph graph = BuildGraph(config);
372 TopologyResult result = graph.TopologicalSort();
373
374 if (result.has_cycles) {
375 switch (cycle_handling) {
377 std::ostringstream oss;
378 oss << "Cyclic dependencies detected in component graph:\n";
379 for (const auto &cycle : result.cycles) {
380 oss << " " << cycle.ToString() << "\n";
381 }
382 throw ConfigError(oss.str());
383 }
386 // Caller should log warning
387 // Include cyclic components in execution order (one-step lag accepted)
388 // Find components that weren't included due to cycles
389 std::set<std::string> included(result.execution_order.begin(),
390 result.execution_order.end());
391 std::vector<std::string> all_nodes = graph.GetNodes();
392
393 // Sort for deterministic ordering
394 std::sort(all_nodes.begin(), all_nodes.end());
395
396 // Add missing (cyclic) components at the end
397 for (const auto &node : all_nodes) {
398 if (included.find(node) == included.end()) {
399 result.execution_order.push_back(node);
400 }
401 }
402 break;
403 }
404 }
405 }
406
407 return result;
408 }
409
420 [[nodiscard]] static SchedulerGroupConfig
421 GenerateSchedulerGroup(const TopologyResult &result, const std::string &group_name = "auto",
422 double rate_hz = 400.0) {
424 group.name = group_name;
425 group.rate_hz = rate_hz;
426 group.priority = 0;
427
428 int priority = 0;
429 for (const auto &component : result.execution_order) {
430 group.members.emplace_back(component, priority++);
431 }
432
433 return group;
434 }
435
445 static void ApplyTopologyOrder(SchedulerConfig &config, const TopologyResult &result) {
446 // Build priority map from topology order
447 std::unordered_map<std::string, int> priority_map;
448 for (size_t i = 0; i < result.execution_order.size(); ++i) {
449 priority_map[result.execution_order[i]] = static_cast<int>(i);
450 }
451
452 // Track which components are already scheduled
453 std::set<std::string> scheduled;
454
455 // Reorder members within each group
456 for (auto &group : config.groups) {
457 for (auto &member : group.members) {
458 auto it = priority_map.find(member.component);
459 if (it != priority_map.end()) {
460 member.priority = it->second;
461 scheduled.insert(member.component);
462 }
463 }
464
465 // Sort members by their topology priority
466 std::sort(
467 group.members.begin(), group.members.end(),
468 [](const GroupMember &a, const GroupMember &b) { return a.priority < b.priority; });
469 }
470
471 // Add unscheduled components to a new group
472 std::vector<std::string> unscheduled;
473 for (const auto &comp : result.execution_order) {
474 if (scheduled.find(comp) == scheduled.end()) {
475 unscheduled.push_back(comp);
476 }
477 }
478
479 if (!unscheduled.empty()) {
480 SchedulerGroupConfig unscheduled_group;
481 unscheduled_group.name = "unscheduled";
482 unscheduled_group.rate_hz = config.groups.empty() ? 400.0 : config.groups[0].rate_hz;
483 unscheduled_group.priority = static_cast<int>(config.groups.size());
484
485 for (const auto &comp : unscheduled) {
486 auto it = priority_map.find(comp);
487 int prio = (it != priority_map.end()) ? it->second : 0;
488 unscheduled_group.members.emplace_back(comp, prio);
489 }
490
491 config.groups.push_back(unscheduled_group);
492 }
493 }
494};
495
496} // namespace icarus
Consolidated error handling for Icarus.
Centralized signal routing configuration.
Simulator and subsystem configuration structs.
Configuration/parsing errors with optional file context.
Definition Error.hpp:185
Dependency graph for component execution ordering.
Definition TopologyAnalyzer.hpp:69
TopologyResult TopologicalSort() const
Perform topological sort using Kahn's algorithm.
Definition TopologyAnalyzer.hpp:137
void AddNode(const std::string &component)
Add a node without any edges.
Definition TopologyAnalyzer.hpp:113
std::vector< std::string > GetDependents(const std::string &component) const
Get dependents of a component (what runs after it).
Definition TopologyAnalyzer.hpp:217
std::vector< CycleInfo > DetectCycles() const
Detect cycles using DFS.
Definition TopologyAnalyzer.hpp:183
void AddEdge(const std::string &producer, const std::string &consumer, const std::string &signal_name="")
Add a dependency edge: producer must execute before consumer.
Definition TopologyAnalyzer.hpp:78
bool Empty() const
Check if graph is empty.
Definition TopologyAnalyzer.hpp:212
std::vector< std::string > GetNodes() const
Get all nodes in the graph.
Definition TopologyAnalyzer.hpp:123
std::vector< std::string > GetDependencies(const std::string &component) const
Get dependencies of a component (what runs before it).
Definition TopologyAnalyzer.hpp:231
size_t Size() const
Get number of nodes.
Definition TopologyAnalyzer.hpp:207
Utility class for building and analyzing component dependency graphs.
Definition TopologyAnalyzer.hpp:295
static SchedulerGroupConfig GenerateSchedulerGroup(const TopologyResult &result, const std::string &group_name="auto", double rate_hz=400.0)
Generate scheduler groups from topological order.
Definition TopologyAnalyzer.hpp:421
static TopologyResult ComputeExecutionOrder(const SimulatorConfig &config, TopologyConfig::CycleHandling cycle_handling=TopologyConfig::CycleHandling::Error)
Compute execution order from configuration.
Definition TopologyAnalyzer.hpp:368
static void ApplyTopologyOrder(SchedulerConfig &config, const TopologyResult &result)
Merge topology order into existing scheduler config.
Definition TopologyAnalyzer.hpp:445
static DependencyGraph BuildGraph(const SimulatorConfig &config, const std::vector< signal::SignalRoute > &routes={})
Build dependency graph from configuration.
Definition TopologyAnalyzer.hpp:307
Definition SignalRouter.hpp:22
Definition AggregationTypes.hpp:13
Information about a detected cycle in the dependency graph.
Definition TopologyAnalyzer.hpp:29
std::vector< std::string > components
Components forming the cycle.
Definition TopologyAnalyzer.hpp:30
std::string ToString() const
Definition TopologyAnalyzer.hpp:33
std::vector< std::string > signals
Signals involved in the cycle.
Definition TopologyAnalyzer.hpp:31
A component member within a scheduler group.
Definition SimulatorConfig.hpp:299
Scheduler configuration with rate groups.
Definition SimulatorConfig.hpp:346
std::vector< SchedulerGroupConfig > groups
Definition SimulatorConfig.hpp:347
A scheduler group with its own rate and priority.
Definition SimulatorConfig.hpp:310
std::vector< GroupMember > members
Definition SimulatorConfig.hpp:314
std::string name
Definition SimulatorConfig.hpp:311
double rate_hz
Must be integer divisor of simulation rate.
Definition SimulatorConfig.hpp:312
int priority
Execution order relative to other groups.
Definition SimulatorConfig.hpp:313
Complete simulation configuration.
Definition SimulatorConfig.hpp:673
std::vector< signal::SignalRoute > routes
Definition SimulatorConfig.hpp:701
std::vector< ComponentConfig > components
Definition SimulatorConfig.hpp:696
CycleHandling
Definition SimulatorConfig.hpp:335
@ BreakAtDelay
Definition SimulatorConfig.hpp:335
@ Warn
Definition SimulatorConfig.hpp:335
@ Error
Definition SimulatorConfig.hpp:335
Result of topological analysis.
Definition TopologyAnalyzer.hpp:52
std::vector< std::string > execution_order
Sorted component names.
Definition TopologyAnalyzer.hpp:53
bool IsValid() const
Definition TopologyAnalyzer.hpp:57
std::vector< CycleInfo > cycles
Detected cycles (if any).
Definition TopologyAnalyzer.hpp:54
bool has_cycles
Quick check for cycles.
Definition TopologyAnalyzer.hpp:55