Skip to content

Commit 0e869f6

Browse files
committed
feat!: Add Function and Optimizer traits for general optimization
Although the primary goal of Gomez - solving non-linear systems of equations - remains still the same, I believe that providing means for general numerical optimization may benefit someone for the features that Gomez has: first-class support for bounds, simple, low-level, iterative API and various utilities. Moreover, two out of three algorithms implemented so far are optimization algorithms, not solvers anyway. As solving equations is the primary goal, the information about general optimization is rather omitted from the documentation and that is on purpose.
1 parent 6b5ad6e commit 0e869f6

16 files changed

+644
-359
lines changed

benches/solvers.rs

+8-26
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ where
3333
let mut fx = x.clone_owned();
3434
let mut iter = 0;
3535
loop {
36-
if solver.next(&f, &dom, &mut x, &mut fx).is_err() {
36+
if solver.next(f, dom, &mut x, &mut fx).is_err() {
3737
return false;
3838
}
3939

@@ -67,10 +67,7 @@ fn rosenbrock1(c: &mut Criterion) {
6767
assert!(solve(
6868
&f,
6969
&dom,
70-
GslSolverWrapper::new(GslFunctionWrapper::new(
71-
f.clone(),
72-
GslVec::from(x.as_slice())
73-
)),
70+
GslSolverWrapper::new(GslFunctionWrapper::new(f, GslVec::from(x.as_slice()))),
7471
x.clone_owned()
7572
))
7673
})
@@ -95,10 +92,7 @@ fn rosenbrock2(c: &mut Criterion) {
9592
assert!(solve(
9693
&f,
9794
&dom,
98-
GslSolverWrapper::new(GslFunctionWrapper::new(
99-
f.clone(),
100-
GslVec::from(x.as_slice())
101-
)),
95+
GslSolverWrapper::new(GslFunctionWrapper::new(f, GslVec::from(x.as_slice()))),
10296
x.clone_owned()
10397
))
10498
})
@@ -123,10 +117,7 @@ fn rosenbrock_scaled(c: &mut Criterion) {
123117
assert!(solve(
124118
&f,
125119
&dom,
126-
GslSolverWrapper::new(GslFunctionWrapper::new(
127-
f.clone(),
128-
GslVec::from(x.as_slice())
129-
)),
120+
GslSolverWrapper::new(GslFunctionWrapper::new(f, GslVec::from(x.as_slice()))),
130121
x.clone_owned()
131122
))
132123
})
@@ -147,10 +138,7 @@ fn rosenbrock_large(c: &mut Criterion) {
147138
assert!(solve(
148139
&f,
149140
&dom,
150-
GslSolverWrapper::new(GslFunctionWrapper::new(
151-
f.clone(),
152-
GslVec::from(x.as_slice())
153-
)),
141+
GslSolverWrapper::new(GslFunctionWrapper::new(f, GslVec::from(x.as_slice()))),
154142
x.clone_owned()
155143
))
156144
})
@@ -175,10 +163,7 @@ fn powell(c: &mut Criterion) {
175163
assert!(solve(
176164
&f,
177165
&dom,
178-
GslSolverWrapper::new(GslFunctionWrapper::new(
179-
f.clone(),
180-
GslVec::from(x.as_slice())
181-
)),
166+
GslSolverWrapper::new(GslFunctionWrapper::new(f, GslVec::from(x.as_slice()))),
182167
x.clone_owned()
183168
))
184169
})
@@ -199,10 +184,7 @@ fn bullard_biegler(c: &mut Criterion) {
199184
assert!(solve(
200185
&f,
201186
&dom,
202-
GslSolverWrapper::new(GslFunctionWrapper::new(
203-
f.clone(),
204-
GslVec::from(x.as_slice())
205-
)),
187+
GslSolverWrapper::new(GslFunctionWrapper::new(f, GslVec::from(x.as_slice()))),
206188
x.clone_owned()
207189
))
208190
})
@@ -249,7 +231,7 @@ where
249231
na::U1::name(),
250232
);
251233

