Skip to main content

optirs_core/metrics/
optimizer.rs

1// Integration with scirs2-metrics for optimization
2//
3// This module provides the MetricOptimizer which uses metrics to guide optimization.
4
5#[cfg(not(feature = "metrics-integration"))]
6use crate::error::OptimError;
7use crate::error::Result;
8use crate::optimizers::Optimizer;
9#[cfg(feature = "metrics-integration")]
10use scirs2_core::ndarray::{Array, Dimension, ScalarOperand};
11#[cfg(not(feature = "metrics-integration"))]
12use scirs2_core::ndarray::{Dimension, ScalarOperand};
13use scirs2_core::numeric::{Float, FromPrimitive};
14#[cfg(feature = "metrics-integration")]
15use std::collections::HashMap;
16use std::fmt::{Debug, Display};
17use std::marker::PhantomData;
18
19/// An optimizer guided by metric values
20#[cfg(feature = "metrics-integration")]
21pub struct MetricOptimizer<F, D>
22where
23    F: Float + Debug + Display + FromPrimitive + ScalarOperand,
24    D: Dimension,
25{
26    /// Base optimizer
27    base_optimizer: Box<dyn Optimizer<F, D>>,
28    /// Current learning rate
29    current_lr: F,
30    /// Metric adapter
31    metric_adapter: scirs2_metrics::integration::optim::MetricOptimizer<F>,
32    /// History of parameter updates
33    history: Vec<HashMap<String, Array<F, D>>>,
34    /// Best parameters found
35    best_params: Option<HashMap<String, Array<F, D>>>,
36    /// PhantomData for F and D
37    _phantom: PhantomData<(F, D)>,
38}
39
40#[cfg(feature = "metrics-integration")]
41impl<F, D> MetricOptimizer<F, D>
42where
43    F: Float + Debug + Display + FromPrimitive + ScalarOperand + Send + Sync + 'static,
44    D: Dimension + 'static,
45{
46    /// Create a new MetricOptimizer
47    ///
48    /// # Errors
49    /// This constructor is currently infallible under the `metrics-integration`
50    /// feature, but returns [`Result`] to keep the API symmetric with the
51    /// feature-disabled fallback (which always errors).
52    pub fn new<O>(optimizer: O, metric_name: &str, maximize: bool) -> Result<Self>
53    where
54        O: Optimizer<F, D> + 'static,
55    {
56        let initial_lr = optimizer.get_learning_rate();
57        Ok(Self {
58            base_optimizer: Box::new(optimizer),
59            current_lr: initial_lr,
60            metric_adapter: scirs2_metrics::integration::optim::MetricOptimizer::new(
61                metric_name,
62                maximize,
63            ),
64            history: Vec::new(),
65            best_params: None,
66            _phantom: PhantomData,
67        })
68    }
69
70    /// Update the optimizer with a metric value
71    pub fn update_metric(&mut self, metric: F) -> Result<()> {
72        self.metric_adapter.add_value(metric);
73        Ok(())
74    }
75
76    /// Update multiple metrics
77    pub fn update_metrics(&mut self, metrics: HashMap<String, F>) -> Result<()> {
78        // Update the primary metric
79        if let Some(value) = metrics.get(self.metric_adapter.metric_name()) {
80            self.metric_adapter.add_value(*value);
81        }
82
83        // Update additional metrics
84        for (name, value) in metrics {
85            if name != self.metric_adapter.metric_name() {
86                self.metric_adapter.add_additional_value(&name, value);
87            }
88        }
89
90        Ok(())
91    }
92
93    /// Get the metric adapter
94    pub fn metric_adapter(&self) -> &scirs2_metrics::integration::optim::MetricOptimizer<F> {
95        &self.metric_adapter
96    }
97
98    /// Get the metric adapter (mutable)
99    pub fn metric_adapter_mut(
100        &mut self,
101    ) -> &mut scirs2_metrics::integration::optim::MetricOptimizer<F> {
102        &mut self.metric_adapter
103    }
104
105    /// Get the base optimizer
106    pub fn base_optimizer(&self) -> &dyn Optimizer<F, D> {
107        &*self.base_optimizer
108    }
109
110    /// Get the base optimizer (mutable)
111    pub fn base_optimizer_mut(&mut self) -> &mut dyn Optimizer<F, D> {
112        &mut *self.base_optimizer
113    }
114
115    /// Get the best parameters found
116    pub fn best_params(&self) -> Option<&HashMap<String, Array<F, D>>> {
117        self.best_params.as_ref()
118    }
119
120    /// Get the parameter update history
121    pub fn history(&self) -> &[HashMap<String, Array<F, D>>] {
122        &self.history
123    }
124
125    /// Reset the optimizer
126    pub fn reset(&mut self) {
127        self.metric_adapter.reset();
128        self.history.clear();
129        self.best_params = None;
130    }
131
132    /// Create a learning rate scheduler for this optimizer
133    pub fn create_lr_scheduler(
134        &self,
135        initial_lr: F,
136        factor: F,
137        patience: usize,
138        min_lr: F,
139    ) -> crate::schedulers::ReduceOnPlateau<F> {
140        let mut scheduler =
141            crate::schedulers::ReduceOnPlateau::new(initial_lr, factor, patience, min_lr);
142
143        // Set mode based on optimization mode
144        match self.metric_adapter.mode() {
145            scirs2_metrics::integration::optim::OptimizationMode::Minimize => {
146                scheduler.mode_min();
147            }
148            scirs2_metrics::integration::optim::OptimizationMode::Maximize => {
149                scheduler.mode_max();
150            }
151        }
152
153        scheduler
154    }
155}
156
157#[cfg(feature = "metrics-integration")]
158impl<F, D> Optimizer<F, D> for MetricOptimizer<F, D>
159where
160    F: Float + Debug + Display + FromPrimitive + ScalarOperand + 'static,
161    D: Dimension + 'static,
162{
163    fn step(&mut self, params: &Array<F, D>, gradients: &Array<F, D>) -> Result<Array<F, D>> {
164        // Use base optimizer to update parameters
165        let updated_params = self.base_optimizer.step(params, gradients)?;
166
167        // Record parameter update in history
168        let mut param_update = HashMap::new();
169        param_update.insert("params".to_string(), updated_params.clone());
170        param_update.insert("gradients".to_string(), gradients.clone());
171        self.history.push(param_update);
172
173        // Update best parameters if metric has improved
174        if let Some(best_value) = self.metric_adapter.best_value() {
175            let is_improvement = match self.metric_adapter.mode() {
176                scirs2_metrics::integration::optim::OptimizationMode::Maximize => {
177                    // If maximizing, latest metric should be greater than best
178                    if let Some(last_value) = self.metric_adapter.history().last() {
179                        *last_value > best_value
180                    } else {
181                        false
182                    }
183                }
184                scirs2_metrics::integration::optim::OptimizationMode::Minimize => {
185                    // If minimizing, latest metric should be less than best
186                    if let Some(last_value) = self.metric_adapter.history().last() {
187                        *last_value < best_value
188                    } else {
189                        false
190                    }
191                }
192            };
193
194            if is_improvement {
195                let mut best_params = HashMap::new();
196                best_params.insert("params".to_string(), updated_params.clone());
197                self.best_params = Some(best_params);
198            }
199        }
200
201        Ok(updated_params)
202    }
203
204    fn get_learning_rate(&self) -> F {
205        self.current_lr
206    }
207
208    fn set_learning_rate(&mut self, learning_rate: F) {
209        self.current_lr = learning_rate;
210    }
211}
212
213/// Error raised when metrics integration is not enabled
214#[cfg(not(feature = "metrics-integration"))]
215#[derive(Debug)]
216pub struct MetricOptimizer<F, D>
217where
218    F: Float + Debug + Display + FromPrimitive + ScalarOperand,
219    D: Dimension,
220{
221    _phantom: PhantomData<(F, D)>,
222}
223
224#[cfg(not(feature = "metrics-integration"))]
225impl<F, D> MetricOptimizer<F, D>
226where
227    F: Float + Debug + Display + FromPrimitive + ScalarOperand,
228    D: Dimension,
229{
230    /// Create a new MetricOptimizer (requires the `metrics-integration` feature)
231    ///
232    /// # Errors
233    /// Returns [`OptimError::MissingDependency`] because this crate was built
234    /// without the `metrics-integration` feature enabled.
235    pub fn new<O>(_optimizer: O, _metric_name: &str, _maximize: bool) -> Result<Self>
236    where
237        O: Optimizer<F, D>,
238    {
239        Err(OptimError::MissingDependency(
240            "metrics-integration feature is not enabled - enable it in your Cargo.toml".to_string(),
241        ))
242    }
243}