cobre_io/config/
simulation.rs1use serde::{Deserialize, Serialize};
4
5use super::scenario_source::RawScenarioSourceConfig;
6use super::training::PhaseSolverProfileConfig;
7
8pub(crate) const DEFAULT_NUM_SCENARIOS: u32 = 2000;
11
12#[derive(Debug, Clone, Deserialize, Serialize)]
14#[serde(default, deny_unknown_fields)]
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16pub struct SimulationConfig {
17 pub enabled: bool,
19
20 pub io_channel_capacity: u32,
22
23 #[serde(default)]
26 pub scenario_source: Option<RawScenarioSourceConfig>,
27
28 #[serde(default)]
31 pub solver: Option<PhaseSolverProfileConfig>,
32
33 #[serde(default)]
36 pub selection: Option<SimulationSelection>,
37}
38
39impl Default for SimulationConfig {
40 fn default() -> Self {
41 Self {
42 enabled: false,
43 io_channel_capacity: 64,
44 scenario_source: None,
45 solver: None,
46 selection: None,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
60#[serde(tag = "method", rename_all = "snake_case", deny_unknown_fields)]
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
62pub enum SimulationSelection {
63 Sampled {
65 num_scenarios: u32,
67 },
68 Enumerated {},
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum NumScenariosResolution {
81 Sampled(u32),
83 Enumerated,
85}
86
87#[cfg(test)]
88#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
89mod tests {
90 use super::{SimulationConfig, SimulationSelection};
91 use crate::config::training::{PriceStrategy, ScaleStrategy};
92
93 #[test]
95 fn sampled_selection_round_trips() {
96 let json = r#"{"enabled": true, "selection": {"method": "sampled", "num_scenarios": 500}}"#;
97 let cfg: SimulationConfig = serde_json::from_str(json).unwrap();
98 assert_eq!(
99 cfg.selection,
100 Some(SimulationSelection::Sampled { num_scenarios: 500 })
101 );
102 }
103
104 #[test]
108 fn flat_num_scenarios_alias_is_deserialize_error() {
109 let alias = r#"{"enabled": true, "num_scenarios": 500}"#;
110 assert!(
111 serde_json::from_str::<SimulationConfig>(alias).is_err(),
112 "flat num_scenarios must be rejected as an unknown field"
113 );
114
115 let arm = r#"{"enabled": true, "selection": {"method": "sampled", "num_scenarios": 500}}"#;
116 let cfg: SimulationConfig = serde_json::from_str(arm).unwrap();
117 assert_eq!(
118 cfg.selection,
119 Some(SimulationSelection::Sampled { num_scenarios: 500 })
120 );
121 }
122
123 #[test]
126 fn enumerated_selection_with_count_is_deserialize_error() {
127 let json =
128 r#"{"enabled": true, "selection": {"method": "enumerated", "num_scenarios": 500}}"#;
129 let result = serde_json::from_str::<SimulationConfig>(json);
130 assert!(
131 result.is_err(),
132 "a count under enumerated must be rejected as unrepresentable"
133 );
134 }
135
136 #[test]
137 fn simulation_solver_profile_block_round_trips() {
138 let json = r#"{
139 "enabled": true,
140 "selection": { "method": "sampled", "num_scenarios": 500 },
141 "solver": {
142 "scale": "off",
143 "price": "row"
144 }
145 }"#;
146 let cfg: SimulationConfig = serde_json::from_str(json).unwrap();
147 let solver = cfg.solver.as_ref().expect("solver present");
148 assert_eq!(solver.scale, Some(ScaleStrategy::Off));
149 assert_eq!(solver.price, Some(PriceStrategy::Row));
150 }
151
152 #[test]
153 fn simulation_solver_profile_absent_is_none() {
154 assert!(SimulationConfig::default().solver.is_none());
155 }
156
157 #[test]
158 fn simulation_solver_profile_steepest_edge_fallback_threshold_round_trips() {
159 let json = r#"{
160 "enabled": true,
161 "selection": { "method": "sampled", "num_scenarios": 500 },
162 "solver": {
163 "steepest_edge_devex_fallback_threshold": 12.5
164 }
165 }"#;
166 let cfg: SimulationConfig = serde_json::from_str(json).unwrap();
167 let solver = cfg.solver.as_ref().expect("solver present");
168 assert_eq!(solver.steepest_edge_devex_fallback_threshold, Some(12.5));
169 }
170}