use serde::{Deserialize, Serialize};
use super::scenario_source::RawScenarioSourceConfig;
use super::training::PhaseSolverProfileConfig;
pub(crate) const DEFAULT_NUM_SCENARIOS: u32 = 2000;
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(default, deny_unknown_fields)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SimulationConfig {
pub enabled: bool,
pub io_channel_capacity: u32,
#[serde(default)]
pub scenario_source: Option<RawScenarioSourceConfig>,
#[serde(default)]
pub solver: Option<PhaseSolverProfileConfig>,
#[serde(default)]
pub selection: Option<SimulationSelection>,
}
impl Default for SimulationConfig {
fn default() -> Self {
Self {
enabled: false,
io_channel_capacity: 64,
scenario_source: None,
solver: None,
selection: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
#[serde(tag = "method", rename_all = "snake_case", deny_unknown_fields)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub enum SimulationSelection {
Sampled {
num_scenarios: u32,
},
Enumerated {},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NumScenariosResolution {
Sampled(u32),
Enumerated,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::{SimulationConfig, SimulationSelection};
use crate::config::training::{PriceStrategy, ScaleStrategy};
#[test]
fn sampled_selection_round_trips() {
let json = r#"{"enabled": true, "selection": {"method": "sampled", "num_scenarios": 500}}"#;
let cfg: SimulationConfig = serde_json::from_str(json).unwrap();
assert_eq!(
cfg.selection,
Some(SimulationSelection::Sampled { num_scenarios: 500 })
);
}
#[test]
fn flat_num_scenarios_alias_is_deserialize_error() {
let alias = r#"{"enabled": true, "num_scenarios": 500}"#;
assert!(
serde_json::from_str::<SimulationConfig>(alias).is_err(),
"flat num_scenarios must be rejected as an unknown field"
);
let arm = r#"{"enabled": true, "selection": {"method": "sampled", "num_scenarios": 500}}"#;
let cfg: SimulationConfig = serde_json::from_str(arm).unwrap();
assert_eq!(
cfg.selection,
Some(SimulationSelection::Sampled { num_scenarios: 500 })
);
}
#[test]
fn enumerated_selection_with_count_is_deserialize_error() {
let json =
r#"{"enabled": true, "selection": {"method": "enumerated", "num_scenarios": 500}}"#;
let result = serde_json::from_str::<SimulationConfig>(json);
assert!(
result.is_err(),
"a count under enumerated must be rejected as unrepresentable"
);
}
#[test]
fn simulation_solver_profile_block_round_trips() {
let json = r#"{
"enabled": true,
"selection": { "method": "sampled", "num_scenarios": 500 },
"solver": {
"scale": "off",
"price": "row"
}
}"#;
let cfg: SimulationConfig = serde_json::from_str(json).unwrap();
let solver = cfg.solver.as_ref().expect("solver present");
assert_eq!(solver.scale, Some(ScaleStrategy::Off));
assert_eq!(solver.price, Some(PriceStrategy::Row));
}
#[test]
fn simulation_solver_profile_absent_is_none() {
assert!(SimulationConfig::default().solver.is_none());
}
#[test]
fn simulation_solver_profile_steepest_edge_fallback_threshold_round_trips() {
let json = r#"{
"enabled": true,
"selection": { "method": "sampled", "num_scenarios": 500 },
"solver": {
"steepest_edge_devex_fallback_threshold": 12.5
}
}"#;
let cfg: SimulationConfig = serde_json::from_str(json).unwrap();
let solver = cfg.solver.as_ref().expect("solver present");
assert_eq!(solver.steepest_edge_devex_fallback_threshold, Some(12.5));
}
}