1use crate::edd::v2::{validate_emc_yaml, validate_experiment_yaml, SchemaValidationError};
7use crate::edd::{ExperimentRunner, RunnerConfig};
8use std::path::Path;
9use std::process::ExitCode;
10
11use super::args::RenderFormat;
12use super::output::{
13 print_emc_report, print_emc_validation_results, print_experiment_result, print_help,
14 print_version,
15};
16use super::schema::validate_emc_schema;
17use super::{Args, Command};
18
19#[must_use]
23pub fn run_cli(args: Args) -> ExitCode {
24 match args.command {
25 Command::Run {
26 experiment_path,
27 seed_override,
28 verbose,
29 } => run_experiment(&experiment_path, seed_override, verbose),
30 Command::Render {
31 domain,
32 format,
33 output,
34 fps,
35 duration,
36 seed,
37 } => render_svg(&domain, &format, &output, fps, duration, seed),
38 Command::Validate { experiment_path } => validate_experiment(&experiment_path),
39 Command::Verify {
40 experiment_path,
41 runs,
42 } => verify_reproducibility(&experiment_path, runs),
43 Command::EmcCheck { experiment_path } => emc_check(&experiment_path),
44 Command::EmcValidate { emc_path } => emc_validate(&emc_path),
45 Command::ListEmc => list_emc(),
46 Command::Help => {
47 print_help();
48 ExitCode::SUCCESS
49 }
50 Command::Version => {
51 print_version();
52 ExitCode::SUCCESS
53 }
54 Command::Error(msg) => {
55 eprintln!("Error: {msg}");
56 print_help();
57 ExitCode::FAILURE
58 }
59 }
60}
61
62#[must_use]
70pub fn run_experiment(path: &Path, seed_override: Option<u64>, verbose: bool) -> ExitCode {
71 println!("╔═══════════════════════════════════════════════════════════════╗");
72 println!("║ simular - EDD Experiment Runner ║");
73 println!("╚═══════════════════════════════════════════════════════════════╝\n");
74
75 let config = RunnerConfig {
76 seed_override,
77 verbose,
78 ..RunnerConfig::default()
79 };
80
81 let mut runner = ExperimentRunner::with_config(config);
82
83 match runner.initialize() {
85 Ok(count) => {
86 if verbose {
87 println!("Loaded {count} EMCs from library");
88 }
89 }
90 Err(e) => {
91 eprintln!("Warning: Failed to scan EMC library: {e}");
92 }
93 }
94
95 println!("Running experiment: {}\n", path.display());
96
97 match runner.run(path) {
98 Ok(result) => {
99 print_experiment_result(&result, verbose);
100 if result.passed {
101 ExitCode::SUCCESS
102 } else {
103 ExitCode::from(1)
104 }
105 }
106 Err(e) => {
107 eprintln!("Error: {e}");
108 ExitCode::from(1)
109 }
110 }
111}
112
113#[must_use]
119pub fn validate_experiment(path: &Path) -> ExitCode {
120 println!("╔═══════════════════════════════════════════════════════════════╗");
121 println!("║ simular - EDD v2 Experiment Schema Validation ║");
122 println!("╚═══════════════════════════════════════════════════════════════╝\n");
123
124 println!("Validating: {}\n", path.display());
125
126 let contents = match std::fs::read_to_string(path) {
128 Ok(c) => c,
129 Err(e) => {
130 eprintln!("✗ Error reading file: {e}");
131 return ExitCode::from(1);
132 }
133 };
134
135 match validate_experiment_yaml(&contents) {
137 Ok(()) => {
138 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
139 println!("✓ Schema validation PASSED");
140 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
141 println!("EDD v2 Compliance:");
142 println!(" ✓ Required fields present (id, seed, emc_ref, simulation, falsification)");
143 println!(" ✓ Falsification criteria defined");
144 println!(" ✓ No prohibited custom code fields");
145 println!("\nNext steps:");
146 println!(" • Run: simular run {}", path.display());
147 println!(" • Verify: simular verify {}", path.display());
148 ExitCode::SUCCESS
149 }
150 Err(e) => {
151 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
152 println!("✗ Schema validation FAILED");
153 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
154 match e {
155 SchemaValidationError::ValidationFailed(errors) => {
156 println!("Validation errors:");
157 for (i, err) in errors.iter().enumerate() {
158 println!(" {}. {err}", i + 1);
159 }
160 }
161 other => {
162 println!("Error: {other}");
163 }
164 }
165 println!("\nSee: docs/specifications/EDD-spec-unified.md for schema requirements");
166 ExitCode::from(1)
167 }
168 }
169}
170
171#[must_use]
178pub fn verify_reproducibility(path: &Path, runs: usize) -> ExitCode {
179 println!("╔═══════════════════════════════════════════════════════════════╗");
180 println!("║ simular - Reproducibility Verification ║");
181 println!("╚═══════════════════════════════════════════════════════════════╝\n");
182
183 let config = RunnerConfig {
184 verify_reproducibility: true,
185 reproducibility_runs: runs,
186 ..RunnerConfig::default()
187 };
188
189 let mut runner = ExperimentRunner::with_config(config);
190
191 if let Err(e) = runner.initialize() {
192 eprintln!("Warning: Failed to scan EMC library: {e}");
193 }
194
195 println!("Verifying reproducibility: {}", path.display());
196 println!("Runs: {runs}\n");
197
198 match runner.verify(path) {
199 Ok(summary) => {
200 let status = if summary.passed { "PASSED" } else { "FAILED" };
201 let sym = if summary.passed { "✓" } else { "✗" };
202
203 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
204 println!("Reproducibility Check");
205 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
206
207 println!(" Runs: {}", summary.runs);
208 println!(" Identical: {}", summary.identical);
209 println!(" Platform: {}", summary.platform);
210 println!("\n Reference Hash: {}", summary.reference_hash);
211
212 if summary.run_hashes.len() > 1 {
213 println!("\n Run Hashes:");
214 for (i, hash) in summary.run_hashes.iter().enumerate() {
215 let match_sym = if hash == &summary.reference_hash {
216 "="
217 } else {
218 "!"
219 };
220 println!(" Run {}: {} {}", i + 1, hash, match_sym);
221 }
222 }
223
224 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
225 println!("{sym} Result: {status}");
226 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
227
228 if summary.passed {
229 ExitCode::SUCCESS
230 } else {
231 ExitCode::from(1)
232 }
233 }
234 Err(e) => {
235 eprintln!("Error: {e}");
236 ExitCode::from(1)
237 }
238 }
239}
240
241#[must_use]
247pub fn emc_check(path: &Path) -> ExitCode {
248 println!("╔═══════════════════════════════════════════════════════════════╗");
249 println!("║ simular - EMC Compliance Check ║");
250 println!("╚═══════════════════════════════════════════════════════════════╝\n");
251
252 let mut runner = ExperimentRunner::new();
253
254 if let Err(e) = runner.initialize() {
255 eprintln!("Warning: Failed to scan EMC library: {e}");
256 }
257
258 println!("Checking EMC compliance: {}\n", path.display());
259
260 match runner.emc_check(path) {
261 Ok(report) => {
262 print_emc_report(&report);
263 if report.passed {
264 ExitCode::SUCCESS
265 } else {
266 ExitCode::from(1)
267 }
268 }
269 Err(e) => {
270 eprintln!("Error: {e}");
271 ExitCode::from(1)
272 }
273 }
274}
275
276#[must_use]
278pub fn list_emc() -> ExitCode {
279 println!("╔═══════════════════════════════════════════════════════════════╗");
280 println!("║ simular - EMC Library ║");
281 println!("╚═══════════════════════════════════════════════════════════════╝\n");
282
283 let mut runner = ExperimentRunner::new();
284
285 match runner.initialize() {
286 Ok(count) => {
287 println!("Found {count} EMCs in library:\n");
288
289 let references = runner.registry_mut().list_references();
290 let mut sorted: Vec<_> = references.into_iter().collect();
291 sorted.sort_unstable();
292
293 let mut current_domain = String::new();
294
295 for reference in &sorted {
296 let parts: Vec<&str> = reference.split('/').collect();
297 if parts.len() >= 2 {
298 let domain = parts[0];
299 if domain != current_domain {
300 if !current_domain.is_empty() {
301 println!();
302 }
303 println!("{}:", domain.to_uppercase());
304 current_domain = domain.to_string();
305 }
306 }
307 println!(" - {reference}");
308 }
309
310 println!("\nUsage: simular run <experiment.yaml>");
311 println!(
312 "Reference EMCs using: equation_model_card.emc_ref: \"{}\"",
313 if sorted.is_empty() {
314 "domain/name"
315 } else {
316 sorted[0]
317 }
318 );
319
320 ExitCode::SUCCESS
321 }
322 Err(e) => {
323 eprintln!("Error scanning EMC library: {e}");
324 ExitCode::from(1)
325 }
326 }
327}
328
329#[must_use]
335pub fn emc_validate(path: &Path) -> ExitCode {
336 println!("╔═══════════════════════════════════════════════════════════════╗");
337 println!("║ simular - EDD v2 EMC Schema Validation ║");
338 println!("╚═══════════════════════════════════════════════════════════════╝\n");
339 println!("Validating EMC: {}\n", path.display());
340
341 let contents = match std::fs::read_to_string(path) {
342 Ok(c) => c,
343 Err(e) => {
344 eprintln!("✗ Error reading file: {e}");
345 return ExitCode::from(1);
346 }
347 };
348
349 match validate_emc_yaml(&contents) {
351 Ok(()) => {
352 println!("✓ EDD v2 JSON Schema validation PASSED\n");
353 }
354 Err(e) => {
355 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
356 println!("✗ EDD v2 JSON Schema validation FAILED");
357 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
358 match e {
359 SchemaValidationError::ValidationFailed(errors) => {
360 println!("Validation errors:");
361 for (i, err) in errors.iter().enumerate() {
362 println!(" {}. {err}", i + 1);
363 }
364 }
365 other => {
366 println!("Error: {other}");
367 }
368 }
369 return ExitCode::from(1);
370 }
371 }
372
373 let yaml: serde_yaml::Value = match serde_yaml::from_str(&contents) {
375 Ok(y) => y,
376 Err(e) => {
377 eprintln!("YAML Syntax Error: {e}");
378 return ExitCode::from(1);
379 }
380 };
381
382 let (errors, warnings) = validate_emc_schema(&yaml);
383 print_emc_validation_results(&yaml, &errors, &warnings);
384
385 if errors.is_empty() {
386 println!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
387 println!("✓ All EMC validations PASSED");
388 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
389 ExitCode::SUCCESS
390 } else {
391 ExitCode::from(1)
392 }
393}
394
395struct RenderCtx<'a> {
397 format: &'a RenderFormat,
398 output: &'a Path,
399 svg: &'a mut crate::renderers::svg::SvgRenderer,
400 keyframes: &'a mut crate::renderers::keyframes::KeyframeRecorder,
401}
402
403fn write_frame(
405 frame: usize,
406 commands: &[crate::orbit::render::RenderCommand],
407 ctx: &mut RenderCtx<'_>,
408) -> Result<(), String> {
409 match ctx.format {
410 RenderFormat::SvgFrames => {
411 let svg = ctx.svg.render(commands);
412 let path = ctx.output.join(format!("frame_{frame:04}.svg"));
413 std::fs::write(&path, &svg).map_err(|e| format!("Error writing frame {frame}: {e}"))
414 }
415 RenderFormat::SvgKeyframes => {
416 ctx.keyframes.record_frame(commands);
417 if frame == 0 {
418 let svg = ctx.svg.render(commands);
419 let path = ctx.output.join("scene.svg");
420 std::fs::write(&path, &svg).map_err(|e| format!("Error writing template SVG: {e}"))
421 } else {
422 Ok(())
423 }
424 }
425 }
426}
427
428pub fn render_svg(
430 domain: &str,
431 format: &RenderFormat,
432 output: &Path,
433 fps: u32,
434 duration: f64,
435 seed: u64,
436) -> ExitCode {
437 use crate::renderers::keyframes::KeyframeRecorder;
438 use crate::renderers::svg::SvgRenderer;
439
440 println!("╔═══════════════════════════════════════════════════════════════╗");
441 println!("║ simular - SVG Render Pipeline ║");
442 println!("╚═══════════════════════════════════════════════════════════════╝\n");
443
444 if let Err(e) = std::fs::create_dir_all(output) {
445 eprintln!("Error creating output directory: {e}");
446 return ExitCode::from(1);
447 }
448
449 let format_name = match format {
450 RenderFormat::SvgFrames => "svg-frames",
451 RenderFormat::SvgKeyframes => "svg-keyframes",
452 };
453 println!("Domain: {domain}");
454 println!("Format: {format_name}");
455 println!("Output: {}", output.display());
456 println!("FPS: {fps}");
457 println!("Duration: {duration}s");
458 println!("Seed: {seed}\n");
459
460 let total_frames = (duration * f64::from(fps)) as usize;
461 let mut svg_renderer = SvgRenderer::new();
462 let mut keyframe_recorder = KeyframeRecorder::new(fps, seed, domain);
463 let mut ctx = RenderCtx {
464 format,
465 output,
466 svg: &mut svg_renderer,
467 keyframes: &mut keyframe_recorder,
468 };
469
470 println!("Rendering {total_frames} frames...");
471
472 let result = match domain {
473 "orbit" => render_orbit(fps, total_frames, &mut ctx),
474 "bouncing_balls" => render_bouncing_balls(fps, seed, total_frames, &mut ctx),
475 _ => {
476 eprintln!("Error: unsupported domain '{domain}'. Supported: orbit, bouncing_balls");
477 return ExitCode::from(1);
478 }
479 };
480
481 if let Err(e) = result {
482 eprintln!("{e}");
483 return ExitCode::from(1);
484 }
485
486 if *format == RenderFormat::SvgKeyframes {
487 let json = ctx.keyframes.to_json();
488 let path = output.join("keyframes.json");
489 if let Err(e) = std::fs::write(&path, &json) {
490 eprintln!("Error writing keyframes: {e}");
491 return ExitCode::from(1);
492 }
493 println!("\r Frames: {total_frames}");
494 println!(" Elements: {}", ctx.keyframes.element_count());
495 println!(" Template: {}", output.join("scene.svg").display());
496 println!(" Keyframes: {}", path.display());
497 } else {
498 println!("\r Frames: {total_frames}");
499 println!(" Output: {}/frame_NNNN.svg", output.display());
500 }
501
502 println!("\n✓ Render complete");
503 ExitCode::SUCCESS
504}
505
506fn render_orbit(fps: u32, total_frames: usize, ctx: &mut RenderCtx<'_>) -> Result<(), String> {
508 use crate::orbit::physics::YoshidaIntegrator;
509 use crate::orbit::render::{render_state, OrbitTrail, RenderConfig};
510 use crate::orbit::scenarios::KeplerConfig;
511 use crate::orbit::units::OrbitTime;
512
513 let config = KeplerConfig::default();
514 let mut state = config.build(1e6);
515 let integrator = YoshidaIntegrator::new();
516 let dt_seconds = 3600.0;
517 let sim_dt_per_frame = 86400.0 / f64::from(fps);
518 let steps_per_frame = (sim_dt_per_frame / dt_seconds).max(1.0) as usize;
519 let render_config = RenderConfig::default();
520 let mut trails = vec![OrbitTrail::new(0), OrbitTrail::new(500)];
521
522 for frame in 0..total_frames {
523 for _ in 0..steps_per_frame {
524 let dt = OrbitTime::from_seconds(dt_seconds);
525 integrator
526 .step(&mut state, dt)
527 .map_err(|_| format!("Physics error at frame {frame}"))?;
528 for (i, body) in state.bodies.iter().enumerate() {
529 if i < trails.len() {
530 let (x, y, _) = body.position.as_meters();
531 trails[i].push(x, y);
532 }
533 }
534 }
535 let commands = render_state(&state, &render_config, &trails, None, None);
536 write_frame(frame, &commands, ctx)?;
537 if frame % 60 == 0 {
538 print!("\r Frame {frame}/{total_frames}");
539 }
540 }
541 Ok(())
542}
543
544fn render_bouncing_balls(
546 fps: u32,
547 seed: u64,
548 total_frames: usize,
549 ctx: &mut RenderCtx<'_>,
550) -> Result<(), String> {
551 use crate::scenarios::bouncing_balls::{BouncingBallsConfig, BouncingBallsState};
552
553 let config = BouncingBallsConfig::default();
554 let mut state = BouncingBallsState::new(config, seed);
555 let dt = 1.0 / f64::from(fps);
556
557 for frame in 0..total_frames {
558 state.step(dt);
559 let commands = state.render();
560 write_frame(frame, &commands, ctx)?;
561 if frame % 60 == 0 {
562 print!("\r Frame {frame}/{total_frames}");
563 }
564 }
565 Ok(())
566}