62 auto root = vulcan::io::YamlEnv::LoadWithIncludesAndEnv(path);
63 return ParseRoot(root, path);
64 }
catch (
const vulcan::io::EnvVarError &e) {
67 "Set the variable or use ${" + e.var_name() +
":default}");
68 }
catch (
const vulcan::io::YamlError &e) {
80 auto root = vulcan::io::YamlNode::Parse(yaml_content);
81 return ParseRoot(root,
"<string>");
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"]);
105 return ParseComponent(node);
113 if (node.Has(
"entity")) {
114 return ParseEntityTemplateContent(node[
"entity"]);
116 return ParseEntityTemplateContent(node);
125 const std::string &source_path) {
130 if (root.Has(
"simulation")) {
131 ParseSimulationSection(cfg, root[
"simulation"]);
135 if (root.Has(
"time")) {
136 ParseTimeSection(cfg, root[
"time"]);
140 bool has_components = root.Has(
"components");
141 bool has_entities = root.Has(
"entities");
142 bool has_swarms = root.Has(
"swarms");
144 if (!has_components && !has_entities && !has_swarms) {
146 "Config must have 'components', 'entities', or 'swarms' section", source_path);
149 if (has_components) {
150 ParseComponentList(cfg.
components, root[
"components"]);
155 ParseEntities(cfg, root[
"entities"], source_path);
160 ParseSwarms(cfg, root[
"swarms"], source_path);
164 if (root.Has(
"routes")) {
165 ParseRoutes(cfg.
routes, root[
"routes"]);
167 if (root.Has(
"cross_entity_routes")) {
168 ParseRoutes(cfg.
routes, root[
"cross_entity_routes"]);
172 if (root.Has(
"scheduler")) {
173 ParseScheduler(cfg.
scheduler, root[
"scheduler"]);
177 if (root.Has(
"integrator")) {
178 ParseIntegrator(cfg.
integrator, root[
"integrator"]);
182 if (root.Has(
"logging")) {
183 ParseLogging(cfg.
logging, root[
"logging"]);
187 if (root.Has(
"recording")) {
188 ParseRecording(cfg.
recording, root[
"recording"]);
192 if (root.Has(
"staging")) {
193 ParseStaging(cfg.
staging, root[
"staging"]);
197 if (root.Has(
"phases")) {
198 ParsePhases(cfg.
phases, root[
"phases"]);
202 if (root.Has(
"output")) {
203 ParseOutput(cfg.
output, root[
"output"]);
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);
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);
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);
233 if (epoch_node.Has(
"week")) {
234 cfg.epoch.gps_configured =
true;
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));
246 static ComponentConfig ParseComponent(
const vulcan::io::YamlNode &node) {
250 cfg.type = node.Require<std::string>(
"type");
251 cfg.name = node.Require<std::string>(
"name");
254 cfg.entity = node.Get<std::string>(
"entity",
"");
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>();
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>();
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>();
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>();
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>();
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>();
305 if (node.Has(
"sources")) {
306 cfg.sources = node[
"sources"].ToVector<std::string>();
310 if (node.Has(
"body_sources")) {
311 cfg.body_sources = node[
"body_sources"].ToVector<std::string>();
313 if (node.Has(
"ecef_sources")) {
314 cfg.ecef_sources = node[
"ecef_sources"].ToVector<std::string>();
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);
333 static void ParseScheduler(SchedulerConfig &scheduler,
const vulcan::io::YamlNode &node) {
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);
343 if (group_node.Has(
"members")) {
344 group_node[
"members"].ForEach([&](
const vulcan::io::YamlNode &member_node) {
346 m.component = member_node.Require<std::string>(
"component");
347 m.priority = member_node.Get<
int>(
"priority", 0);
348 group.members.push_back(m);
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());
358 scheduler.groups.push_back(group);
362 if (node.Has(
"topology")) {
363 auto topo = node[
"topology"];
364 auto mode_str = topo.Get<std::string>(
"mode",
"explicit");
365 scheduler.topology.mode =
368 auto cycle_str = topo.Get<std::string>(
"cycle_detection",
"error");
369 if (cycle_str ==
"warn") {
371 }
else if (cycle_str ==
"break_at_delay") {
377 scheduler.topology.log_order =
378 topo.Get<
bool>(
"log_order", scheduler.topology.log_order);
382 static void ParseIntegrator(IntegratorConfig<double> &integrator,
383 const vulcan::io::YamlNode &node) {
384 auto type_str = node.Get<std::string>(
"type",
"RK4");
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);
392 static void ParseLogging(LogConfig &logging,
const vulcan::io::YamlNode &node) {
394 if (node.Has(
"console_level")) {
395 auto level_str = node.Require<std::string>(
"console_level");
396 logging.console_level = ParseLogLevel(level_str);
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"));
407 logging.progress_enabled = node.Get<
bool>(
"progress_enabled", logging.progress_enabled);
408 logging.profiling_enabled = node.Get<
bool>(
"profiling_enabled", logging.profiling_enabled);
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>();
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);
424 if (node.Has(
"include")) {
425 recording.include = node[
"include"].ToVector<std::string>();
429 if (node.Has(
"exclude")) {
430 recording.exclude = node[
"exclude"].ToVector<std::string>();
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);
441 static void ParseStaging(StageConfig &staging,
const vulcan::io::YamlNode &node) {
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);
453 if (trim.Has(
"zero_derivatives")) {
454 staging.trim.zero_derivatives = trim[
"zero_derivatives"].ToVector<std::string>();
456 if (trim.Has(
"control_signals")) {
457 staging.trim.control_signals = trim[
"control_signals"].ToVector<std::string>();
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);
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>();
475 if (lin.Has(
"inputs")) {
476 staging.linearization.inputs = lin[
"inputs"].ToVector<std::string>();
478 if (lin.Has(
"outputs")) {
479 staging.linearization.outputs = lin[
"outputs"].ToVector<std::string>();
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",
"");
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",
"");
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);
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);
512 static void ParsePhases(PhaseConfig &phases,
const vulcan::io::YamlNode &node) {
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>();
522 phases.initial_phase = node.Get<std::string>(
"initial",
"");
525 phases.entity_prefix = node.Get<std::string>(
"entity_prefix",
"");
528 if (node.Has(
"transitions")) {
529 node[
"transitions"].ForEach([&](
const vulcan::io::YamlNode &trans_node) {
530 PhaseTransition trans;
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;
538 auto it = phases.definitions.find(from_str);
539 if (it != phases.definitions.end()) {
540 trans.from_phase = it->second;
543 trans.from_phase = std::stoi(from_str);
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;
555 trans.to_phase = std::stoi(to_str);
559 trans.condition = trans_node.Get<std::string>(
"condition",
"");
561 phases.transitions.push_back(trans);
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) {
577 EntityInstance instance;
578 instance.name = entity_node.Require<std::string>(
"name");
581 if (entity_node.Has(
"template")) {
583 auto template_node = entity_node[
"template"];
585 }
else if (entity_node.Has(
"entity")) {
589 throw icarus::ConfigError(
"Entity instance must have 'template' or inline 'entity'",
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);
602 ExpandEntity(cfg, instance);
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) {
613 swarm.name_prefix = swarm_node.Require<std::string>(
"name_prefix");
614 swarm.count = swarm_node.Get<
int>(
"count", 1);
617 if (swarm_node.Has(
"template")) {
619 }
else if (swarm_node.Has(
"entity")) {
622 throw icarus::ConfigError(
"Swarm must have 'template' or inline 'entity'",
627 ExpandSwarm(cfg, swarm);
636 static void ExpandSwarm(SimulatorConfig &cfg,
const SwarmConfig &swarm) {
637 for (
int i = 0; i < swarm.count; ++i) {
639 std::ostringstream oss;
640 oss << swarm.name_prefix <<
"_" << std::setfill(
'0') << std::setw(3) << i;
641 std::string instance_name = oss.str();
644 EntityInstance instance;
645 instance.entity_template = swarm.entity_template;
646 instance.name = instance_name;
649 ExpandEntity(cfg, instance);
656 static EntityTemplate ParseEntityTemplateContent(
const vulcan::io::YamlNode &content) {
659 tmpl.name = content.Get<std::string>(
"name",
"");
660 tmpl.description = content.Get<std::string>(
"description",
"");
663 if (content.Has(
"components")) {
664 ParseComponentList(tmpl.components, content[
"components"]);
668 if (content.Has(
"routes")) {
669 ParseRoutes(tmpl.routes, content[
"routes"]);
673 if (content.Has(
"scheduler")) {
674 ParseScheduler(tmpl.scheduler, content[
"scheduler"]);
678 if (content.Has(
"staging")) {
679 ParseStaging(tmpl.staging, content[
"staging"]);
688 static ComponentConfig ParseComponentOverride(
const vulcan::io::YamlNode &node) {
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>();
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>();
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>();
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>();
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>();
733 static void ExpandEntity(SimulatorConfig &cfg,
const EntityInstance &instance) {
734 const auto &tmpl = instance.entity_template;
737 for (
auto comp_cfg : tmpl.components) {
739 if (instance.overrides.count(comp_cfg.name)) {
740 comp_cfg = MergeConfigs(comp_cfg, instance.overrides.at(comp_cfg.name));
744 comp_cfg.entity = instance.name;
746 cfg.components.push_back(comp_cfg);
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);
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;
762 cfg.scheduler.groups.push_back(group);
769 static ComponentConfig MergeConfigs(
const ComponentConfig &base,
770 const ComponentConfig &overrides) {
771 ComponentConfig merged = base;
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;
792 static LogLevel ParseLogLevel(
const std::string &level_str) {
793 if (level_str ==
"Trace" || level_str ==
"trace")
795 if (level_str ==
"Debug" || level_str ==
"debug")
797 if (level_str ==
"Info" || level_str ==
"info")
799 if (level_str ==
"Warning" || level_str ==
"warning")
801 if (level_str ==
"Error" || level_str ==
"error")
803 if (level_str ==
"Off" || level_str ==
"off")