use super::super::{ErrorKind, ValidationContext, schema::ParsedData};
pub(super) fn check_sobol_power_of_2(data: &ParsedData, ctx: &mut ValidationContext) {
use cobre_core::temporal::NoiseMethod;
for stage in &data.stages.stages {
if stage.id < 0 {
continue; }
let bf = stage.scenario_config.branching_factor;
if stage.scenario_config.noise_method == NoiseMethod::QmcSobol && !bf.is_power_of_two() {
let suggestion = if bf > 0 {
let lower = 1usize << (usize::BITS - bf.leading_zeros() - 1);
let upper = lower << 1;
format!("consider {lower} or {upper}")
} else {
"consider a positive power of 2".to_string()
};
ctx.add_warning(
ErrorKind::ModelQuality,
"stages.json",
Some(format!("Stage {}", stage.id)),
format!(
"Stage {}: qmc_sobol with num_openings={bf} which is not a \
power of 2; Sobol sequences have optimal uniformity at powers \
of 2 ({suggestion})",
stage.id,
),
);
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::panic,
clippy::too_many_lines,
clippy::doc_markdown,
clippy::cast_possible_truncation,
clippy::cast_possible_wrap,
clippy::cast_sign_loss
)]
mod tests {
use super::super::test_support::*;
use super::super::validate_semantic_stages_penalties_scenarios;
use cobre_core::temporal::{NoiseMethod, ScenarioSourceConfig};
use crate::validation::{ErrorKind, ValidationContext};
#[test]
fn test_sobol_non_power_of_2_emits_warning() {
let mut stages = make_stages_5b(vec![0]);
stages.stages[0].scenario_config = ScenarioSourceConfig {
branching_factor: 50,
noise_method: NoiseMethod::QmcSobol,
};
let data = make_data_5b(
vec![make_hydro_ordered_penalties(1)],
stages,
vec![make_bus_with_deficit(1, 10.0)],
vec![],
vec![],
None,
);
let mut ctx = ValidationContext::new();
validate_semantic_stages_penalties_scenarios(&data, &mut ctx);
let all_warnings = ctx.warnings();
let quality_warnings: Vec<_> = all_warnings
.iter()
.filter(|w| w.kind == ErrorKind::ModelQuality && w.message.contains("qmc_sobol"))
.collect();
assert_eq!(
quality_warnings.len(),
1,
"expected exactly 1 ModelQuality warning, got: {:?}",
ctx.warnings()
);
let msg = &quality_warnings[0].message;
assert!(
msg.contains("50"),
"warning message should contain the branching factor '50', got: {msg}"
);
assert!(
msg.contains("Stage "),
"warning message should contain 'Stage ', got: {msg}"
);
}
#[test]
fn test_sobol_power_of_2_no_warning() {
let mut stages = make_stages_5b(vec![0]);
stages.stages[0].scenario_config = ScenarioSourceConfig {
branching_factor: 64,
noise_method: NoiseMethod::QmcSobol,
};
let data = make_data_5b(
vec![make_hydro_ordered_penalties(1)],
stages,
vec![make_bus_with_deficit(1, 10.0)],
vec![],
vec![],
None,
);
let mut ctx = ValidationContext::new();
validate_semantic_stages_penalties_scenarios(&data, &mut ctx);
let all_warnings = ctx.warnings();
let quality_warnings: Vec<_> = all_warnings
.iter()
.filter(|w| w.kind == ErrorKind::ModelQuality && w.message.contains("qmc_sobol"))
.collect();
assert!(
quality_warnings.is_empty(),
"branching_factor=64 (power of 2) should produce no ModelQuality warnings, \
got: {quality_warnings:?}"
);
}
#[test]
fn test_saa_non_power_of_2_no_warning() {
let mut stages = make_stages_5b(vec![0]);
stages.stages[0].scenario_config = ScenarioSourceConfig {
branching_factor: 50,
noise_method: NoiseMethod::Saa,
};
let data = make_data_5b(
vec![make_hydro_ordered_penalties(1)],
stages,
vec![make_bus_with_deficit(1, 10.0)],
vec![],
vec![],
None,
);
let mut ctx = ValidationContext::new();
validate_semantic_stages_penalties_scenarios(&data, &mut ctx);
let all_warnings = ctx.warnings();
let quality_warnings: Vec<_> = all_warnings
.iter()
.filter(|w| w.kind == ErrorKind::ModelQuality && w.message.contains("qmc_sobol"))
.collect();
assert!(
quality_warnings.is_empty(),
"SAA with non-power-of-2 branching factor should produce no ModelQuality warnings, \
got: {quality_warnings:?}"
);
}
#[test]
fn test_sobol_mixed_stages_only_warns_non_power() {
let mut stages = make_stages_5b(vec![0, 1]);
stages.stages[0].scenario_config = ScenarioSourceConfig {
branching_factor: 100,
noise_method: NoiseMethod::QmcSobol,
};
stages.stages[1].scenario_config = ScenarioSourceConfig {
branching_factor: 128,
noise_method: NoiseMethod::QmcSobol,
};
let data = make_data_5b(
vec![make_hydro_ordered_penalties(1)],
stages,
vec![make_bus_with_deficit(1, 10.0)],
vec![],
vec![],
None,
);
let mut ctx = ValidationContext::new();
validate_semantic_stages_penalties_scenarios(&data, &mut ctx);
let all_warnings = ctx.warnings();
let quality_warnings: Vec<_> = all_warnings
.iter()
.filter(|w| w.kind == ErrorKind::ModelQuality && w.message.contains("qmc_sobol"))
.collect();
assert_eq!(
quality_warnings.len(),
1,
"expected exactly 1 ModelQuality warning (for stage 0 only), got: {:?}",
ctx.warnings()
);
let msg = &quality_warnings[0].message;
assert!(
msg.contains("Stage 0"),
"warning should be for stage 0, got: {msg}"
);
assert!(
msg.contains("100"),
"warning should mention branching_factor 100, got: {msg}"
);
}
}