Skip to content

Commit c69a26b

Browse files
committed
Merge branch 'feature/372-rust-float' into feature/410-ros2-error-handling
2 parents b0b2b06 + 56934b7 commit c69a26b

2 files changed

Lines changed: 62 additions & 94 deletions

File tree

docs/openrust-arithmetic.mdx

Lines changed: 47 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -4,132 +4,110 @@ title: Single and double precision
44
description: OpEn with f32 and f64 number types
55
---
66

7+
import Tabs from '@theme/Tabs';
8+
import TabItem from '@theme/TabItem';
9+
710
:::note Info
8-
The functionality presented here was introduced in OpEn version [`0.12.0`](https://pypi.org/project/opengen/#history).
9-
The new API is fully backward-compatible with previous versions of OpEn.
11+
The functionality presented here was introduced in OpEn version [`0.12.0`](https://crates.io/crates/optimization_engine/0.12.0-alpha.1).
12+
The new API is fully backward-compatible with previous versions of OpEn
13+
with `f64` being the default scalar type.
1014
:::
1115

1216
## Overview
1317

14-
OpEn's Rust API supports both `f64` and `f32`.
15-
16-
Most public Rust types are generic over a scalar type `T` with `T: num::Float`, and in most places the default type is `f64`. This means:
17-
18-
- if you do nothing special, you will usually get `f64`
19-
- if you want single precision, you can explicitly use `f32`
20-
- all quantities involved in one solver instance should use the same scalar type
21-
22-
In particular, this applies to:
23-
24-
- cost and gradient functions
25-
- constraints
26-
- `Problem`
27-
- caches such as `PANOCCache`, `FBSCache`, and `AlmCache`
28-
- optimizers such as `PANOCOptimizer`, `FBSOptimizer`, and `AlmOptimizer`
29-
- solver status types such as `SolverStatus<T>` and `AlmOptimizerStatus<T>`
30-
31-
## When to use `f64` and when to use `f32`
32-
33-
### `f64`
34-
35-
Use `f64` when you want maximum numerical robustness and accuracy. This is the safest default for:
18+
OpEn's Rust API now supports both `f64` and `f32`. Note that with `f32`
19+
you may encounter issues with convergence, especially if you are solving
20+
particularly ill-conditioned problems. On the other hand, `f32` is sometimes
21+
the preferred type for embedded applications and can lead to lower
22+
solve times.
3623

37-
- desktop applications
38-
- difficult nonlinear problems
39-
- problems with tight tolerances
40-
- problems that are sensitive to conditioning
24+
When using `f32`: (i) make sure the problem is properly scaled,
25+
and (ii) you may want to opt for less demanding tolerances.
4126

42-
### `f32`
27+
## PANOC example
4328

44-
Use `f32` when memory footprint and throughput matter more than ultimate accuracy. This is often useful for:
29+
Below you can see two examples of using the solver with single and double
30+
precision arithmetic.
4531

46-
- embedded applications
47-
- high-rate MPC loops
48-
- applications where moderate tolerances are acceptable
32+
<Tabs>
4933

50-
In general, `f32` may require:
5134

52-
- slightly looser tolerances
53-
- more careful scaling of the problem
54-
- fewer expectations about extremely small residuals
55-
56-
## The default: `f64`
57-
58-
If your functions, constants, and vectors use `f64`, you can often omit the scalar type completely.
35+
<TabItem value="using-f32" label="Single precision">
5936

6037
```rust
6138
use optimization_engine::{constraints, panoc::PANOCCache, Problem, SolverError};
6239
use optimization_engine::panoc::PANOCOptimizer;
6340

64-
let tolerance = 1e-6;
41+
let tolerance = 1e-4_f32;
6542
let lbfgs_memory = 10;
66-
let radius = 1.0;
43+
let radius = 1.0_f32;
6744

6845
let bounds = constraints::Ball2::new(None, radius);
6946

70-
let df = |u: &[f64], grad: &mut [f64]| -> Result<(), SolverError> {
71-
grad[0] = u[0] + u[1] + 1.0;
72-
grad[1] = u[0] + 2.0 * u[1] - 1.0;
47+
let df = |u: &[f32], grad: &mut [f32]| -> Result<(), SolverError> {
48+
grad[0] = u[0] + u[1] + 1.0_f32;
49+
grad[1] = u[0] + 2.0_f32 * u[1] - 1.0_f32;
7350
Ok(())
7451
};
7552

76-
let f = |u: &[f64], cost: &mut f64| -> Result<(), SolverError> {
77-
*cost = 0.5 * (u[0] * u[0] + u[1] * u[1]);
53+
let f = |u: &[f32], cost: &mut f32| -> Result<(), SolverError> {
54+
*cost = 0.5_f32 * (u[0] * u[0] + u[1] * u[1]);
7855
Ok(())
7956
};
8057

8158
let problem = Problem::new(&bounds, df, f);
82-
let mut cache = PANOCCache::new(2, tolerance, lbfgs_memory);
59+
let mut cache = PANOCCache::<f32>::new(2, tolerance, lbfgs_memory);
8360
let mut optimizer = PANOCOptimizer::new(problem, &mut cache);
8461

85-
let mut u = [0.0, 0.0];
62+
let mut u = [0.0_f32, 0.0_f32];
8663
let status = optimizer.solve(&mut u).unwrap();
8764
assert!(status.has_converged());
8865
```
66+
</TabItem>
8967

90-
Because all literals and function signatures above are `f64`, the compiler infers `T = f64`.
91-
92-
## Using `f32`
93-
94-
To use single precision, make the scalar type explicit throughout the problem definition.
68+
<TabItem value="default-f64" label="Double precision" default>
9569

9670
```rust
9771
use optimization_engine::{constraints, panoc::PANOCCache, Problem, SolverError};
9872
use optimization_engine::panoc::PANOCOptimizer;
9973

100-
let tolerance = 1e-4_f32;
74+
let tolerance = 1e-6;
10175
let lbfgs_memory = 10;
102-
let radius = 1.0_f32;
76+
let radius = 1.0;
10377

10478
let bounds = constraints::Ball2::new(None, radius);
10579

106-
let df = |u: &[f32], grad: &mut [f32]| -> Result<(), SolverError> {
107-
grad[0] = u[0] + u[1] + 1.0_f32;
108-
grad[1] = u[0] + 2.0_f32 * u[1] - 1.0_f32;
80+
let df = |u: &[f64], grad: &mut [f64]| -> Result<(), SolverError> {
81+
grad[0] = u[0] + u[1] + 1.0;
82+
grad[1] = u[0] + 2.0 * u[1] - 1.0;
10983
Ok(())
11084
};
11185

112-
let f = |u: &[f32], cost: &mut f32| -> Result<(), SolverError> {
113-
*cost = 0.5_f32 * (u[0] * u[0] + u[1] * u[1]);
86+
let f = |u: &[f64], cost: &mut f64| -> Result<(), SolverError> {
87+
*cost = 0.5 * (u[0] * u[0] + u[1] * u[1]);
11488
Ok(())
11589
};
11690

11791
let problem = Problem::new(&bounds, df, f);
118-
let mut cache = PANOCCache::<f32>::new(2, tolerance, lbfgs_memory);
92+
let mut cache = PANOCCache::new(2, tolerance, lbfgs_memory);
11993
let mut optimizer = PANOCOptimizer::new(problem, &mut cache);
12094

121-
let mut u = [0.0_f32, 0.0_f32];
95+
let mut u = [0.0, 0.0];
12296
let status = optimizer.solve(&mut u).unwrap();
12397
assert!(status.has_converged());
12498
```
99+
</TabItem>
100+
101+
</Tabs>
125102

126-
The key idea is that the same scalar type must be used consistently in:
103+
To use single precision, make sure that the following are all using `f32`:
127104

128105
- the initial guess `u`
129106
- the closures for the cost and gradient
130107
- the constraints
131108
- the cache
132109
- any tolerances and numerical constants
110+
- You are explicitly using `PANOCCache::<f32>` as in the above example
133111

134112
## Example with FBS
135113

@@ -175,7 +153,7 @@ For example, if you use:
175153

176154
then the whole ALM solve runs in single precision.
177155

178-
If instead you use plain `f64` literals and `&[f64]` closures, the solver runs in double precision.
156+
If instead you use plain `f64` literals and `&[f64]` closures, the solver runs in double precision. This is the default behaviour.
179157

180158
## Type inference tips
181159

@@ -188,28 +166,11 @@ Good ways to make `f32` intent clear are:
188166
- annotate caches explicitly, for example `PANOCCache::<f32>::new(...)`
189167
- annotate closure arguments, for example `|u: &[f32], grad: &mut [f32]|`
190168

191-
## Important rule: do not mix `f32` and `f64`
192-
193-
The following combinations are problematic:
169+
:::warning Important rule: do not mix `f32` and `f64`
170+
For example, the following combinations are problematic:
194171

195172
- `u: &[f32]` with a cost function writing to `&mut f64`
196173
- `Ball2::new(None, 1.0_f64)` together with `PANOCCache::<f32>`
197-
- `tolerance = 1e-6` in one place and `1e-6_f32` elsewhere if inference becomes ambiguous
198174

199175
Choose one scalar type per optimization problem and use it everywhere.
200-
201-
## Choosing tolerances
202-
203-
When moving from `f64` to `f32`, it is often a good idea to relax tolerances.
204-
205-
Typical starting points are:
206-
207-
- `f64`: `1e-6`, `1e-8`, or smaller if needed
208-
- `f32`: `1e-4` or `1e-5`
209-
210-
The right choice depends on:
211-
212-
- scaling of the problem
213-
- conditioning
214-
- solver settings
215-
- whether the problem is solved repeatedly in real time
176+
:::

docs/openrust-basic.md

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ The definition of an optimization problem consists in specifying the following t
2525
- the set of constraints, $U$, as an implementation of a trait
2626

2727
### Cost functions
28+
29+
:::note Info
30+
Throughout this document we will be using `f64`, which is the default
31+
scalar type. However, OpEn now supports `f32` as well.
32+
:::
33+
34+
2835
The **cost function** `f` is a Rust function of type `|u: &[f64], cost: &mut f64| -> Result<(), SolverError>`. The first argument, `u`, is the argument of the function. The second argument, is a mutable reference to the result (cost). The function returns a *status code* of the type `Result<(), SolverError>` and the status code `Ok(())` means that the computation was successful. Other status codes can be used to encode errors/exceptions as defined in the [`SolverError`] enum.
2936

3037
As an example, consider the cost function $f:\mathbb{R}^2\to\mathbb{R}$ that maps a two-dimensional
@@ -33,8 +40,8 @@ vector $u$ to $f(u) = 5 u_1 - u_2^2$. This will be:
3340

3441
```rust
3542
let f = |u: &[f64], c: &mut f64| -> Result<(), SolverError> {
36-
*c = 5.0 * u[0] - u[1].powi(2);
37-
Ok(())
43+
*c = 5.0 * u[0] - u[1].powi(2);
44+
Ok(())
3845
};
3946
```
4047

@@ -50,9 +57,9 @@ This function can be implemented as follows:
5057

5158
```rust
5259
let df = |u: &[f64], grad: &mut [f64]| -> Result<(), SolverError> {
53-
grad[0] = 5.0;
54-
grad[1] = -2.0*u[1];
55-
Ok(())
60+
grad[0] = 5.0;
61+
grad[1] = -2.0*u[1];
62+
Ok(())
5663
};
5764
```
5865

@@ -291,9 +298,9 @@ fn main() {
291298
}
292299
};
293300

294-
// define the bounds at every iteration
295-
let bounds = constraints::Ball2::new(None, radius);
296-
301+
// define the bounds at every iteration
302+
let bounds = constraints::Ball2::new(None, radius);
303+
297304
// the problem definition is updated at every iteration
298305
let problem = Problem::new(&bounds, df, f);
299306

0 commit comments

Comments
 (0)