optirs_learned/
domain_objectives.rs1use scirs2_core::ndarray::Array1;
16use scirs2_core::numeric::Float;
17use std::fmt::Debug;
18
19use crate::error::{OptimError, Result};
20
21pub trait MetaObjective<T: Float + Debug + Send + Sync + 'static> {
27 fn dimension(&self) -> usize;
29
30 fn initial_parameters(&self) -> Array1<T>;
32
33 fn loss_and_gradient(&self, params: &Array1<T>) -> Result<(T, Array1<T>)>;
38
39 fn name(&self) -> &str {
41 "meta-objective"
42 }
43}
44
45#[derive(Debug, Clone)]
52pub struct QuadraticObjective<T: Float + Debug + Send + Sync + 'static> {
53 curvature: Array1<T>,
54 optimum: Array1<T>,
55 start: Array1<T>,
56 label: String,
57}
58
59impl<T: Float + Debug + Send + Sync + 'static> QuadraticObjective<T> {
60 pub fn from_curvature(curvature: Vec<f64>, start_value: f64) -> Result<Self> {
67 if curvature.is_empty() {
68 return Err(OptimError::InvalidConfig(
69 "a quadratic objective needs at least one coordinate".to_string(),
70 ));
71 }
72 if !start_value.is_finite() {
73 return Err(OptimError::InvalidConfig(
74 "start_value must be finite".to_string(),
75 ));
76 }
77 for value in &curvature {
78 if !(value.is_finite() && *value > 0.0) {
79 return Err(OptimError::InvalidConfig(format!(
80 "curvature entries must be finite and positive, got {value}"
81 )));
82 }
83 }
84 let dimension = curvature.len();
85 let cast =
86 |v: f64| -> T { scirs2_core::numeric::NumCast::from(v).unwrap_or_else(|| T::zero()) };
87 Ok(Self {
88 curvature: Array1::from_iter(curvature.into_iter().map(cast)),
89 optimum: Array1::zeros(dimension),
90 start: Array1::from_elem(dimension, cast(start_value)),
91 label: "quadratic".to_string(),
92 })
93 }
94
95 pub fn isotropic(dimension: usize, curvature: f64, start_value: f64) -> Result<Self> {
100 Self::from_curvature(vec![curvature; dimension], start_value)
101 }
102
103 pub fn with_optimum(mut self, optimum: Vec<f64>) -> Result<Self> {
109 if optimum.len() != self.curvature.len() {
110 return Err(OptimError::InvalidConfig(format!(
111 "optimum has {} coordinates but the objective has {}",
112 optimum.len(),
113 self.curvature.len()
114 )));
115 }
116 if optimum.iter().any(|v| !v.is_finite()) {
117 return Err(OptimError::InvalidConfig(
118 "optimum entries must be finite".to_string(),
119 ));
120 }
121 self.optimum = Array1::from_iter(
122 optimum
123 .into_iter()
124 .map(|v| scirs2_core::numeric::NumCast::from(v).unwrap_or_else(|| T::zero())),
125 );
126 Ok(self)
127 }
128
129 pub fn labelled(mut self, label: impl Into<String>) -> Self {
131 self.label = label.into();
132 self
133 }
134
135 pub fn condition_number(&self) -> f64 {
137 let mut low = f64::INFINITY;
138 let mut high = 0.0_f64;
139 for value in self.curvature.iter() {
140 let v = value.to_f64().unwrap_or(0.0);
141 low = low.min(v);
142 high = high.max(v);
143 }
144 if low > 0.0 && low.is_finite() {
145 high / low
146 } else {
147 f64::INFINITY
148 }
149 }
150}
151
152impl<T: Float + Debug + Send + Sync + 'static> MetaObjective<T> for QuadraticObjective<T> {
153 fn dimension(&self) -> usize {
154 self.curvature.len()
155 }
156
157 fn initial_parameters(&self) -> Array1<T> {
158 self.start.clone()
159 }
160
161 fn loss_and_gradient(&self, params: &Array1<T>) -> Result<(T, Array1<T>)> {
162 if params.len() != self.curvature.len() {
163 return Err(OptimError::InvalidConfig(format!(
164 "quadratic objective has {} coordinates but received {}",
165 self.curvature.len(),
166 params.len()
167 )));
168 }
169 let half = scirs2_core::numeric::NumCast::from(0.5).unwrap_or_else(|| T::one());
170 let mut loss = T::zero();
171 let mut gradient = Array1::zeros(params.len());
172 for i in 0..params.len() {
173 let offset = params[i] - self.optimum[i];
174 let scaled = self.curvature[i] * offset;
175 loss = loss + half * scaled * offset;
176 gradient[i] = scaled;
177 }
178 Ok((loss, gradient))
179 }
180
181 fn name(&self) -> &str {
182 &self.label
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 #[test]
191 fn quadratic_loss_and_gradient_match_the_closed_form() {
192 let task = QuadraticObjective::<f64>::from_curvature(vec![2.0, 8.0], 0.0)
193 .expect("task")
194 .with_optimum(vec![1.0, -0.5])
195 .expect("optimum")
196 .labelled("test-bowl");
197 assert_eq!(task.dimension(), 2);
198 assert_eq!(task.name(), "test-bowl");
199 assert!((task.condition_number() - 4.0).abs() < 1e-12);
200
201 let point = Array1::from_vec(vec![3.0, 0.5]);
204 let (loss, gradient) = task.loss_and_gradient(&point).expect("evaluate");
205 assert!((loss - 8.0).abs() < 1e-12, "loss {loss}");
206 assert!((gradient[0] - 4.0).abs() < 1e-12);
207 assert!((gradient[1] - 8.0).abs() < 1e-12);
208
209 let (loss, gradient) = task
211 .loss_and_gradient(&Array1::from_vec(vec![1.0, -0.5]))
212 .expect("evaluate");
213 assert_eq!(loss, 0.0);
214 assert!(gradient.iter().all(|g| *g == 0.0));
215 }
216
217 #[test]
220 fn quadratic_gradient_matches_finite_differences() {
221 let task =
222 QuadraticObjective::<f64>::from_curvature(vec![0.7, 3.1, 11.0], 0.0).expect("task");
223 let point = Array1::from_vec(vec![0.4, -1.2, 2.5]);
224 let (_, analytic) = task.loss_and_gradient(&point).expect("analytic");
225
226 let h = 1e-6;
227 for i in 0..point.len() {
228 let mut plus = point.clone();
229 let mut minus = point.clone();
230 plus[i] += h;
231 minus[i] -= h;
232 let (loss_plus, _) = task.loss_and_gradient(&plus).expect("plus");
233 let (loss_minus, _) = task.loss_and_gradient(&minus).expect("minus");
234 let numeric = (loss_plus - loss_minus) / (2.0 * h);
235 assert!(
236 (analytic[i] - numeric).abs() < 1e-6,
237 "coordinate {i}: analytic {} vs numeric {numeric}",
238 analytic[i]
239 );
240 }
241 }
242
243 #[test]
244 fn invalid_objectives_are_rejected() {
245 assert!(QuadraticObjective::<f64>::from_curvature(vec![], 0.0).is_err());
246 assert!(QuadraticObjective::<f64>::from_curvature(vec![0.0], 0.0).is_err());
247 assert!(QuadraticObjective::<f64>::from_curvature(vec![-1.0], 0.0).is_err());
248 assert!(QuadraticObjective::<f64>::from_curvature(vec![f64::NAN], 0.0).is_err());
249 assert!(QuadraticObjective::<f64>::from_curvature(vec![1.0], f64::INFINITY).is_err());
250 assert!(QuadraticObjective::<f64>::isotropic(3, 1.0, 0.5)
251 .expect("task")
252 .with_optimum(vec![0.0, 0.0])
253 .is_err());
254 let task = QuadraticObjective::<f64>::isotropic(2, 1.0, 0.5).expect("task");
255 assert!(task
256 .loss_and_gradient(&Array1::from_vec(vec![1.0]))
257 .is_err());
258 }
259}