use serde::{Deserialize, Serialize};
use super::types::ParameterKind;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParameterRange {
pub kind: ParameterKind,
pub min: f64,
pub max: f64,
pub step: Option<f64>,
pub default: f64,
}
impl ParameterRange {
#[must_use]
pub fn step_count(&self) -> Option<usize> {
let step = self.step?;
if step <= 0.0 {
return None;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
Some(((self.max - self.min) / step).floor() as usize + 1)
}
#[must_use]
pub fn clamp(&self, value: f64) -> f64 {
value.clamp(self.min, self.max)
}
#[must_use]
pub fn contains(&self, value: f64) -> bool {
(self.min..=self.max).contains(&value)
}
#[must_use]
pub fn quantize(&self, value: f64) -> f64 {
if let Some(step) = self.step
&& step > 0.0
{
let quantized = self.min + ((value - self.min) / step).round() * step;
return self.clamp((quantized * 100.0).round() / 100.0);
}
value
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.min.is_finite()
&& self.max.is_finite()
&& self.default.is_finite()
&& self.min <= self.max
&& self.step.is_none_or(|s| s.is_finite() && s > 0.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct SearchSpace {
pub parameters: Vec<ParameterRange>,
}
impl Default for SearchSpace {
fn default() -> Self {
Self {
parameters: vec![
ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 1.0,
step: Some(0.1),
default: 0.7,
},
ParameterRange {
kind: ParameterKind::TopP,
min: 0.1,
max: 1.0,
step: Some(0.05),
default: 0.9,
},
ParameterRange {
kind: ParameterKind::TopK,
min: 1.0,
max: 100.0,
step: Some(5.0),
default: 40.0,
},
ParameterRange {
kind: ParameterKind::FrequencyPenalty,
min: -2.0,
max: 2.0,
step: Some(0.2),
default: 0.0,
},
ParameterRange {
kind: ParameterKind::PresencePenalty,
min: -2.0,
max: 2.0,
step: Some(0.2),
default: 0.0,
},
],
}
}
}
impl SearchSpace {
#[must_use]
pub fn range_for(&self, kind: ParameterKind) -> Option<&ParameterRange> {
self.parameters.iter().find(|r| r.kind == kind)
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.parameters.iter().all(ParameterRange::is_valid)
}
#[must_use]
pub fn grid_size(&self) -> usize {
self.parameters
.iter()
.filter_map(ParameterRange::step_count)
.sum()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn step_count_with_step() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 1.0,
step: Some(0.5),
default: 0.5,
};
assert_eq!(r.step_count(), Some(3)); }
#[test]
fn step_count_no_step() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 1.0,
step: None,
default: 0.5,
};
assert_eq!(r.step_count(), None);
}
#[test]
fn step_count_zero_step() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 1.0,
step: Some(0.0),
default: 0.5,
};
assert_eq!(r.step_count(), None);
}
#[test]
fn clamp_below_min() {
let r = ParameterRange {
kind: ParameterKind::TopP,
min: 0.1,
max: 1.0,
step: Some(0.1),
default: 0.9,
};
assert!((r.clamp(-1.0) - 0.1).abs() < f64::EPSILON);
}
#[test]
fn clamp_above_max() {
let r = ParameterRange {
kind: ParameterKind::TopP,
min: 0.1,
max: 1.0,
step: Some(0.1),
default: 0.9,
};
assert!((r.clamp(2.0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn clamp_within_range() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 2.0,
step: Some(0.1),
default: 0.7,
};
assert!((r.clamp(1.0) - 1.0).abs() < f64::EPSILON);
}
#[test]
fn contains_within_range() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 2.0,
step: Some(0.1),
default: 0.7,
};
assert!(r.contains(1.0));
assert!(r.contains(0.0));
assert!(r.contains(2.0));
assert!(!r.contains(-0.1));
assert!(!r.contains(2.1));
}
#[test]
fn quantize_snaps_to_nearest_step() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 2.0,
step: Some(0.1),
default: 0.7,
};
let q = r.quantize(0.73);
assert!((q - 0.7).abs() < 1e-10, "expected 0.7, got {q}");
}
#[test]
fn quantize_no_step_returns_value_unchanged() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 2.0,
step: None,
default: 0.7,
};
assert!((r.quantize(1.234) - 1.234).abs() < f64::EPSILON);
}
#[test]
fn quantize_clamps_result() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 1.0,
step: Some(0.1),
default: 0.5,
};
let q = r.quantize(100.0);
assert!(q <= 1.0, "quantize must clamp to max");
}
#[test]
fn quantize_avoids_fp_accumulation() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 2.0,
step: Some(0.1),
default: 0.7,
};
let accumulated = 0.1_f64 * 7.0;
let q = r.quantize(accumulated);
assert!(
(q - 0.7).abs() < 1e-10,
"expected 0.7, got {q} (accumulated={accumulated})"
);
}
#[test]
fn default_search_space_has_five_parameters() {
let space = SearchSpace::default();
assert_eq!(space.parameters.len(), 5);
}
#[test]
fn default_grid_size_is_reasonable() {
let space = SearchSpace::default();
let size = space.grid_size();
assert!(size > 0);
assert!(size < 200);
}
#[test]
fn range_for_finds_temperature() {
let space = SearchSpace::default();
let range = space.range_for(ParameterKind::Temperature);
assert!(range.is_some());
assert!((range.unwrap().default - 0.7).abs() < f64::EPSILON);
}
#[test]
fn range_for_missing_returns_none() {
let space = SearchSpace::default();
let range = space.range_for(ParameterKind::RetrievalTopK);
assert!(range.is_none());
}
#[test]
fn grid_size_empty_space_is_zero() {
let space = SearchSpace { parameters: vec![] };
assert_eq!(space.grid_size(), 0);
}
#[test]
fn quantize_with_nonzero_min_anchors_to_min() {
let r = ParameterRange {
kind: ParameterKind::TopK,
min: 1.0,
max: 100.0,
step: Some(5.0),
default: 40.0,
};
let q = r.quantize(6.0);
assert!(
(q - 6.0).abs() < 1e-10,
"expected 6.0 (min-anchored grid), got {q}"
);
let q2 = r.quantize(3.0);
assert!((q2 - 1.0).abs() < 1e-10, "expected 1.0, got {q2}");
}
#[test]
fn quantize_negative_step_returns_unchanged() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 0.0,
max: 2.0,
step: Some(-0.1),
default: 0.7,
};
assert!((r.quantize(0.75) - 0.75).abs() < f64::EPSILON);
}
#[test]
fn parameter_range_is_valid_for_default() {
for r in &SearchSpace::default().parameters {
assert!(r.is_valid(), "default range {:?} is invalid", r.kind);
}
}
#[test]
fn parameter_range_invalid_when_min_gt_max() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: 2.0,
max: 0.0,
step: Some(0.1),
default: 1.0,
};
assert!(!r.is_valid());
}
#[test]
fn parameter_range_invalid_when_nonfinite() {
let r = ParameterRange {
kind: ParameterKind::Temperature,
min: f64::NAN,
max: 2.0,
step: Some(0.1),
default: 0.7,
};
assert!(!r.is_valid());
}
#[test]
fn search_space_is_valid_for_default() {
assert!(SearchSpace::default().is_valid());
}
#[test]
fn search_space_invalid_when_range_inverted() {
let space = SearchSpace {
parameters: vec![ParameterRange {
kind: ParameterKind::Temperature,
min: 2.0,
max: 0.0,
step: Some(0.1),
default: 1.0,
}],
};
assert!(!space.is_valid());
}
}