252-
match self.f.apply(&x, &mut fx) {
234+
match self.f.eval(&x, &mut fx) {
253235
Ok(_) => GslStatus::ok(),
254236
Err(_) => GslStatus::err(GslError::BadFunc),
255237
}

examples/rosenbrock.rs

+9-7
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ struct Rosenbrock {
99
b: f64,
1010
}
1111

12-
impl System for Rosenbrock {
12+
impl Problem for Rosenbrock {
1313
type Scalar = f64;
1414
type Dim = na::U2;
1515

16-
fn apply<Sx, Sfx>(
16+
fn dim(&self) -> Self::Dim {
17+
na::U2::name()
18+
}
19+
}
20+
21+
impl System for Rosenbrock {
22+
fn eval<Sx, Sfx>(
1723
&self,
1824
x: &na::Vector<Self::Scalar, Self::Dim, Sx>,
1925
fx: &mut na::Vector<Self::Scalar, Self::Dim, Sfx>,
20-
) -> Result<(), SystemError>
26+
) -> Result<(), Error>
2127
where
2228
Sx: na::storage::Storage<Self::Scalar, Self::Dim>,
2329
Sfx: na::storage::StorageMut<Self::Scalar, Self::Dim>,
@@ -27,10 +33,6 @@ impl System for Rosenbrock {
2733

2834
Ok(())
2935
}
30-
31-
fn dim(&self) -> Self::Dim {
32-
na::U2::name()
33-
}
3436
}
3537

3638
fn main() -> Result<(), String> {

src/analysis/initial.rs

+5-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use nalgebra::{
1616
use thiserror::Error;
1717

1818
use crate::{
19-
core::{Domain, System, SystemError},
19+
core::{Domain, Error, Problem, System},
2020
derivatives::{Jacobian, JacobianError, EPSILON_SQRT},
2121
};
2222

@@ -25,14 +25,14 @@ use crate::{
2525
pub enum InitialGuessAnalysisError {
2626
/// Error that occurred when evaluating the system.
2727
#[error("{0}")]
28-
System(#[from] SystemError),
28+
System(#[from] Error),
2929
/// Error that occurred when computing the Jacobian matrix.
3030
#[error("{0}")]
3131
Jacobian(#[from] JacobianError),
3232
}
3333

3434
/// Initial guesses analyzer. See [module](self) documentation for more details.
35-
pub struct InitialGuessAnalysis<F: System> {
35+
pub struct InitialGuessAnalysis<F: Problem> {
3636
nonlinear: Vec<usize>,
3737
ty: PhantomData<F>,
3838
}
@@ -59,7 +59,7 @@ where
5959
let scale = OVector::from_iterator_generic(f.dim(), U1::name(), scale_iter);
6060

6161
// Compute F'(x) in the initial point.
62-
f.apply(x, fx)?;
62+
f.eval(x, fx)?;
6363
let jac1 = Jacobian::new(f, x, &scale, fx)?;
6464

6565
// Compute Newton step.
@@ -74,7 +74,7 @@ where
7474
*x += p;
7575

7676
// Compute F'(x) after one Newton step.
77-
f.apply(x, fx)?;
77+
f.eval(x, fx)?;
7878
let jac2 = Jacobian::new(f, x, &scale, fx)?;
7979

8080
// Linear variables have no effect on the Jacobian matrix. They can be

src/core.rs

+11-5
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,23 @@
11
//! Core abstractions and types for Gomez.
22
//!
3-
//! *Users* are mainly interested in implementing the [`System`] trait,
4-
//! optionally specifying the [domain](Domain).
3+
//! *Users* are mainly interested in implementing the [`System`] and [`Problem`]
4+
//! traits, optionally specifying the [domain](Domain).
55
//!
66
//! Algorithms *developers* are interested in implementing the [`Solver`] trait
7-
//! and using extension traits [`SystemExt`] and [`VectorDomainExt`] as well as
8-
//! tools in [derivatives](crate::derivatives) or
9-
//! [population](crate::population) modules.
7+
//! (or possibly the [`Optimizer`] trait too) and using extension traits (e.g.,
8+
//! [`VectorDomainExt`]) as well as tools in [derivatives](crate::derivatives)
9+
//! or [population](crate::population) modules.
1010
11+
mod base;
1112
mod domain;
13+
mod function;
14+
mod optimizer;
1215
mod solver;
1316
mod system;
1417

18+
pub use base::*;
1519
pub use domain::*;
20+
pub use function::*;
21+
pub use optimizer::*;
1622
pub use solver::*;
1723
pub use system::*;

src/core/base.rs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use nalgebra::{Dim, RealField};
2+
use thiserror::Error;
3+
4+
use super::domain::Domain;
5+
6+
/// The base trait for [`System`](super::system::System) and
7+
/// [`Function`](super::function::Function).
8+
pub trait Problem {
9+
/// Type of the scalar, usually f32 or f64.
10+
type Scalar: RealField;
11+
12+
/// Dimension of the system. Can be fixed
13+
/// ([`Const`](nalgebra::base::dimension::Const)) or dynamic
14+
/// ([`Dynamic`](nalgebra::base::dimension::Dynamic)).
15+
type Dim: Dim;
16+
17+
/// Return the actual dimension of the system. This is needed for dynamic
18+
/// systems.
19+
fn dim(&self) -> Self::Dim;
20+
21+
/// Get the domain (bound constraints) of the system. If not overridden, the
22+
/// system is unconstrained.
23+
fn domain(&self) -> Domain<Self::Scalar> {
24+
Domain::with_dim(self.dim().value())
25+
}
26+
}
27+
28+
/// Error encountered while applying variables to the function.
29+
#[derive(Debug, Error)]
30+
pub enum Error {
31+
/// The number of variables does not match the dimensionality
32+
/// ([`Problem::dim`]) of the problem.
33+
#[error("invalid dimensionality")]
34+
InvalidDimensionality,
35+
/// An invalid value (NaN, positive or negative infinity) of a residual or
36+
/// the function value occurred.
37+
#[error("invalid value encountered")]
38+
InvalidValue,
39+
/// A custom error specific to the system or function.
40+
#[error("{0}")]
41+
Custom(Box<dyn std::error::Error>),
42+
}

src/core/function.rs

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
use nalgebra::{
2+
allocator::Allocator, storage::Storage, storage::StorageMut, DefaultAllocator, Vector,
3+
};
4+
use num_traits::Zero;
5+
6+
use super::{
7+
base::{Error, Problem},
8+
system::System,
9+
};
10+
11+
/// The trait for defining functions.
12+
///
13+
/// ## Defining a function
14+
///
15+
/// A function is any type that implements [`Function`] and [`Problem`] traits.
16+
/// There are two required associated types (scalar type and dimension type) and
17+
/// two required methods: [`apply`](Function::apply) and [`dim`](Problem::dim).
18+
///
19+
/// ```rust
20+
/// use gomez::nalgebra as na;
21+
/// use gomez::prelude::*;
22+
/// use na::{Dim, DimName};
23+
///
24+
/// // A problem is represented by a type.
25+
/// struct Rosenbrock {
26+
/// a: f64,
27+
/// b: f64,
28+
/// }
29+
///
30+
/// impl Problem for Rosenbrock {
31+
/// // The numeric type. Usually f64 or f32.
32+
/// type Scalar = f64;
33+
/// // The dimension of the problem. Can be either statically known or dynamic.
34+
/// type Dim = na::U2;
35+
///
36+
/// // Return the actual dimension of the system.
37+
/// fn dim(&self) -> Self::Dim {
38+
/// na::U2::name()
39+
/// }
40+
/// }
41+
///
42+
/// impl Function for Rosenbrock {
43+
/// // Apply trial values of variables to the function.
44+
/// fn apply<Sx>(
45+
/// &self,
46+
/// x: &na::Vector<Self::Scalar, Self::Dim, Sx>,
47+
/// ) -> Result<Self::Scalar, Error>
48+
/// where
49+
/// Sx: na::storage::Storage<Self::Scalar, Self::Dim>,
50+
/// {
51+
/// // Compute the function value.
52+
/// Ok((self.a - x[0]).powi(2) + self.b * (x[1] - x[0].powi(2)).powi(2))
53+
/// }
54+
/// }
55+
/// ```
56+
pub trait Function: Problem {
57+
/// Calculate the function value given values of the variables.
58+
fn apply<Sx>(&self, x: &Vector<Self::Scalar, Self::Dim, Sx>) -> Result<Self::Scalar, Error>
59+
where
60+
Sx: Storage<Self::Scalar, Self::Dim>;
61+
62+
/// Calculate the norm of residuals of the system given values of the
63+
/// variable for cases when the function is actually a system of equations.
64+
///
65+
/// The optimizers should prefer calling this function because the
66+
/// implementation for systems reuse `fx` for calculating the residuals and
67+
/// do not make an unnecessary allocation for it.
68+
fn apply_eval<Sx, Sfx>(
69+
&self,
70+
x: &Vector<Self::Scalar, Self::Dim, Sx>,
71+
fx: &mut Vector<Self::Scalar, Self::Dim, Sfx>,
72+
) -> Result<Self::Scalar, Error>
73+
where
74+
Sx: Storage<Self::Scalar, Self::Dim>,
75+
Sfx: StorageMut<Self::Scalar, Self::Dim>,
76+
{
77+
let norm = self.apply(x)?;
78+
fx.fill(Self::Scalar::zero());
79+
fx[0] = norm;
80+
Ok(norm)
81+
}
82+
}
83+
84+
impl<F: System> Function for F
85+
where
86+
DefaultAllocator: Allocator<F::Scalar, F::Dim>,
87+
{
88+
fn apply<Sx>(&self, x: &Vector<Self::Scalar, Self::Dim, Sx>) -> Result<Self::Scalar, Error>
89+
where
90+
Sx: Storage<Self::Scalar, Self::Dim>,
91+
{
92+
let mut fx = x.clone_owned();
93+
self.apply_eval(x, &mut fx)
94+
}
95+
96+
fn apply_eval<Sx, Sfx>(
97+
&self,
98+
x: &Vector<Self::Scalar, Self::Dim, Sx>,
99+
fx: &mut Vector<Self::Scalar, Self::Dim, Sfx>,
100+
) -> Result<Self::Scalar, Error>
101+
where
102+
Sx: Storage<Self::Scalar, Self::Dim>,
103+
Sfx: StorageMut<Self::Scalar, Self::Dim>,
104+
{
105+
self.eval(x, fx)?;
106+
let norm = fx.norm();
107+
Ok(norm)
108+
}
109+
}

0 commit comments

Comments
 (0)