1#[derive(Debug, Clone)]
3pub enum ScheduleKind {
4 Geometric { rate: f64 },
5 Linear { steps: usize },
6 ReciprocalIter,
7}
8
9impl ScheduleKind {
10 #[must_use]
18 pub fn geometric_rate_from_steps(tau_start: f64, tau_min: f64, steps: usize) -> f64 {
19 let steps = steps.max(1);
20 (tau_min / tau_start).powf(1.0 / steps as f64)
21 }
22}
23
24#[derive(Debug, Clone)]
32pub struct GumbelTemperatureSchedule {
33 pub tau_start: f64,
34 pub tau_min: f64,
35 pub decay: ScheduleKind,
36 pub iter_count: usize,
37}
38
39impl GumbelTemperatureSchedule {
40 #[must_use = "build error must be handled"]
41 pub fn new(tau_start: f64, tau_min: f64, decay: ScheduleKind) -> Result<Self, String> {
42 let sched = Self {
43 tau_start,
44 tau_min,
45 decay,
46 iter_count: 0,
47 };
48 sched.validate()?;
49 Ok(sched)
50 }
51
52 pub fn validate(&self) -> Result<(), String> {
53 if !(self.tau_start.is_finite() && self.tau_start > 0.0) {
54 return Err(format!(
55 "GumbelTemperatureSchedule: tau_start must be finite and positive; got {}",
56 self.tau_start
57 ));
58 }
59 if !(self.tau_min.is_finite() && self.tau_min > 0.0) {
60 return Err(format!(
61 "GumbelTemperatureSchedule: tau_min must be finite and positive; got {}",
62 self.tau_min
63 ));
64 }
65 if self.tau_min > self.tau_start {
66 return Err(format!(
67 "GumbelTemperatureSchedule: tau_min ({}) cannot exceed tau_start ({})",
68 self.tau_min, self.tau_start
69 ));
70 }
71 match self.decay {
72 ScheduleKind::Geometric { rate } => {
73 if !(rate.is_finite() && rate > 0.0 && rate < 1.0) {
74 return Err(format!(
75 "GumbelTemperatureSchedule::Geometric: rate must be in (0, 1); got {rate}"
76 ));
77 }
78 }
79 ScheduleKind::Linear { steps } => {
80 if steps == 0 {
81 return Err("GumbelTemperatureSchedule::Linear: steps must be positive".into());
82 }
83 }
84 ScheduleKind::ReciprocalIter => {}
85 }
86 Ok(())
87 }
88
89 pub fn current_tau(&self, iter: usize) -> f64 {
90 let raw = match self.decay {
91 ScheduleKind::Geometric { rate } => self.tau_start * rate.powf(iter as f64),
92 ScheduleKind::Linear { steps } => {
93 if iter >= steps {
94 self.tau_min
95 } else {
96 let frac = iter as f64 / steps as f64;
97 self.tau_start + frac * (self.tau_min - self.tau_start)
98 }
99 }
100 ScheduleKind::ReciprocalIter => self.tau_start / (1.0 + iter as f64),
101 };
102 raw.max(self.tau_min)
103 }
104
105 pub fn step(&mut self) -> f64 {
106 let tau = self.current_tau(self.iter_count);
107 self.iter_count += 1;
108 tau
109 }
110}
111
112#[derive(Debug, Clone, PartialEq)]
113pub enum SearchStrategy {
114 Fixed,
115 ExponentialSweep { values: Vec<f64> },
116}
117
118impl SearchStrategy {
119 #[must_use]
120 pub fn is_fixed(&self) -> bool {
121 matches!(self, Self::Fixed)
122 }
123
124 #[must_use]
125 pub fn sweep_values(&self) -> Option<&[f64]> {
126 match self {
127 Self::Fixed => None,
128 Self::ExponentialSweep { values } => Some(values),
129 }
130 }
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136
137 fn geometric(rate: f64) -> GumbelTemperatureSchedule {
138 GumbelTemperatureSchedule::new(1.0, 0.01, ScheduleKind::Geometric { rate }).unwrap()
139 }
140
141 #[test]
144 fn new_ok_for_valid_geometric() {
145 assert!(
146 GumbelTemperatureSchedule::new(1.0, 0.1, ScheduleKind::Geometric { rate: 0.9 }).is_ok()
147 );
148 }
149
150 #[test]
151 fn new_err_for_non_positive_tau_start() {
152 assert!(GumbelTemperatureSchedule::new(0.0, 0.1, ScheduleKind::ReciprocalIter).is_err());
153 assert!(
154 GumbelTemperatureSchedule::new(f64::NAN, 0.1, ScheduleKind::ReciprocalIter).is_err()
155 );
156 }
157
158 #[test]
159 fn new_err_for_tau_min_exceeds_tau_start() {
160 assert!(
161 GumbelTemperatureSchedule::new(0.5, 1.0, ScheduleKind::Geometric { rate: 0.9 })
162 .is_err()
163 );
164 }
165
166 #[test]
167 fn new_err_for_geometric_rate_out_of_range() {
168 assert!(
169 GumbelTemperatureSchedule::new(1.0, 0.1, ScheduleKind::Geometric { rate: 1.0 })
170 .is_err()
171 );
172 assert!(
173 GumbelTemperatureSchedule::new(1.0, 0.1, ScheduleKind::Geometric { rate: 0.0 })
174 .is_err()
175 );
176 }
177
178 #[test]
179 fn new_err_for_linear_zero_steps() {
180 assert!(
181 GumbelTemperatureSchedule::new(1.0, 0.1, ScheduleKind::Linear { steps: 0 }).is_err()
182 );
183 }
184
185 #[test]
188 fn geometric_iter_zero_returns_tau_start() {
189 let s = geometric(0.5);
190 assert!((s.current_tau(0) - 1.0).abs() < 1e-14);
191 }
192
193 #[test]
194 fn geometric_decays_by_rate_each_step() {
195 let s = geometric(0.5);
196 assert!((s.current_tau(2) - 0.25).abs() < 1e-12);
198 }
199
200 #[test]
201 fn geometric_clamps_at_tau_min() {
202 let s = GumbelTemperatureSchedule::new(1.0, 0.5, ScheduleKind::Geometric { rate: 0.1 })
203 .unwrap();
204 assert!((s.current_tau(5) - 0.5).abs() < 1e-14);
206 }
207
208 #[test]
211 fn linear_iter_zero_returns_tau_start() {
212 let s =
213 GumbelTemperatureSchedule::new(2.0, 0.5, ScheduleKind::Linear { steps: 10 }).unwrap();
214 assert!((s.current_tau(0) - 2.0).abs() < 1e-14);
215 }
216
217 #[test]
218 fn linear_at_steps_returns_tau_min() {
219 let s =
220 GumbelTemperatureSchedule::new(2.0, 0.5, ScheduleKind::Linear { steps: 10 }).unwrap();
221 assert!((s.current_tau(10) - 0.5).abs() < 1e-14);
222 }
223
224 #[test]
227 fn reciprocal_iter_zero_returns_tau_start() {
228 let s = GumbelTemperatureSchedule::new(4.0, 0.1, ScheduleKind::ReciprocalIter).unwrap();
229 assert!((s.current_tau(0) - 4.0).abs() < 1e-14);
230 }
231
232 #[test]
233 fn reciprocal_iter_one_halves_tau_start() {
234 let s = GumbelTemperatureSchedule::new(4.0, 0.1, ScheduleKind::ReciprocalIter).unwrap();
235 assert!((s.current_tau(1) - 2.0).abs() < 1e-14);
236 }
237
238 #[test]
241 fn step_increments_iter_count() {
242 let mut s = geometric(0.5);
243 assert_eq!(s.iter_count, 0);
244 s.step();
245 assert_eq!(s.iter_count, 1);
246 s.step();
247 assert_eq!(s.iter_count, 2);
248 }
249
250 #[test]
253 fn fixed_is_fixed_and_has_no_sweep_values() {
254 let s = SearchStrategy::Fixed;
255 assert!(s.is_fixed());
256 assert!(s.sweep_values().is_none());
257 }
258
259 #[test]
260 fn exponential_sweep_is_not_fixed_and_returns_values() {
261 let s = SearchStrategy::ExponentialSweep {
262 values: vec![1.0, 2.0, 3.0],
263 };
264 assert!(!s.is_fixed());
265 assert_eq!(s.sweep_values().unwrap(), &[1.0, 2.0, 3.0]);
266 }
267}