Skip to main content

numra_optim/
types.rs

1//! Common types for optimization algorithms.
2//!
3//! Author: Moussa Leblouba
4//! Date: 8 February 2026
5//! Modified: 2 May 2026
6
7use numra_core::Scalar;
8
9/// Options common to unconstrained optimizers.
10#[derive(Clone, Debug)]
11pub struct OptimOptions<S: Scalar> {
12    pub max_iter: usize,
13    pub gtol: S,
14    pub ftol: S,
15    pub xtol: S,
16    pub verbose: bool,
17}
18
19impl<S: Scalar> Default for OptimOptions<S> {
20    fn default() -> Self {
21        Self {
22            max_iter: 1000,
23            gtol: S::from_f64(1e-8),
24            ftol: S::from_f64(1e-12),
25            xtol: S::from_f64(1e-12),
26            verbose: false,
27        }
28    }
29}
30
31impl<S: Scalar> OptimOptions<S> {
32    pub fn max_iter(mut self, n: usize) -> Self {
33        self.max_iter = n;
34        self
35    }
36    pub fn gtol(mut self, tol: S) -> Self {
37        self.gtol = tol;
38        self
39    }
40    pub fn ftol(mut self, tol: S) -> Self {
41        self.ftol = tol;
42        self
43    }
44    pub fn xtol(mut self, tol: S) -> Self {
45        self.xtol = tol;
46        self
47    }
48}
49
50/// Status of an optimization run.
51#[derive(Clone, Debug, PartialEq, Eq)]
52pub enum OptimStatus {
53    GradientConverged,
54    FunctionConverged,
55    StepConverged,
56    MaxIterations,
57    LineSearchFailed,
58    Infeasible,
59    Unbounded,
60}
61
62/// Record of a single iteration for convergence history.
63#[derive(Clone, Debug)]
64pub struct IterationRecord<S: Scalar> {
65    pub iteration: usize,
66    pub objective: S,
67    pub gradient_norm: S,
68    pub step_size: S,
69    pub constraint_violation: S,
70}
71
72/// A single point on a Pareto front.
73#[derive(Clone, Debug)]
74pub struct ParetoPoint<S: Scalar> {
75    /// Decision variable values.
76    pub x: Vec<S>,
77    /// Objective function values.
78    pub objectives: Vec<S>,
79}
80
81/// Result of multi-objective optimization.
82#[derive(Clone, Debug)]
83pub struct ParetoResult<S: Scalar> {
84    /// Pareto-optimal points (non-dominated solutions).
85    pub points: Vec<ParetoPoint<S>>,
86}
87
88/// Parametric sensitivity of optimal solution w.r.t. problem parameters.
89#[derive(Clone, Debug)]
90pub struct ParamSensitivity<S: Scalar> {
91    /// Parameter names.
92    pub names: Vec<String>,
93    /// Sensitivity matrix: `dx_i/dp_j` stored row-major, `n_vars x n_params`.
94    /// `values[i * n_params + j]` = dxstar_i/dp_j.
95    pub values: Vec<S>,
96    /// Number of decision variables.
97    pub n_vars: usize,
98    /// Number of parameters.
99    pub n_params: usize,
100}
101
102impl<S: Scalar> ParamSensitivity<S> {
103    /// Sensitivity of all variables to parameter `j`.
104    pub fn column(&self, j: usize) -> Vec<S> {
105        (0..self.n_vars)
106            .map(|i| self.values[i * self.n_params + j])
107            .collect()
108    }
109
110    /// Sensitivity of variable `i` to all parameters.
111    pub fn row(&self, i: usize) -> Vec<S> {
112        self.values[i * self.n_params..(i + 1) * self.n_params].to_vec()
113    }
114
115    /// Single entry: dxstar_i/dp_j.
116    pub fn get(&self, i: usize, j: usize) -> S {
117        self.values[i * self.n_params + j]
118    }
119}
120
121/// Result of an optimization.
122#[derive(Clone, Debug)]
123pub struct OptimResult<S: Scalar> {
124    pub x: Vec<S>,
125    pub f: S,
126    pub grad: Vec<S>,
127    pub iterations: usize,
128    pub n_feval: usize,
129    pub n_geval: usize,
130    pub converged: bool,
131    pub message: String,
132    pub status: OptimStatus,
133    pub history: Vec<IterationRecord<S>>,
134    pub lambda_eq: Vec<S>,
135    pub lambda_ineq: Vec<S>,
136    pub active_bounds: Vec<usize>,
137    pub constraint_violation: S,
138    pub wall_time_secs: f64,
139    pub pareto: Option<ParetoResult<S>>,
140    pub sensitivity: Option<ParamSensitivity<S>>,
141}
142
143impl<S: Scalar> OptimResult<S> {
144    /// Set wall_time_secs from elapsed time since `start`.
145    pub(crate) fn with_wall_time(mut self, start: std::time::Instant) -> Self {
146        self.wall_time_secs = start.elapsed().as_secs_f64();
147        self
148    }
149
150    /// Construct a result for an unconstrained optimization, filling constrained
151    /// fields with defaults.
152    #[allow(clippy::too_many_arguments)]
153    pub fn unconstrained(
154        x: Vec<S>,
155        f: S,
156        grad: Vec<S>,
157        iterations: usize,
158        n_feval: usize,
159        n_geval: usize,
160        converged: bool,
161        message: String,
162        status: OptimStatus,
163    ) -> Self {
164        Self {
165            x,
166            f,
167            grad,
168            iterations,
169            n_feval,
170            n_geval,
171            converged,
172            message,
173            status,
174            history: Vec::new(),
175            lambda_eq: Vec::new(),
176            lambda_ineq: Vec::new(),
177            active_bounds: Vec::new(),
178            constraint_violation: S::ZERO,
179            wall_time_secs: 0.0,
180            pareto: None,
181            sensitivity: None,
182        }
183    }
184}