optirs_core/plugin/validation/
convergence.rs1use super::{SuiteResult, TestResult, TestSummary, ValidationConfig, ValidationTestSuite};
9use crate::plugin::core::*;
10use scirs2_core::ndarray::Array1;
11use scirs2_core::numeric::Float;
12use std::collections::HashMap;
13use std::time::Instant;
14
15pub(super) type ObjectiveFn<A> = Box<dyn Fn(&Array1<A>) -> A + Send + Sync>;
17
18pub(super) type GradientFn<A> = Box<dyn Fn(&Array1<A>) -> Array1<A> + Send + Sync>;
20
21#[derive(Debug)]
23pub struct ConvergenceTestSuite<A: Float + std::fmt::Debug + Send + Sync> {
24 config: ValidationConfig,
25 test_problems: Vec<TestProblem<A>>,
26}
27
28impl<A: Float + std::fmt::Debug + Send + Sync + 'static> ConvergenceTestSuite<A> {
29 pub fn new(config: ValidationConfig) -> Self {
35 let dimension = config
36 .test_data_sizes
37 .iter()
38 .copied()
39 .find(|size| *size > 0)
40 .unwrap_or(4)
41 .min(4096);
42 Self {
43 config,
44 test_problems: vec![TestProblem::sum_of_squares(dimension)],
45 }
46 }
47
48 pub fn test_problems(&self) -> &[TestProblem<A>] {
50 &self.test_problems
51 }
52}
53
54impl<A: Float + std::fmt::Debug + Send + Sync> ConvergenceTestSuite<A> {
55 fn test_quadratic_convergence(&self, plugin: &mut dyn OptimizerPlugin<A>) -> TestResult {
59 let start_time = Instant::now();
60 if !self.config.check_convergence {
61 return TestResult {
62 passed: true,
63 message: "convergence testing disabled by ValidationConfig::check_convergence"
64 .to_string(),
65 execution_time: start_time.elapsed(),
66 data: HashMap::new(),
67 };
68 }
69 let problem = match self.test_problems.first() {
72 Some(problem) => problem,
73 None => {
74 return TestResult {
75 passed: false,
76 message: "no convergence problem is configured".to_string(),
77 execution_time: start_time.elapsed(),
78 data: HashMap::new(),
79 }
80 }
81 };
82 let dim = problem.initial_params.len();
83 let iterations = problem.max_iterations;
84
85 if let Err(e) = plugin.initialize(&[dim]) {
86 return TestResult {
87 passed: false,
88 message: format!("initialize failed before convergence run: {e}"),
89 execution_time: start_time.elapsed(),
90 data: HashMap::new(),
91 };
92 }
93
94 let mut params: Array1<A> = problem.initial_params.clone();
95 let loss = |p: &Array1<A>| -> A { (problem.objective_fn)(p) };
96 let initial_loss = loss(¶ms);
97
98 for step in 0..iterations {
99 let gradients = (problem.gradient_fn)(¶ms);
100 match plugin.step(¶ms, &gradients) {
101 Ok(next) => params = next,
102 Err(e) => {
103 return TestResult {
104 passed: false,
105 message: format!(
106 "step failed at iteration {step} during convergence run: {e}"
107 ),
108 execution_time: start_time.elapsed(),
109 data: HashMap::new(),
110 };
111 }
112 }
113 let current_loss = loss(¶ms);
114 if !current_loss.is_finite() {
115 return TestResult {
116 passed: false,
117 message: format!("loss diverged to a non-finite value by iteration {step}"),
118 execution_time: start_time.elapsed(),
119 data: HashMap::new(),
120 };
121 }
122 }
123
124 let final_loss = loss(¶ms);
125 let passed = final_loss < initial_loss;
126
127 TestResult {
128 passed,
129 message: format!(
130 "{} over {iterations} steps: initial={initial_loss:?} final={final_loss:?}",
131 problem.name
132 ),
133 execution_time: start_time.elapsed(),
134 data: HashMap::new(),
135 }
136 }
137}
138
139impl<A: Float + std::fmt::Debug + Send + Sync> ValidationTestSuite<A> for ConvergenceTestSuite<A> {
140 fn run_tests(&self, plugin: &mut dyn OptimizerPlugin<A>) -> SuiteResult {
141 let start_time = Instant::now();
142 let result = self.test_quadratic_convergence(plugin);
143 let passed = result.passed;
144
145 SuiteResult {
146 suite_name: "Convergence".to_string(),
147 test_results: vec![result],
148 suite_passed: passed,
149 execution_time: start_time.elapsed(),
150 summary: TestSummary {
151 total_tests: 1,
152 passed_tests: passed as usize,
153 failed_tests: (!passed) as usize,
154 skipped_tests: 0,
155 success_rate: if passed { 1.0 } else { 0.0 },
156 },
157 verified: true,
158 }
159 }
160
161 fn name(&self) -> &str {
162 "Convergence Tests"
163 }
164
165 fn description(&self) -> &str {
166 "Tests for optimization convergence"
167 }
168
169 fn test_count(&self) -> usize {
170 1
171 }
172}
173
174pub struct TestProblem<A: Float + std::fmt::Debug> {
176 pub name: String,
178 pub initial_params: Array1<A>,
180 pub objective_fn: ObjectiveFn<A>,
182 pub gradient_fn: GradientFn<A>,
184 pub optimal_value: Option<A>,
186 pub max_iterations: usize,
188 pub convergence_tolerance: A,
190}
191
192impl<A: Float + std::fmt::Debug + Send + Sync + 'static> TestProblem<A> {
193 pub fn sum_of_squares(dimension: usize) -> Self {
198 let dimension = dimension.max(1);
199 Self {
200 name: format!("sum_of_squares_{dimension}d"),
201 initial_params: Array1::from_iter(
202 (0..dimension).map(|i| A::from(2.0 + i as f64).unwrap_or_else(A::one)),
203 ),
204 objective_fn: Box::new(|params: &Array1<A>| {
205 params.iter().fold(A::zero(), |acc, &x| acc + x * x)
206 }),
207 gradient_fn: Box::new(|params: &Array1<A>| params.mapv(|x| x + x)),
208 optimal_value: Some(A::zero()),
209 max_iterations: 200,
210 convergence_tolerance: A::from(1e-6).unwrap_or_else(A::zero),
211 }
212 }
213}
214
215impl<A: Float + std::fmt::Debug + Send + Sync> std::fmt::Debug for TestProblem<A> {
216 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217 f.debug_struct("TestProblem")
218 .field("name", &self.name)
219 .field("initial_params", &self.initial_params)
220 .field("objective_fn", &"<function>")
221 .field("gradient_fn", &"<function>")
222 .field("optimal_value", &self.optimal_value)
223 .field("max_iterations", &self.max_iterations)
224 .field("convergence_tolerance", &self.convergence_tolerance)
225 .finish()
226 }
227}