1use checkpointing::{compressed_binary_checkpointing_system, restore_checkpoint_file};
3use clap::Parser;
4use cli::CliOptions;
5use dynamecs::components::{
6 get_simulation_time, get_step_index, register_default_components, DynamecsAppSettings, SimulationTime, StepIndex,
7 TimeStep,
8};
9use dynamecs::storages::{ImmutableSingularStorage, SingularStorage};
10use dynamecs::{register_component, Component, System, Systems, Universe};
11use eyre::{eyre, Context};
12use serde::{Deserialize, Serialize};
13use std::fs::read_to_string;
14use std::path::{Path, PathBuf};
15use tracing::{debug, info, info_span, instrument, warn};
16
17pub extern crate eyre;
18pub extern crate serde;
19pub extern crate tracing;
20
21mod checkpointing;
22mod cli;
23mod config_override;
24mod tracing_impl;
25
26pub use tracing_impl::register_signal_handler;
27pub use tracing_impl::setup_tracing;
28
29#[derive(Debug)]
30pub struct Scenario {
31 name: String,
32 pub duration: Option<f64>,
33 pub state: Universe,
34 pub pre_systems: Systems,
35 pub simulation_systems: Systems,
36 pub post_systems: Systems,
37}
38
39impl Scenario {
40 pub fn default_with_name(name: impl Into<String>) -> Self {
41 Self {
42 name: name.into(),
43 duration: None,
44 state: Default::default(),
45 pre_systems: Default::default(),
46 simulation_systems: Default::default(),
47 post_systems: Default::default(),
48 }
49 }
50
51 pub fn name(&self) -> &str {
52 &self.name
53 }
54}
55
56pub struct DynamecsApp<Config = ()> {
57 config: Config,
58 scenario: Option<Scenario>,
59 dt_override: Option<f64>,
61 max_steps: Option<usize>,
62 restore_from_checkpoint: Option<PathBuf>,
64 checkpoint_system: Option<Box<dyn System>>,
66}
67
68impl<Config> DynamecsApp<Config> {
69 pub fn from_config_and_app_settings(config: Config) -> Self {
70 Self {
71 config,
72 scenario: None,
73 dt_override: None,
74 max_steps: None,
75 restore_from_checkpoint: None,
76 checkpoint_system: None,
77 }
78 }
79
80 pub fn with_scenario_initializer<I>(mut self, initializer: I) -> eyre::Result<Self>
81 where
82 I: FnOnce(&Config) -> eyre::Result<Scenario>,
83 {
84 let mut scenario = initializer(&self.config)?;
85
86 let scenario_name = scenario.name().to_string();
87 let app_settings = DynamecsAppSettings {
88 scenario_output_dir: get_output_dir().join(&scenario_name),
89 scenario_name,
90 };
91
92 scenario
93 .state
94 .insert_storage(ImmutableSingularStorage::new(app_settings));
95
96 if let Some(dt) = self.dt_override {
97 info!("Overriding time step dt = {}", dt);
98 scenario
99 .state
100 .insert_storage(SingularStorage::new(TimeStep(dt)));
101 }
102
103 self.scenario = Some(scenario);
104 Ok(self)
105 }
106
107 pub fn write_checkpoints(mut self, enable_write_checkpoints: bool) -> Self {
109 self.checkpoint_system = enable_write_checkpoints.then(|| compressed_binary_checkpointing_system().into());
110 self
111 }
112
113 pub fn restore_checkpoint<P: Into<PathBuf>>(mut self, checkpoint_path: P) -> Self {
115 self.restore_from_checkpoint = Some(checkpoint_path.into());
116 self
117 }
118
119 #[instrument(level = "info", skip_all)]
120 pub fn run(mut self) -> eyre::Result<()> {
121 if let Some(scenario) = &mut self.scenario {
122 register_default_components();
124 register_component::<DynamecsAppSettings>();
125 scenario.pre_systems.register_components();
126 scenario.simulation_systems.register_components();
127 scenario.post_systems.register_components();
128
129 if let Some(checkpoint_path) = &self.restore_from_checkpoint {
130 let universe = restore_checkpoint_file(checkpoint_path)?;
131 scenario.state = universe;
132
133 let step_index = get_step_index(&scenario.state).0;
134 info!(
135 "Restored simulation state with step index {} from file \"{}\"",
136 step_index,
137 checkpoint_path.display()
138 );
139 }
140
141 info!("Starting simulation of scenario \"{}\"", scenario.name());
142 loop {
143 let state = &mut scenario.state;
144 let SimulationTime(mut sim_time) = get_simulation_time(&*state);
145 let StepIndex(step_index) = get_step_index(&*state);
146 let TimeStep(dt) = get_time_step_or_set_default(state);
147
148 if let Some(max_steps) = self.max_steps {
149 if step_index > max_steps {
150 break;
151 }
152 } else if let Some(duration) = scenario.duration {
153 if sim_time >= duration {
154 break;
155 }
156 }
157
158 let _span = info_span!("step", step_index).entered();
161
162 if step_index == 0 {
163 debug!("Running post-systems for initial state");
167 {
168 let _span = info_span!("post_systems").entered();
169 scenario.post_systems.run_all(state)?;
170 }
171 }
172
173 info!(
175 "Starting step {} at simulation time {:3.5} (dt = {:3.5e})",
176 step_index, sim_time, dt
177 );
178 {
179 let _span = info_span!("pre_systems").entered();
180 scenario.pre_systems.run_all(state)?;
181 }
182 {
183 let _span = info_span!("simulation_systems").entered();
184 scenario.simulation_systems.run_all(state)?;
185 }
186
187 sim_time += dt;
188 set_singular_component(state, SimulationTime(sim_time));
189 set_singular_component(state, StepIndex(step_index + 1));
190
191 {
192 let _span = info_span!("post_systems").entered();
193 scenario.post_systems.run_all(state)?;
194 }
195
196 if let Some(checkpoint_system) = &mut self.checkpoint_system {
197 checkpoint_system
198 .run(state)
199 .wrap_err("failed to run checkpointing system")?;
200 }
201 }
202
203 info!("Simulation ended");
204 Ok(())
205 } else {
206 Err(eyre!("cannot run scenario: no scenario initializer provided",))
207 }
208 }
209}
210
211fn set_singular_component<C>(state: &mut Universe, component: C)
212where
213 C: Serialize + for<'de> Deserialize<'de>,
214 C: Component<Storage = SingularStorage<C>>,
215{
216 state.insert_storage(SingularStorage::new(component));
217}
218
219fn get_time_step_or_set_default(state: &mut Universe) -> TimeStep {
220 if let Some(storage) = state.try_get_component_storage::<TimeStep>() {
221 storage.get_component().clone()
222 } else {
223 let default_dt = state.get_component_storage::<TimeStep>().get_component();
224 info!("No time step configured. Using default dt = {}", default_dt.0);
225 default_dt.clone()
226 }
227}
228
229impl DynamecsApp<()> {
230 pub fn configure_from_cli<Config>() -> eyre::Result<DynamecsApp<Config>>
231 where
232 Config: Serialize,
233 for<'de> Config: Deserialize<'de>,
234 {
235 let opt = CliOptions::parse();
236
237 info!("Output base path: {}", opt.output_dir.display());
238
239 if opt.config_file.is_some() && opt.config_string.is_some() {
240 return Err(eyre!("config file and config string are mutually exclusive"));
241 }
242
243 let initial_config: Config = if let Some(path) = opt.config_file {
244 info!("Reading config file from {}.", path.display());
245 let config_str =
246 read_to_string(&path).wrap_err_with(|| format!("failed to read config file at {}", path.display()))?;
247 json5::from_str(&config_str).wrap_err("failed to deserialize supplied JSON5 configuration file")
248 } else if let Some(config_str) = opt.config_string {
249 info!("Using configuration provided from CLI interface");
250 json5::from_str(&config_str).wrap_err("failed to deserialize supplied JSON5 configuration string")
251 } else {
252 let default_config_str = "{}";
253 info!(
254 r#"No configuration specified. Trying to use the empty document {} as default."#,
255 default_config_str
256 );
257 Ok(json5::from_str("{}").wrap_err(
258 "failed to deserialize configuration from empty document {}. \
259 You need to either provide all required configuration parameters, \
260 or make sure that your configuration can be deserialized from an empty document,",
261 )?)
262 }?;
263
264 let mut config_json =
265 serde_json::to_value(initial_config).wrap_err("failed to serialize initial config as JSON")?;
266
267 if !opt.overrides.is_empty() {
268 let overridden_config: serde_json::Value =
269 config_override::apply_config_overrides(config_json, &opt.overrides)?;
270 config_json = serde_json::from_value(overridden_config).wrap_err_with(|| {
271 "invalid config overrides: cannot deserialize configuration from \
272 overridden configuration"
273 })?;
274 }
275
276 let mut unknown_fields = false;
279 let config: Config = serde_ignored::deserialize(&config_json, |path| {
280 warn!(
281 "Ignored unknown field {} during deserialization of configuration",
282 path.to_string()
283 );
284 unknown_fields = true;
285 })
286 .wrap_err_with(|| {
287 let json_str = serde_json::to_string_pretty(&config_json)
288 .unwrap_or_else(|err| format!("<failed to serialize to JSON: {err}>"));
289 format!(
290 "failed to deserialize the following JSON configuration \
291 into a valid configuration: \n{json_str}"
292 )
293 })?;
294
295 if unknown_fields && !opt.allow_unknown_config {
296 return Err(eyre!("There were unknown fields in the configuration. Please fix provided config or see --help for how to ignore unknown fields."));
297 }
298
299 let config_json_str = serde_json::to_string_pretty(&config)?;
302 info!("Using configuration: \n{}", config_json_str);
303
304 if let Some(dt) = opt.dt {
305 if dt <= 0.0 {
306 return Err(eyre!("time step dt must be positive"));
307 }
308 }
309
310 let checkpoint_system = opt
311 .write_checkpoints
312 .then(|| compressed_binary_checkpointing_system().into());
313
314 Ok(DynamecsApp {
315 config,
316 scenario: None,
317 dt_override: opt.dt,
318 max_steps: opt.max_steps,
319 restore_from_checkpoint: opt.restore_checkpoint,
320 checkpoint_system,
321 })
322 }
323}
324
325pub fn get_output_dir() -> PathBuf {
329 let cli_args = CliOptions::parse();
330 cli_args.output_dir
331}
332
333pub fn get_default_output_dir() -> &'static Path {
340 Path::new("output")
341}
342
343#[macro_export]
366macro_rules! dynamecs_main {
367 ($scenario:expr) => {
368 fn main() -> Result<(), Box<dyn std::error::Error>> {
369 let _tracing_guard = $crate::setup_tracing()?;
370 $crate::register_signal_handler()?;
371 fn main_internal() -> Result<(), Box<dyn std::error::Error>> {
372 $crate::DynamecsApp::configure_from_cli()?
373 .with_scenario_initializer($scenario)?
374 .run()?;
375 Ok(())
376 }
377
378 main_internal().map_err(|err| {
379 let msg = if let Some(source) = err.source() {
380 format!("{err:#},\ncaused by: {}", source)
381 } else {
382 format!("{err:#}")
383 };
384 $crate::tracing::error!("{msg}");
385 err
386 })
387 }
388 };
389}