Icarus
Vehicle Simulation as a Transformable Computational Graph, built on Vulcan and Janus
Loading...
Searching...
No Matches
SimulationLoader.hpp
Go to the documentation of this file.
1#pragma once
2
15
17#include <icarus/core/Error.hpp>
19#include <iomanip>
20#include <sstream>
21#include <string>
22#include <vector>
23#include <vulcan/io/YamlEnv.hpp>
24#include <vulcan/io/YamlNode.hpp>
25
26namespace icarus::io {
27
44 public:
45 // =========================================================================
46 // Main Loading Methods
47 // =========================================================================
48
60 static SimulatorConfig Load(const std::string &path) {
61 try {
62 auto root = vulcan::io::YamlEnv::LoadWithIncludesAndEnv(path);
63 return ParseRoot(root, path);
64 } catch (const vulcan::io::EnvVarError &e) {
65 // EnvVarError derives from YamlError, must catch first
66 throw icarus::ConfigError("Undefined environment variable: " + e.var_name(), path, -1,
67 "Set the variable or use ${" + e.var_name() + ":default}");
68 } catch (const vulcan::io::YamlError &e) {
69 throw icarus::ConfigError(e.what(), path);
70 }
71 }
72
79 static SimulatorConfig Parse(const std::string &yaml_content) {
80 auto root = vulcan::io::YamlNode::Parse(yaml_content);
81 return ParseRoot(root, "<string>");
82 }
83
84 // =========================================================================
85 // Legacy Methods (for backward compatibility)
86 // =========================================================================
87
92 static std::vector<ComponentConfig> LoadComponents(const std::string &yaml_path) {
93 auto root = vulcan::io::YamlNode::LoadFile(yaml_path);
94 std::vector<ComponentConfig> configs;
95 if (root.Has("components")) {
96 ParseComponentList(configs, root["components"]);
97 }
98 return configs;
99 }
100
104 static ComponentConfig LoadComponent(const vulcan::io::YamlNode &node) {
105 return ParseComponent(node);
106 }
107
111 static EntityTemplate LoadEntityTemplate(const vulcan::io::YamlNode &node) {
112 // Handle both "entity:" wrapper and direct template
113 if (node.Has("entity")) {
114 return ParseEntityTemplateContent(node["entity"]);
115 }
116 return ParseEntityTemplateContent(node);
117 }
118
119 private:
120 // =========================================================================
121 // Root Parsing
122 // =========================================================================
123
124 static SimulatorConfig ParseRoot(const vulcan::io::YamlNode &root,
125 const std::string &source_path) {
126 SimulatorConfig cfg;
127 cfg.source_file = source_path;
128
129 // Parse identity section
130 if (root.Has("simulation")) {
131 ParseSimulationSection(cfg, root["simulation"]);
132 }
133
134 // Parse time section
135 if (root.Has("time")) {
136 ParseTimeSection(cfg, root["time"]);
137 }
138
139 // Either components (single-file) or entities (multi-file) must exist
140 bool has_components = root.Has("components");
141 bool has_entities = root.Has("entities");
142 bool has_swarms = root.Has("swarms");
143
144 if (!has_components && !has_entities && !has_swarms) {
146 "Config must have 'components', 'entities', or 'swarms' section", source_path);
147 }
148
149 if (has_components) {
150 ParseComponentList(cfg.components, root["components"]);
151 }
152
153 // Entity expansion for multi-file mode
154 if (has_entities) {
155 ParseEntities(cfg, root["entities"], source_path);
156 }
157
158 // Swarm expansion for bulk entity spawning
159 if (has_swarms) {
160 ParseSwarms(cfg, root["swarms"], source_path);
161 }
162
163 // Parse routes
164 if (root.Has("routes")) {
165 ParseRoutes(cfg.routes, root["routes"]);
166 }
167 if (root.Has("cross_entity_routes")) {
168 ParseRoutes(cfg.routes, root["cross_entity_routes"]);
169 }
170
171 // Parse scheduler
172 if (root.Has("scheduler")) {
173 ParseScheduler(cfg.scheduler, root["scheduler"]);
174 }
175
176 // Parse integrator
177 if (root.Has("integrator")) {
178 ParseIntegrator(cfg.integrator, root["integrator"]);
179 }
180
181 // Parse logging
182 if (root.Has("logging")) {
183 ParseLogging(cfg.logging, root["logging"]);
184 }
185
186 // Parse recording
187 if (root.Has("recording")) {
188 ParseRecording(cfg.recording, root["recording"]);
189 }
190
191 // Parse staging
192 if (root.Has("staging")) {
193 ParseStaging(cfg.staging, root["staging"]);
194 }
195
196 // Parse phases
197 if (root.Has("phases")) {
198 ParsePhases(cfg.phases, root["phases"]);
199 }
200
201 // Parse output
202 if (root.Has("output")) {
203 ParseOutput(cfg.output, root["output"]);
204 }
205
206 return cfg;
207 }
208
209 // =========================================================================
210 // Section Parsers
211 // =========================================================================
212
213 static void ParseSimulationSection(SimulatorConfig &cfg, const vulcan::io::YamlNode &node) {
214 cfg.name = node.Get<std::string>("name", cfg.name);
215 cfg.version = node.Get<std::string>("version", cfg.version);
216 cfg.description = node.Get<std::string>("description", cfg.description);
217 }
218
219 static void ParseTimeSection(SimulatorConfig &cfg, const vulcan::io::YamlNode &node) {
220 cfg.t_start = node.Get<double>("start", cfg.t_start);
221 cfg.t_end = node.Get<double>("end", cfg.t_end);
222 cfg.dt = node.Get<double>("dt", cfg.dt);
223
224 // Parse epoch configuration if present
225 if (node.Has("epoch")) {
226 auto epoch_node = node["epoch"];
227 cfg.epoch.system = epoch_node.Get<std::string>("system", cfg.epoch.system);
228 cfg.epoch.reference = epoch_node.Get<std::string>("reference", cfg.epoch.reference);
229 cfg.epoch.jd = epoch_node.Get<double>("jd", cfg.epoch.jd);
230 cfg.epoch.gps_week = epoch_node.Get<int>("week", cfg.epoch.gps_week);
231 cfg.epoch.gps_seconds = epoch_node.Get<double>("seconds", cfg.epoch.gps_seconds);
232 // Mark GPS as configured if week was explicitly provided
233 if (epoch_node.Has("week")) {
234 cfg.epoch.gps_configured = true;
235 }
236 }
237 }
238
239 static void ParseComponentList(std::vector<ComponentConfig> &components,
240 const vulcan::io::YamlNode &node) {
241 node.ForEach([&](const vulcan::io::YamlNode &comp_node) {
242 components.push_back(ParseComponent(comp_node));
243 });
244 }
245
246 static ComponentConfig ParseComponent(const vulcan::io::YamlNode &node) {
247 ComponentConfig cfg;
248
249 // Required fields
250 cfg.type = node.Require<std::string>("type");
251 cfg.name = node.Require<std::string>("name");
252
253 // Optional entity prefix
254 cfg.entity = node.Get<std::string>("entity", "");
255
256 // Extract scalars
257 if (node.Has("scalars")) {
258 node["scalars"].ForEachEntry(
259 [&](const std::string &key, const vulcan::io::YamlNode &val) {
260 cfg.scalars[key] = val.As<double>();
261 });
262 }
263
264 // Extract vectors
265 if (node.Has("vectors")) {
266 node["vectors"].ForEachEntry(
267 [&](const std::string &key, const vulcan::io::YamlNode &val) {
268 cfg.vectors[key] = val.ToVector<double>();
269 });
270 }
271
272 // Extract arrays
273 if (node.Has("arrays")) {
274 node["arrays"].ForEachEntry(
275 [&](const std::string &key, const vulcan::io::YamlNode &val) {
276 cfg.arrays[key] = val.ToVector<double>();
277 });
278 }
279
280 // Extract strings
281 if (node.Has("strings")) {
282 node["strings"].ForEachEntry(
283 [&](const std::string &key, const vulcan::io::YamlNode &val) {
284 cfg.strings[key] = val.As<std::string>();
285 });
286 }
287
288 // Extract integers
289 if (node.Has("integers")) {
290 node["integers"].ForEachEntry(
291 [&](const std::string &key, const vulcan::io::YamlNode &val) {
292 cfg.integers[key] = val.As<int64_t>();
293 });
294 }
295
296 // Extract booleans
297 if (node.Has("booleans")) {
298 node["booleans"].ForEachEntry(
299 [&](const std::string &key, const vulcan::io::YamlNode &val) {
300 cfg.booleans[key] = val.As<bool>();
301 });
302 }
303
304 // Extract sources list (for aggregators)
305 if (node.Has("sources")) {
306 cfg.sources = node["sources"].ToVector<std::string>();
307 }
308
309 // Extract frame-categorized source lists (for ForceAggregator)
310 if (node.Has("body_sources")) {
311 cfg.body_sources = node["body_sources"].ToVector<std::string>();
312 }
313 if (node.Has("ecef_sources")) {
314 cfg.ecef_sources = node["ecef_sources"].ToVector<std::string>();
315 }
316
317 return cfg;
318 }
319
320 static void ParseRoutes(std::vector<signal::SignalRoute> &routes,
321 const vulcan::io::YamlNode &node) {
322 node.ForEach([&](const vulcan::io::YamlNode &route_node) {
323 signal::SignalRoute route;
324 route.input_path = route_node.Require<std::string>("input");
325 route.output_path = route_node.Require<std::string>("output");
326 route.gain = route_node.Get<double>("gain", 1.0);
327 route.offset = route_node.Get<double>("offset", 0.0);
328 route.delay = route_node.Get<double>("delay", 0.0);
329 routes.push_back(route);
330 });
331 }
332
333 static void ParseScheduler(SchedulerConfig &scheduler, const vulcan::io::YamlNode &node) {
334 // Clear default groups if explicit groups are provided
335 if (node.Has("groups")) {
336 scheduler.groups.clear();
337 node["groups"].ForEach([&](const vulcan::io::YamlNode &group_node) {
338 SchedulerGroupConfig group;
339 group.name = group_node.Require<std::string>("name");
340 group.rate_hz = group_node.Get<double>("rate_hz", 100.0);
341 group.priority = group_node.Get<int>("priority", 0);
342
343 if (group_node.Has("members")) {
344 group_node["members"].ForEach([&](const vulcan::io::YamlNode &member_node) {
345 GroupMember m;
346 m.component = member_node.Require<std::string>("component");
347 m.priority = member_node.Get<int>("priority", 0);
348 group.members.push_back(m);
349 });
350 }
351
352 // Parse active_phases for phase-based gating
353 if (group_node.Has("active_phases")) {
354 auto phases = group_node["active_phases"].ToVector<int32_t>();
355 group.active_phases = std::set<int32_t>(phases.begin(), phases.end());
356 }
357
358 scheduler.groups.push_back(group);
359 });
360 }
361
362 if (node.Has("topology")) {
363 auto topo = node["topology"];
364 auto mode_str = topo.Get<std::string>("mode", "explicit");
365 scheduler.topology.mode =
366 (mode_str == "automatic") ? SchedulingMode::Automatic : SchedulingMode::Explicit;
367
368 auto cycle_str = topo.Get<std::string>("cycle_detection", "error");
369 if (cycle_str == "warn") {
370 scheduler.topology.cycle_detection = TopologyConfig::CycleHandling::Warn;
371 } else if (cycle_str == "break_at_delay") {
372 scheduler.topology.cycle_detection = TopologyConfig::CycleHandling::BreakAtDelay;
373 } else {
374 scheduler.topology.cycle_detection = TopologyConfig::CycleHandling::Error;
375 }
376
377 scheduler.topology.log_order =
378 topo.Get<bool>("log_order", scheduler.topology.log_order);
379 }
380 }
381
382 static void ParseIntegrator(IntegratorConfig<double> &integrator,
383 const vulcan::io::YamlNode &node) {
384 auto type_str = node.Get<std::string>("type", "RK4");
385 integrator.type = parse_integrator_type(type_str);
386 integrator.abs_tol = node.Get<double>("abs_tol", integrator.abs_tol);
387 integrator.rel_tol = node.Get<double>("rel_tol", integrator.rel_tol);
388 integrator.min_dt = node.Get<double>("dt_min", integrator.min_dt);
389 integrator.max_dt = node.Get<double>("dt_max", integrator.max_dt);
390 }
391
392 static void ParseLogging(LogConfig &logging, const vulcan::io::YamlNode &node) {
393 // Console level
394 if (node.Has("console_level")) {
395 auto level_str = node.Require<std::string>("console_level");
396 logging.console_level = ParseLogLevel(level_str);
397 }
398
399 // File logging
400 logging.file_enabled = node.Get<bool>("file_enabled", logging.file_enabled);
401 logging.file_path = node.Get<std::string>("file_path", logging.file_path);
402 if (node.Has("file_level")) {
403 logging.file_level = ParseLogLevel(node.Require<std::string>("file_level"));
404 }
405
406 // Features
407 logging.progress_enabled = node.Get<bool>("progress_enabled", logging.progress_enabled);
408 logging.profiling_enabled = node.Get<bool>("profiling_enabled", logging.profiling_enabled);
409
410 // Telemetry
411 logging.telemetry_enabled = node.Get<bool>("telemetry_enabled", logging.telemetry_enabled);
412 logging.telemetry_path = node.Get<std::string>("telemetry_path", logging.telemetry_path);
413 if (node.Has("telemetry_signals")) {
414 logging.telemetry_signals = node["telemetry_signals"].ToVector<std::string>();
415 }
416 }
417
418 static void ParseRecording(RecordingConfig &recording, const vulcan::io::YamlNode &node) {
419 recording.enabled = node.Get<bool>("enabled", recording.enabled);
420 recording.path = node.Get<std::string>("path", recording.path);
421 recording.mode = node.Get<std::string>("mode", recording.mode);
422
423 // Include patterns
424 if (node.Has("include")) {
425 recording.include = node["include"].ToVector<std::string>();
426 }
427
428 // Exclude patterns
429 if (node.Has("exclude")) {
430 recording.exclude = node["exclude"].ToVector<std::string>();
431 }
432
433 recording.include_derivatives =
434 node.Get<bool>("include_derivatives", recording.include_derivatives);
435 recording.include_inputs = node.Get<bool>("include_inputs", recording.include_inputs);
436 recording.flush_interval = node.Get<int>("flush_interval", recording.flush_interval);
437 recording.decimation = node.Get<int>("decimation", recording.decimation);
438 recording.export_csv = node.Get<bool>("export_csv", recording.export_csv);
439 }
440
441 static void ParseStaging(StageConfig &staging, const vulcan::io::YamlNode &node) {
442 // Trim config
443 if (node.Has("trim")) {
444 auto trim = node["trim"];
445 staging.trim.enabled = trim.Get<bool>("enabled", false);
446 staging.trim.mode = trim.Get<std::string>("mode", staging.trim.mode);
447 staging.trim.method = trim.Get<std::string>("method", staging.trim.method);
448 staging.trim.tolerance = trim.Get<double>("tolerance", staging.trim.tolerance);
449 staging.trim.max_iterations =
450 trim.Get<int>("max_iterations", staging.trim.max_iterations);
451
452 // Equilibrium mode settings
453 if (trim.Has("zero_derivatives")) {
454 staging.trim.zero_derivatives = trim["zero_derivatives"].ToVector<std::string>();
455 }
456 if (trim.Has("control_signals")) {
457 staging.trim.control_signals = trim["control_signals"].ToVector<std::string>();
458 }
459
460 // Warmstart mode settings
461 staging.trim.recording_path =
462 trim.Get<std::string>("recording_path", staging.trim.recording_path);
463 staging.trim.resume_time = trim.Get<double>("resume_time", staging.trim.resume_time);
464 staging.trim.validate_schema =
465 trim.Get<bool>("validate_schema", staging.trim.validate_schema);
466 }
467
468 // Linearization config
469 if (node.Has("linearization")) {
470 auto lin = node["linearization"];
471 staging.linearization.enabled = lin.Get<bool>("enabled", false);
472 if (lin.Has("states")) {
473 staging.linearization.states = lin["states"].ToVector<std::string>();
474 }
475 if (lin.Has("inputs")) {
476 staging.linearization.inputs = lin["inputs"].ToVector<std::string>();
477 }
478 if (lin.Has("outputs")) {
479 staging.linearization.outputs = lin["outputs"].ToVector<std::string>();
480 }
481 staging.linearization.export_matlab = lin.Get<bool>("export_matlab", false);
482 staging.linearization.export_numpy = lin.Get<bool>("export_numpy", false);
483 staging.linearization.export_json = lin.Get<bool>("export_json", false);
484 staging.linearization.output_dir = lin.Get<std::string>("output_dir", "");
485 }
486
487 // Symbolics config
488 if (node.Has("symbolics")) {
489 auto sym = node["symbolics"];
490 staging.symbolics.enabled = sym.Get<bool>("enabled", false);
491 staging.symbolics.generate_dynamics = sym.Get<bool>("generate_dynamics", true);
492 staging.symbolics.generate_jacobian = sym.Get<bool>("generate_jacobian", false);
493 staging.symbolics.output_dir = sym.Get<std::string>("output_dir", "");
494 }
495
496 // Validation settings
497 staging.validate_wiring = node.Get<bool>("validate_wiring", staging.validate_wiring);
498 staging.warn_on_unwired = node.Get<bool>("warn_on_unwired", staging.warn_on_unwired);
499 }
500
501 static void ParseOutput(OutputConfig &output, const vulcan::io::YamlNode &node) {
502 output.directory = node.Get<std::string>("directory", output.directory);
503 output.data_dictionary = node.Get<bool>("data_dictionary", output.data_dictionary);
504 output.data_dictionary_format =
505 node.Get<std::string>("data_dictionary_format", output.data_dictionary_format);
506 output.telemetry = node.Get<bool>("telemetry", output.telemetry);
507 output.telemetry_format =
508 node.Get<std::string>("telemetry_format", output.telemetry_format);
509 output.timing_report = node.Get<bool>("timing_report", output.timing_report);
510 }
511
512 static void ParsePhases(PhaseConfig &phases, const vulcan::io::YamlNode &node) {
513 // Parse phase definitions: name -> integer value
514 if (node.Has("definitions")) {
515 node["definitions"].ForEachEntry(
516 [&](const std::string &name, const vulcan::io::YamlNode &val) {
517 phases.definitions[name] = val.As<int32_t>();
518 });
519 }
520
521 // Parse initial phase
522 phases.initial_phase = node.Get<std::string>("initial", "");
523
524 // Parse entity prefix (optional)
525 phases.entity_prefix = node.Get<std::string>("entity_prefix", "");
526
527 // Parse transitions
528 if (node.Has("transitions")) {
529 node["transitions"].ForEach([&](const vulcan::io::YamlNode &trans_node) {
530 PhaseTransition trans;
531
532 // Parse 'from' phase (can be string name or -1 for any)
533 if (trans_node.Has("from")) {
534 auto from_str = trans_node.Get<std::string>("from", "");
535 if (from_str == "*" || from_str == "any") {
536 trans.from_phase = -1;
537 } else {
538 auto it = phases.definitions.find(from_str);
539 if (it != phases.definitions.end()) {
540 trans.from_phase = it->second;
541 } else {
542 // Try parsing as integer
543 trans.from_phase = std::stoi(from_str);
544 }
545 }
546 }
547
548 // Parse 'to' phase
549 auto to_str = trans_node.Require<std::string>("to");
550 auto to_it = phases.definitions.find(to_str);
551 if (to_it != phases.definitions.end()) {
552 trans.to_phase = to_it->second;
553 } else {
554 // Try parsing as integer
555 trans.to_phase = std::stoi(to_str);
556 }
557
558 // Parse condition
559 trans.condition = trans_node.Get<std::string>("condition", "");
560
561 phases.transitions.push_back(trans);
562 });
563 }
564 }
565
566 // =========================================================================
567 // Entity Parsing and Expansion
568 // =========================================================================
569
573 static void ParseEntities(SimulatorConfig &cfg, const vulcan::io::YamlNode &node,
574 const std::string &source_path) {
575 node.ForEach([&](const vulcan::io::YamlNode &entity_node) {
576 // Parse entity instance
577 EntityInstance instance;
578 instance.name = entity_node.Require<std::string>("name");
579
580 // Load template (inline or from file via !include)
581 if (entity_node.Has("template")) {
582 // Template is either a YamlNode (from !include) or needs to be loaded
583 auto template_node = entity_node["template"];
584 instance.entity_template = LoadEntityTemplate(template_node);
585 } else if (entity_node.Has("entity")) {
586 // Inline entity definition
587 instance.entity_template = LoadEntityTemplate(entity_node);
588 } else {
589 throw icarus::ConfigError("Entity instance must have 'template' or inline 'entity'",
590 source_path);
591 }
592
593 // Parse overrides
594 if (entity_node.Has("overrides")) {
595 entity_node["overrides"].ForEachEntry(
596 [&](const std::string &comp_name, const vulcan::io::YamlNode &override_node) {
597 instance.overrides[comp_name] = ParseComponentOverride(override_node);
598 });
599 }
600
601 // Expand this entity into flat components and routes
602 ExpandEntity(cfg, instance);
603 });
604 }
605
609 static void ParseSwarms(SimulatorConfig &cfg, const vulcan::io::YamlNode &node,
610 const std::string &source_path) {
611 node.ForEach([&](const vulcan::io::YamlNode &swarm_node) {
612 SwarmConfig swarm;
613 swarm.name_prefix = swarm_node.Require<std::string>("name_prefix");
614 swarm.count = swarm_node.Get<int>("count", 1);
615
616 // Load template
617 if (swarm_node.Has("template")) {
618 swarm.entity_template = LoadEntityTemplate(swarm_node["template"]);
619 } else if (swarm_node.Has("entity")) {
620 swarm.entity_template = LoadEntityTemplate(swarm_node);
621 } else {
622 throw icarus::ConfigError("Swarm must have 'template' or inline 'entity'",
623 source_path);
624 }
625
626 // Expand swarm into entity instances
627 ExpandSwarm(cfg, swarm);
628 });
629 }
630
636 static void ExpandSwarm(SimulatorConfig &cfg, const SwarmConfig &swarm) {
637 for (int i = 0; i < swarm.count; ++i) {
638 // Generate instance name with zero-padded index
639 std::ostringstream oss;
640 oss << swarm.name_prefix << "_" << std::setfill('0') << std::setw(3) << i;
641 std::string instance_name = oss.str();
642
643 // Create entity instance (no overrides for swarm copies)
644 EntityInstance instance;
645 instance.entity_template = swarm.entity_template;
646 instance.name = instance_name;
647
648 // Expand as normal entity
649 ExpandEntity(cfg, instance);
650 }
651 }
652
656 static EntityTemplate ParseEntityTemplateContent(const vulcan::io::YamlNode &content) {
657 EntityTemplate tmpl;
658
659 tmpl.name = content.Get<std::string>("name", "");
660 tmpl.description = content.Get<std::string>("description", "");
661
662 // Parse components
663 if (content.Has("components")) {
664 ParseComponentList(tmpl.components, content["components"]);
665 }
666
667 // Parse internal routes
668 if (content.Has("routes")) {
669 ParseRoutes(tmpl.routes, content["routes"]);
670 }
671
672 // Parse scheduler
673 if (content.Has("scheduler")) {
674 ParseScheduler(tmpl.scheduler, content["scheduler"]);
675 }
676
677 // Parse staging
678 if (content.Has("staging")) {
679 ParseStaging(tmpl.staging, content["staging"]);
680 }
681
682 return tmpl;
683 }
684
688 static ComponentConfig ParseComponentOverride(const vulcan::io::YamlNode &node) {
689 ComponentConfig cfg;
690
691 // Only parse the override fields that are present
692 if (node.Has("scalars")) {
693 node["scalars"].ForEachEntry(
694 [&](const std::string &key, const vulcan::io::YamlNode &val) {
695 cfg.scalars[key] = val.As<double>();
696 });
697 }
698
699 if (node.Has("vectors")) {
700 node["vectors"].ForEachEntry(
701 [&](const std::string &key, const vulcan::io::YamlNode &val) {
702 cfg.vectors[key] = val.ToVector<double>();
703 });
704 }
705
706 if (node.Has("strings")) {
707 node["strings"].ForEachEntry(
708 [&](const std::string &key, const vulcan::io::YamlNode &val) {
709 cfg.strings[key] = val.As<std::string>();
710 });
711 }
712
713 if (node.Has("integers")) {
714 node["integers"].ForEachEntry(
715 [&](const std::string &key, const vulcan::io::YamlNode &val) {
716 cfg.integers[key] = val.As<int64_t>();
717 });
718 }
719
720 if (node.Has("booleans")) {
721 node["booleans"].ForEachEntry(
722 [&](const std::string &key, const vulcan::io::YamlNode &val) {
723 cfg.booleans[key] = val.As<bool>();
724 });
725 }
726
727 return cfg;
728 }
729
733 static void ExpandEntity(SimulatorConfig &cfg, const EntityInstance &instance) {
734 const auto &tmpl = instance.entity_template;
735
736 // Expand components with entity prefix
737 for (auto comp_cfg : tmpl.components) {
738 // Apply overrides if present
739 if (instance.overrides.count(comp_cfg.name)) {
740 comp_cfg = MergeConfigs(comp_cfg, instance.overrides.at(comp_cfg.name));
741 }
742
743 // Set entity prefix
744 comp_cfg.entity = instance.name;
745
746 cfg.components.push_back(comp_cfg);
747 }
748
749 // Expand internal routes (relative -> absolute)
750 for (auto route : tmpl.routes) {
751 route.input_path = instance.name + "." + route.input_path;
752 route.output_path = instance.name + "." + route.output_path;
753 cfg.routes.push_back(route);
754 }
755
756 // Merge scheduler groups (prefix with entity name)
757 for (auto group : tmpl.scheduler.groups) {
758 group.name = instance.name + "." + group.name;
759 for (auto &member : group.members) {
760 member.component = instance.name + "." + member.component;
761 }
762 cfg.scheduler.groups.push_back(group);
763 }
764 }
765
769 static ComponentConfig MergeConfigs(const ComponentConfig &base,
770 const ComponentConfig &overrides) {
771 ComponentConfig merged = base;
772
773 // Overrides replace at the key level
774 for (const auto &[k, v] : overrides.scalars)
775 merged.scalars[k] = v;
776 for (const auto &[k, v] : overrides.vectors)
777 merged.vectors[k] = v;
778 for (const auto &[k, v] : overrides.strings)
779 merged.strings[k] = v;
780 for (const auto &[k, v] : overrides.integers)
781 merged.integers[k] = v;
782 for (const auto &[k, v] : overrides.booleans)
783 merged.booleans[k] = v;
784
785 return merged;
786 }
787
788 // =========================================================================
789 // Helpers
790 // =========================================================================
791
792 static LogLevel ParseLogLevel(const std::string &level_str) {
793 if (level_str == "Trace" || level_str == "trace")
794 return LogLevel::Trace;
795 if (level_str == "Debug" || level_str == "debug")
796 return LogLevel::Debug;
797 if (level_str == "Info" || level_str == "info")
798 return LogLevel::Info;
799 if (level_str == "Warning" || level_str == "warning")
800 return LogLevel::Warning;
801 if (level_str == "Error" || level_str == "error")
802 return LogLevel::Error;
803 if (level_str == "Off" || level_str == "off")
804 return LogLevel::Fatal; // Off not in enum, use Fatal
805 return LogLevel::Info; // Default
806 }
807};
808
809} // namespace icarus::io
810
811// =============================================================================
812// SimulatorConfig::FromFile() implementation
813// =============================================================================
814// Defined here to avoid circular include issues
815
816namespace icarus {
817
818inline SimulatorConfig SimulatorConfig::FromFile(const std::string &path) {
819 return io::SimulationLoader::Load(path);
820}
821
822inline EntityTemplate EntityTemplate::FromFile(const std::string &yaml_path) {
823 auto root = vulcan::io::YamlEnv::LoadWithIncludesAndEnv(yaml_path);
825}
826
827inline std::tuple<std::vector<ComponentConfig>, std::vector<signal::SignalRoute>, SchedulerConfig>
829 std::vector<ComponentConfig> expanded_components;
830 std::vector<signal::SignalRoute> expanded_routes;
831 SchedulerConfig merged_scheduler;
832
833 // Helper to merge overrides
834 auto merge_configs = [](const ComponentConfig &base, const ComponentConfig &overrides) {
835 ComponentConfig merged = base;
836 for (const auto &[k, v] : overrides.scalars)
837 merged.scalars[k] = v;
838 for (const auto &[k, v] : overrides.vectors)
839 merged.vectors[k] = v;
840 for (const auto &[k, v] : overrides.strings)
841 merged.strings[k] = v;
842 for (const auto &[k, v] : overrides.integers)
843 merged.integers[k] = v;
844 for (const auto &[k, v] : overrides.booleans)
845 merged.booleans[k] = v;
846 return merged;
847 };
848
849 // Expand entities
850 for (const auto &instance : entities) {
851 const auto &tmpl = instance.entity_template;
852
853 // Expand components with entity prefix
854 for (auto comp_cfg : tmpl.components) {
855 if (instance.overrides.count(comp_cfg.name)) {
856 comp_cfg = merge_configs(comp_cfg, instance.overrides.at(comp_cfg.name));
857 }
858 comp_cfg.entity = instance.name;
859 expanded_components.push_back(comp_cfg);
860 }
861
862 // Expand internal routes
863 for (auto route : tmpl.routes) {
864 route.input_path = instance.name + "." + route.input_path;
865 route.output_path = instance.name + "." + route.output_path;
866 expanded_routes.push_back(route);
867 }
868
869 // Merge scheduler groups
870 for (auto group : tmpl.scheduler.groups) {
871 group.name = instance.name + "." + group.name;
872 for (auto &member : group.members) {
873 member.component = instance.name + "." + member.component;
874 }
875 merged_scheduler.groups.push_back(group);
876 }
877 }
878
879 // Expand swarms
880 for (const auto &swarm : swarms) {
881 for (int i = 0; i < swarm.count; ++i) {
882 std::ostringstream oss;
883 oss << swarm.name_prefix << "_" << std::setfill('0') << std::setw(3) << i;
884 std::string instance_name = oss.str();
885
886 // Expand components
887 for (auto comp_cfg : swarm.entity_template.components) {
888 comp_cfg.entity = instance_name;
889 expanded_components.push_back(comp_cfg);
890 }
891
892 // Expand routes
893 for (auto route : swarm.entity_template.routes) {
894 route.input_path = instance_name + "." + route.input_path;
895 route.output_path = instance_name + "." + route.output_path;
896 expanded_routes.push_back(route);
897 }
898
899 // Merge scheduler groups
900 for (auto group : swarm.entity_template.scheduler.groups) {
901 group.name = instance_name + "." + group.name;
902 for (auto &member : group.members) {
903 member.component = instance_name + "." + member.component;
904 }
905 merged_scheduler.groups.push_back(group);
906 }
907 }
908 }
909
910 // Add cross-entity routes (already absolute)
911 for (const auto &route : cross_entity_routes) {
912 expanded_routes.push_back(route);
913 }
914
915 return {expanded_components, expanded_routes, merged_scheduler};
916}
917
918} // namespace icarus
Configuration container for components with typed accessors.
Consolidated error handling for Icarus.
Simulator and subsystem configuration structs.
Configuration/parsing errors with optional file context.
Definition Error.hpp:185
Loads complete simulation configuration from YAML.
Definition SimulationLoader.hpp:43
static std::vector< ComponentConfig > LoadComponents(const std::string &yaml_path)
Load just the component list from a file.
Definition SimulationLoader.hpp:92
static ComponentConfig LoadComponent(const vulcan::io::YamlNode &node)
Load a single component config from a YamlNode.
Definition SimulationLoader.hpp:104
static EntityTemplate LoadEntityTemplate(const vulcan::io::YamlNode &node)
Load entity template from YAML node (public API).
Definition SimulationLoader.hpp:111
static SimulatorConfig Load(const std::string &path)
Load complete simulation config from file.
Definition SimulationLoader.hpp:60
static SimulatorConfig Parse(const std::string &yaml_content)
Parse simulation config from YAML string (for testing).
Definition SimulationLoader.hpp:79
Definition ScalarFormat.hpp:17
Definition AggregationTypes.hpp:13
SchedulingMode
Scheduling mode enumeration.
Definition SimulatorConfig.hpp:291
@ Automatic
Topological sort from signal dependencies (Future TODO).
Definition SimulatorConfig.hpp:292
@ Explicit
User-defined execution order.
Definition SimulatorConfig.hpp:293
IntegratorType parse_integrator_type(const std::string &name)
Parse integrator type from string (case-insensitive).
Definition IntegratorTypes.hpp:56
LogLevel
Log severity levels.
Definition Console.hpp:35
@ Warning
Potential issues.
Definition Console.hpp:40
@ Info
Normal operation.
Definition Console.hpp:38
@ Fatal
Unrecoverable errors.
Definition Console.hpp:42
@ Error
Recoverable errors.
Definition Console.hpp:41
@ Debug
Debugging info.
Definition Console.hpp:37
@ Trace
Most verbose, internal debugging.
Definition Console.hpp:36
Configuration container for components.
Definition ComponentConfig.hpp:37
std::unordered_map< std::string, int64_t > integers
Definition ComponentConfig.hpp:54
std::unordered_map< std::string, bool > booleans
Definition ComponentConfig.hpp:55
std::unordered_map< std::string, std::string > strings
Definition ComponentConfig.hpp:53
std::unordered_map< std::string, std::vector< double > > vectors
Vec3 stored as [x,y,z].
Definition ComponentConfig.hpp:51
std::unordered_map< std::string, double > scalars
Definition ComponentConfig.hpp:50
std::vector< signal::SignalRoute > cross_entity_routes
Definition SimulatorConfig.hpp:645
std::vector< EntityInstance > entities
Definition SimulatorConfig.hpp:643
std::tuple< std::vector< ComponentConfig >, std::vector< signal::SignalRoute >, SchedulerConfig > ExpandAll() const
Expand all entities and swarms to flat component list.
Definition SimulationLoader.hpp:828
std::vector< SwarmConfig > swarms
Definition SimulatorConfig.hpp:644
Entity template loaded from YAML.
Definition SimulatorConfig.hpp:568
static EntityTemplate FromFile(const std::string &yaml_path)
Load template from YAML file (implemented in SimulationLoader.hpp).
Definition SimulationLoader.hpp:822
Scheduler configuration with rate groups.
Definition SimulatorConfig.hpp:346
std::vector< SchedulerGroupConfig > groups
Definition SimulatorConfig.hpp:347
Complete simulation configuration.
Definition SimulatorConfig.hpp:673
OutputConfig output
Definition SimulatorConfig.hpp:736
std::string name
Definition SimulatorConfig.hpp:677
SchedulerConfig scheduler
Definition SimulatorConfig.hpp:706
StageConfig staging
Definition SimulatorConfig.hpp:711
RecordingConfig recording
Definition SimulatorConfig.hpp:731
std::vector< signal::SignalRoute > routes
Definition SimulatorConfig.hpp:701
LogConfig logging
Definition SimulatorConfig.hpp:726
PhaseConfig phases
Definition SimulatorConfig.hpp:716
std::string source_file
Path to source YAML file (set by loader).
Definition SimulatorConfig.hpp:680
static SimulatorConfig FromFile(const std::string &path)
Definition SimulationLoader.hpp:818
std::vector< ComponentConfig > components
Definition SimulatorConfig.hpp:696
IntegratorConfig< double > integrator
Definition SimulatorConfig.hpp:721
@ BreakAtDelay
Definition SimulatorConfig.hpp:335
@ Warn
Definition SimulatorConfig.hpp:335
@ Error
Definition SimulatorConfig.hpp:335