1use std::fs;
2use std::path::{Path, PathBuf};
3
4use anyhow::Result;
5use serde::{Deserialize, Serialize};
6
7use crate::config::{SweepCase, SweepPreset};
8use crate::integrator::simulate;
9use crate::outputs::write_run_outputs;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SweepCaseSummary {
13 pub case_id: String,
14 pub group: String,
15 pub note: String,
16 pub scenario: String,
17 pub output_dir: String,
18 pub success: bool,
19 pub continuous_power_mw: f64,
20 pub burst_power_mw: f64,
21 pub burst_duration_s: f64,
22 pub pulse_energy_gj: f64,
23 pub initial_ep_gj: f64,
24 pub thermal_rejection_mw_per_k: f64,
25 pub actuator_demand_scale: f64,
26 pub damping_scale: f64,
27 pub stiffness_scale: f64,
28 pub burst_cadence_s: Option<f64>,
29 pub allocation_strategy: Option<String>,
30 pub min_ep_gj: f64,
31 pub energy_depleted_gj: f64,
32 pub peak_temperature_k: f64,
33 pub peak_temperature_c: f64,
34 pub time_above_thermal_threshold_s: f64,
35 pub recharge_time_s: Option<f64>,
36 pub time_to_any_threshold_s: Option<f64>,
37 pub first_local_buffer_breach_s: Option<f64>,
38 pub first_admissible_breach_s: Option<f64>,
39 pub effective_duty_cycle: f64,
40 pub recharge_readiness_fraction: f64,
41 pub successful_burst_fraction: f64,
42 pub mean_authority_utilization: f64,
43 pub mean_delivered_ratio: f64,
44 pub degraded_state_fraction: f64,
45 pub min_local_buffer_mj: f64,
46 pub local_imbalance_max_mj: f64,
47 pub saturation_count: usize,
48 pub delivered_mechanical_work_j: f64,
49 pub energy_breach: bool,
50 pub thermal_breach: bool,
51 pub local_buffer_breach: bool,
52 pub saturation_breach: bool,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SweepAggregate {
57 pub preset: String,
58 pub root_dir: PathBuf,
59 pub cases_dir: PathBuf,
60 pub case_summaries: Vec<SweepCaseSummary>,
61}
62
63pub fn run_sweep(
64 preset: SweepPreset,
65 cases: Vec<SweepCase>,
66 run_root: &Path,
67) -> Result<SweepAggregate> {
68 let cases_dir = run_root.join("sweeps").join(preset.as_str());
69 fs::create_dir_all(&cases_dir)?;
70
71 let mut case_summaries = Vec::new();
72 for case in cases {
73 let case_dir = cases_dir.join(&case.metadata.case_id);
74 fs::create_dir_all(&case_dir)?;
75 let result = simulate(case.config.clone())?;
76 write_run_outputs(&case_dir, &result)?;
77
78 case_summaries.push(SweepCaseSummary {
79 case_id: case.metadata.case_id.clone(),
80 group: case.metadata.group.clone(),
81 note: case.metadata.note.clone(),
82 scenario: result.config.scenario.name.clone(),
83 output_dir: case_dir.to_string_lossy().to_string(),
84 success: result.summary.success,
85 continuous_power_mw: case.metadata.continuous_power_mw,
86 burst_power_mw: case.metadata.burst_power_mw,
87 burst_duration_s: case.metadata.burst_duration_s,
88 pulse_energy_gj: case.metadata.pulse_energy_gj,
89 initial_ep_gj: case.metadata.initial_ep_gj,
90 thermal_rejection_mw_per_k: case.metadata.thermal_rejection_mw_per_k,
91 actuator_demand_scale: case.metadata.actuator_demand_scale,
92 damping_scale: case.metadata.damping_scale,
93 stiffness_scale: case.metadata.stiffness_scale,
94 burst_cadence_s: case.metadata.burst_cadence_s,
95 allocation_strategy: case
96 .metadata
97 .allocation_strategy
98 .map(|strategy| format!("{strategy:?}")),
99 min_ep_gj: result.summary.min_ep_j / 1.0e9,
100 energy_depleted_gj: result.summary.energy_depleted_j / 1.0e9,
101 peak_temperature_k: result.summary.peak_temperature_k,
102 peak_temperature_c: result.summary.peak_temperature_k - 273.15,
103 time_above_thermal_threshold_s: result.summary.time_above_thermal_threshold_s,
104 recharge_time_s: result.summary.recharge_time_s,
105 time_to_any_threshold_s: result.summary.time_to_any_threshold_s,
106 first_local_buffer_breach_s: result.summary.first_local_buffer_breach_s,
107 first_admissible_breach_s: result.summary.first_admissible_breach_s,
108 effective_duty_cycle: result.summary.effective_duty_cycle,
109 recharge_readiness_fraction: result.summary.recharge_readiness_fraction,
110 successful_burst_fraction: result.summary.successful_burst_fraction,
111 mean_authority_utilization: result.summary.mean_authority_utilization,
112 mean_delivered_ratio: result.summary.mean_delivered_ratio,
113 degraded_state_fraction: result.summary.degraded_state_fraction,
114 min_local_buffer_mj: result.summary.min_local_buffer_j / 1.0e6,
115 local_imbalance_max_mj: result.summary.local_imbalance_max_j / 1.0e6,
116 saturation_count: result.summary.saturation_count,
117 delivered_mechanical_work_j: result.summary.delivered_mechanical_work_j,
118 energy_breach: result.summary.energy_breach,
119 thermal_breach: result.summary.thermal_breach,
120 local_buffer_breach: result.summary.local_buffer_breach,
121 saturation_breach: result.summary.saturation_breach,
122 });
123 }
124
125 Ok(SweepAggregate {
126 preset: preset.as_str().to_string(),
127 root_dir: run_root.to_path_buf(),
128 cases_dir,
129 case_summaries,
130 })
131}