use super::{DistributionType, HyperparameterSpace, ParameterRange};
use scirs2_core::numeric::Float;
use scirs2_core::random::Random;
pub type StrategyRng = Random<scirs2_core::random::rngs::StdRng>;
pub type SuggestedParameters<T> = (
std::collections::HashMap<String, T>,
std::collections::HashMap<String, String>,
);
pub fn sorted_parameter_names<T: Float>(space: &HyperparameterSpace<T>) -> Vec<String> {
let mut names: Vec<String> = space.parameter_ranges().keys().cloned().collect();
names.sort();
names
}
pub fn sorted_categorical_names<T: Float>(space: &HyperparameterSpace<T>) -> Vec<String> {
let mut names: Vec<String> = space.categorical_options().keys().cloned().collect();
names.sort();
names
}
pub fn bounds<T: Float>(range: &ParameterRange<T>) -> (f64, f64) {
let a = range.min_value.to_f64().unwrap_or(0.0);
let b = range.max_value.to_f64().unwrap_or(0.0);
if a <= b {
(a, b)
} else {
(b, a)
}
}
pub fn is_log_axis<T: Float>(range: &ParameterRange<T>) -> bool {
let (lower, _) = bounds(range);
range.log_scale && lower > 0.0
}
pub fn normalize<T: Float>(range: &ParameterRange<T>, value: f64) -> f64 {
let (lower, upper) = bounds(range);
if is_log_axis(range) {
let log_lower = lower.ln();
let log_upper = upper.ln();
if log_upper <= log_lower {
return 0.0;
}
((value.max(f64::MIN_POSITIVE).ln() - log_lower) / (log_upper - log_lower)).clamp(0.0, 1.0)
} else {
if upper <= lower {
return 0.0;
}
((value - lower) / (upper - lower)).clamp(0.0, 1.0)
}
}
pub fn denormalize<T: Float>(range: &ParameterRange<T>, unit: f64) -> f64 {
let (lower, upper) = bounds(range);
let unit = unit.clamp(0.0, 1.0);
if unit <= 0.0 {
return lower;
}
if unit >= 1.0 {
return upper;
}
if is_log_axis(range) {
let log_lower = lower.ln();
let log_upper = upper.ln();
(log_lower + unit * (log_upper - log_lower)).exp()
} else {
lower + unit * (upper - lower)
}
}
pub fn snap_to_discrete<T: Float>(range: &ParameterRange<T>, value: f64) -> f64 {
let Some(values) = range.discrete_values.as_ref() else {
return value;
};
let mut best = value;
let mut best_distance = f64::INFINITY;
for candidate in values {
let candidate = candidate.to_f64().unwrap_or(0.0);
let distance = (candidate - value).abs();
if distance < best_distance {
best_distance = distance;
best = candidate;
}
}
best
}
pub fn sample_parameter<T: Float>(range: &ParameterRange<T>, rng: &mut StrategyRng) -> f64 {
if let Some(values) = range.discrete_values.as_ref() {
if !values.is_empty() {
let idx = rng.gen_range(0..values.len());
return values[idx].to_f64().unwrap_or(0.0);
}
}
let unit = match range.distribution {
DistributionType::Uniform => rng.gen_range(0.0..1.0),
DistributionType::Normal => {
let mut unit = 0.5 + standard_normal(rng) * 0.25;
let mut attempts = 0;
while !(0.0..=1.0).contains(&unit) && attempts < 16 {
unit = 0.5 + standard_normal(rng) * 0.25;
attempts += 1;
}
unit.clamp(0.0, 1.0)
}
DistributionType::LogNormal => {
let raw = standard_normal(rng).exp();
(raw / (raw + 1.0)).clamp(0.0, 1.0)
}
DistributionType::Beta => {
let mut draws = [
rng.gen_range(0.0..1.0),
rng.gen_range(0.0..1.0),
rng.gen_range(0.0..1.0),
];
draws.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
draws[1]
}
DistributionType::Exponential => {
let u = rng.gen_range(0.0..1.0_f64).max(f64::MIN_POSITIVE);
let raw = -u.ln();
(raw / (raw + 1.0)).clamp(0.0, 1.0)
}
};
denormalize(range, unit)
}
pub fn standard_normal(rng: &mut StrategyRng) -> f64 {
let u1 = rng.gen_range(0.0..1.0_f64).max(f64::MIN_POSITIVE);
let u2 = rng.gen_range(0.0..1.0_f64);
(-2.0 * u1.ln()).sqrt() * (std::f64::consts::TAU * u2).cos()
}
#[cfg(test)]
mod tests {
use super::*;
fn linear_range() -> ParameterRange<f64> {
ParameterRange {
name: "x".to_string(),
min_value: 2.0,
max_value: 6.0,
distribution: DistributionType::Uniform,
log_scale: false,
discrete_values: None,
}
}
fn log_range() -> ParameterRange<f64> {
ParameterRange {
name: "lr".to_string(),
min_value: 1e-4,
max_value: 1e-1,
distribution: DistributionType::Uniform,
log_scale: true,
discrete_values: None,
}
}
#[test]
fn normalization_round_trips_on_both_axes() {
for range in [linear_range(), log_range()] {
for unit in [0.0, 0.1, 0.5, 0.9, 1.0] {
let raw = denormalize(&range, unit);
let back = normalize(&range, raw);
assert!(
(back - unit).abs() < 1e-9,
"unit {unit} round-tripped to {back}"
);
}
}
}
#[test]
fn log_axis_is_ignored_for_non_positive_bounds() {
let range = ParameterRange {
name: "x".to_string(),
min_value: -1.0,
max_value: 1.0,
distribution: DistributionType::Uniform,
log_scale: true,
discrete_values: None,
};
assert!(!is_log_axis(&range));
let value = denormalize(&range, 0.5);
assert!(value.is_finite(), "got {value}");
assert!((value - 0.0).abs() < 1e-12);
}
#[test]
fn every_distribution_stays_in_range_and_is_not_uniform_by_default() {
let mut rng = Random::seed(1234);
for distribution in [
DistributionType::Uniform,
DistributionType::Normal,
DistributionType::LogNormal,
DistributionType::Beta,
DistributionType::Exponential,
] {
let range = ParameterRange {
name: "x".to_string(),
min_value: 0.0,
max_value: 1.0,
distribution,
log_scale: false,
discrete_values: None,
};
let samples: Vec<f64> = (0..2000)
.map(|_| sample_parameter(&range, &mut rng))
.collect();
for value in &samples {
assert!(
(0.0..=1.0).contains(value),
"{distribution:?} produced {value}"
);
}
let mean = samples.iter().sum::<f64>() / samples.len() as f64;
match distribution {
DistributionType::Exponential => {
assert!(mean < 0.45, "Exponential mean {mean} looks uniform")
}
DistributionType::Uniform => {
assert!((mean - 0.5).abs() < 0.05, "uniform mean {mean}")
}
_ => {}
}
}
}
#[test]
fn discrete_ranges_only_ever_emit_declared_values() {
let mut rng = Random::seed(9);
let range = ParameterRange {
name: "batch".to_string(),
min_value: 8.0,
max_value: 128.0,
distribution: DistributionType::Uniform,
log_scale: false,
discrete_values: Some(vec![8.0, 16.0, 32.0, 64.0, 128.0]),
};
for _ in 0..200 {
let value = sample_parameter(&range, &mut rng);
assert!(
[8.0, 16.0, 32.0, 64.0, 128.0].contains(&value),
"got {value}"
);
}
assert_eq!(snap_to_discrete(&range, 30.0), 32.0);
assert_eq!(snap_to_discrete(&range, 1000.0), 128.0);
}
#[test]
fn ordering_helpers_are_deterministic() {
let mut space: HyperparameterSpace<f64> = HyperparameterSpace::new();
for name in ["zeta", "alpha", "mu"] {
space.add_parameter(
name.to_string(),
ParameterRange {
name: name.to_string(),
min_value: 0.0,
max_value: 1.0,
distribution: DistributionType::Uniform,
log_scale: false,
discrete_values: None,
},
);
}
space.add_categorical_parameter("opt".to_string(), vec!["adam".to_string()]);
space.add_categorical_parameter("act".to_string(), vec!["relu".to_string()]);
assert_eq!(sorted_parameter_names(&space), vec!["alpha", "mu", "zeta"]);
assert_eq!(sorted_categorical_names(&space), vec!["act", "opt"]);
}
}