Skip to content

Commit 355f035

Browse files
committed
handling errors via py bindings
- update unit tests - and website docs - and benchmarks
1 parent 0bd29d4 commit 355f035

5 files changed

Lines changed: 280 additions & 57 deletions

File tree

docs/example_navigation_py.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ sys.path.insert(1, './my_optimizers/navigation')
154154
import navigation
155155

156156
solver = navigation.solver()
157-
result = solver.run(p=[-1.0, 2.0, 0.0],
158-
initial_guess=[1.0] * (nu*N))
159-
u_star = result.solution
157+
response = solver.run(p=[-1.0, 2.0, 0.0],
158+
initial_guess=[1.0] * (nu*N))
159+
u_star = response.get().solution
160160

161161

162162
# Plot solution
@@ -289,5 +289,5 @@ problem = og.builder.Problem(u, p, cost).with_constraints(bounds)
289289
Then, when we use the optimiser we to provide the vector `p`. For example, if `z0 = (-1, 2, 0)` and `xref = 1`, `yref = 0.6`, `thetaref = 0.05` we use
290290

291291
```python
292-
result = solver.run(p=[-1.0, 2.0, 0.0, 1.0, 0.6, 0.05])
292+
result = solver.run(p=[-1.0, 2.0, 0.0, 1.0, 0.6, 0.05]).get()
293293
```

docs/python-bindings.md

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,22 +78,37 @@ Then you will be able to use it as follows:
7878

7979
```python
8080
solver = rosenbrock.solver()
81-
result = solver.run(p=[20., 1.])
81+
response = solver.run(p=[20., 1.])
82+
result = response.get()
8283
u_star = result.solution
8384
```
8485

8586
In the first line, `solver = rosenbrock.solver()`, we obtain an instance of
8687
`Solver`, which can be used to solve parametric optimization problems.
87-
In the second line, `result = solver.run(p=[20., 1.])`, we call the solver
88+
In the second line, `response = solver.run(p=[20., 1.])`, we call the solver
8889
with parameter $p=(20, 1)$. Method `run` accepts another three optional
8990
arguments, namely:
9091

9192
- `initial_guess` (can be either a list or a numpy array),
9293
- `initial_lagrange_multipliers`, and
9394
- `initial_penalty`
9495

95-
The solver returns an object of type `OptimizerSolution` with the following
96-
properties:
96+
The solver returns an object of type `SolverResponse`, similar to the TCP
97+
interface. First call `response.is_ok()` to determine whether the call
98+
succeeded, then call `response.get()` to obtain either a `SolverStatus`
99+
object or a `SolverError`.
100+
101+
```python
102+
response = solver.run(p=[20., 1.])
103+
if response.is_ok():
104+
status = response.get()
105+
u_star = status.solution
106+
else:
107+
error = response.get()
108+
print(error.code, error.message)
109+
```
110+
111+
The `SolverStatus` object exposes the following properties:
97112

98113

99114
| Property | Explanation |
@@ -112,6 +127,13 @@ properties:
112127

113128
These are the same properties as those of `opengen.tcp.SolverStatus`.
114129

130+
If the call fails, `response.get()` returns a `SolverError` with:
131+
132+
| Property | Explanation |
133+
|-----------|-------------|
134+
| `code` | Error code, aligned with the TCP interface |
135+
| `message` | Detailed error message |
136+
115137

116138
## Importing optimizer with variable name
117139

open-codegen/opengen/templates/python/python_bindings.rs

Lines changed: 147 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ use {{ meta.optimizer_name }}::*;
1111
#[pymodule]
1212
fn {{ meta.optimizer_name }}(_py: Python, m: &PyModule) -> PyResult<()> {
1313
m.add_function(wrap_pyfunction!(solver, m)?)?;
14-
m.add_class::<OptimizerSolution>()?;
14+
m.add_class::<SolverStatus>()?;
15+
m.add_class::<SolverError>()?;
16+
m.add_class::<SolverResponse>()?;
1517
m.add_class::<Solver>()?;
18+
m.add("OptimizerSolution", m.getattr("SolverStatus")?)?;
1619
Ok(())
1720
}
1821

@@ -22,9 +25,53 @@ fn solver() -> PyResult<Solver> {
2225
Ok(Solver { cache })
2326
}
2427

28+
#[derive(Clone)]
29+
struct SolverStatusData {
30+
exit_status: String,
31+
num_outer_iterations: usize,
32+
num_inner_iterations: usize,
33+
last_problem_norm_fpr: f64,
34+
f1_infeasibility: f64,
35+
f2_norm: f64,
36+
solve_time_ms: f64,
37+
penalty: f64,
38+
solution: Vec<f64>,
39+
lagrange_multipliers: Vec<f64>,
40+
cost: f64,
41+
}
42+
43+
impl SolverStatusData {
44+
fn from_status(status: AlmOptimizerStatus, solution: &[f64]) -> Self {
45+
SolverStatusData {
46+
exit_status: format!("{:?}", status.exit_status()),
47+
num_outer_iterations: status.num_outer_iterations(),
48+
num_inner_iterations: status.num_inner_iterations(),
49+
last_problem_norm_fpr: status.last_problem_norm_fpr(),
50+
f1_infeasibility: status.delta_y_norm_over_c(),
51+
f2_norm: status.f2_norm(),
52+
penalty: status.penalty(),
53+
lagrange_multipliers: status.lagrange_multipliers().clone().unwrap_or_default(),
54+
solve_time_ms: (status.solve_time().as_nanos() as f64) / 1e6,
55+
solution: solution.to_vec(),
56+
cost: status.cost(),
57+
}
58+
}
59+
}
60+
61+
#[derive(Clone)]
62+
struct SolverErrorData {
63+
code: i32,
64+
message: String,
65+
}
66+
67+
enum SolverResponsePayload {
68+
Ok(SolverStatusData),
69+
Err(SolverErrorData),
70+
}
71+
2572
/// Solution and solution status of optimizer
2673
#[pyclass]
27-
struct OptimizerSolution {
74+
struct SolverStatus {
2875
#[pyo3(get)]
2976
exit_status: String,
3077
#[pyo3(get)]
@@ -49,6 +96,64 @@ struct OptimizerSolution {
4996
cost: f64,
5097
}
5198

99+
impl From<SolverStatusData> for SolverStatus {
100+
fn from(status: SolverStatusData) -> Self {
101+
SolverStatus {
102+
exit_status: status.exit_status,
103+
num_outer_iterations: status.num_outer_iterations,
104+
num_inner_iterations: status.num_inner_iterations,
105+
last_problem_norm_fpr: status.last_problem_norm_fpr,
106+
f1_infeasibility: status.f1_infeasibility,
107+
f2_norm: status.f2_norm,
108+
solve_time_ms: status.solve_time_ms,
109+
penalty: status.penalty,
110+
solution: status.solution,
111+
lagrange_multipliers: status.lagrange_multipliers,
112+
cost: status.cost,
113+
}
114+
}
115+
}
116+
117+
#[pyclass]
118+
struct SolverError {
119+
#[pyo3(get)]
120+
code: i32,
121+
#[pyo3(get)]
122+
message: String,
123+
}
124+
125+
impl From<SolverErrorData> for SolverError {
126+
fn from(error: SolverErrorData) -> Self {
127+
SolverError {
128+
code: error.code,
129+
message: error.message,
130+
}
131+
}
132+
}
133+
134+
#[pyclass]
135+
struct SolverResponse {
136+
payload: SolverResponsePayload,
137+
}
138+
139+
#[pymethods]
140+
impl SolverResponse {
141+
fn is_ok(&self) -> bool {
142+
matches!(self.payload, SolverResponsePayload::Ok(_))
143+
}
144+
145+
fn get(&self, py: Python<'_>) -> PyResult<PyObject> {
146+
match &self.payload {
147+
SolverResponsePayload::Ok(status) => {
148+
Ok(Py::new(py, SolverStatus::from(status.clone()))?.into_py(py))
149+
}
150+
SolverResponsePayload::Err(error) => {
151+
Ok(Py::new(py, SolverError::from(error.clone()))?.into_py(py))
152+
}
153+
}
154+
}
155+
}
156+
52157
#[pyclass]
53158
struct Solver {
54159
cache: AlmCache,
@@ -65,20 +170,24 @@ impl Solver {
65170
initial_guess: Option<Vec<f64>>,
66171
initial_lagrange_multipliers: Option<Vec<f64>>,
67172
initial_penalty: Option<f64>,
68-
) -> PyResult<Option<OptimizerSolution>> {
173+
) -> PyResult<SolverResponse> {
69174
let mut u = [0.0; {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES];
70175

71176
// ----------------------------------------------------
72177
// Set initial value
73178
// ----------------------------------------------------
74179
if let Some(u0) = initial_guess {
75180
if u0.len() != {{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES {
76-
println!(
77-
"1600 -> Initial guess has incompatible dimensions: {} != {}",
78-
u0.len(),
79-
{{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES
80-
);
81-
return Ok(None);
181+
return Ok(SolverResponse {
182+
payload: SolverResponsePayload::Err(SolverErrorData {
183+
code: 1600,
184+
message: format!(
185+
"initial guess has incompatible dimensions: provided {}, expected {}",
186+
u0.len(),
187+
{{meta.optimizer_name|upper}}_NUM_DECISION_VARIABLES
188+
),
189+
}),
190+
});
82191
}
83192
u.copy_from_slice(&u0);
84193
}
@@ -88,25 +197,33 @@ impl Solver {
88197
// ----------------------------------------------------
89198
if let Some(y0) = &initial_lagrange_multipliers {
90199
if y0.len() != {{meta.optimizer_name|upper}}_N1 {
91-
println!(
92-
"1700 -> wrong dimension of Langrange multipliers: {} != {}",
93-
y0.len(),
94-
{{meta.optimizer_name|upper}}_N1
95-
);
96-
return Ok(None);
200+
return Ok(SolverResponse {
201+
payload: SolverResponsePayload::Err(SolverErrorData {
202+
code: 1700,
203+
message: format!(
204+
"wrong dimension of Langrange multipliers: provided {}, expected {}",
205+
y0.len(),
206+
{{meta.optimizer_name|upper}}_N1
207+
),
208+
}),
209+
});
97210
}
98211
}
99212

100213
// ----------------------------------------------------
101214
// Check parameter
102215
// ----------------------------------------------------
103216
if p.len() != {{meta.optimizer_name|upper}}_NUM_PARAMETERS {
104-
println!(
105-
"3003 -> wrong number of parameters: {} != {}",
106-
p.len(),
107-
{{meta.optimizer_name|upper}}_NUM_PARAMETERS
108-
);
109-
return Ok(None);
217+
return Ok(SolverResponse {
218+
payload: SolverResponsePayload::Err(SolverErrorData {
219+
code: 3003,
220+
message: format!(
221+
"wrong number of parameters: provided {}, expected {}",
222+
p.len(),
223+
{{meta.optimizer_name|upper}}_NUM_PARAMETERS
224+
),
225+
}),
226+
});
110227
}
111228

112229
// ----------------------------------------------------
@@ -121,23 +238,15 @@ impl Solver {
121238
);
122239

123240
match solver_status {
124-
Ok(status) => Ok(Some(OptimizerSolution {
125-
exit_status: format!("{:?}", status.exit_status()),
126-
num_outer_iterations: status.num_outer_iterations(),
127-
num_inner_iterations: status.num_inner_iterations(),
128-
last_problem_norm_fpr: status.last_problem_norm_fpr(),
129-
f1_infeasibility: status.delta_y_norm_over_c(),
130-
f2_norm: status.f2_norm(),
131-
penalty: status.penalty(),
132-
lagrange_multipliers: status.lagrange_multipliers().clone().unwrap_or_default(),
133-
solve_time_ms: (status.solve_time().as_nanos() as f64) / 1e6,
134-
solution: u.to_vec(),
135-
cost: status.cost(),
136-
})),
137-
Err(_) => {
138-
println!("2000 -> Problem solution failed (solver error)");
139-
Ok(None)
140-
}
241+
Ok(status) => Ok(SolverResponse {
242+
payload: SolverResponsePayload::Ok(SolverStatusData::from_status(status, &u)),
243+
}),
244+
Err(err) => Ok(SolverResponse {
245+
payload: SolverResponsePayload::Err(SolverErrorData {
246+
code: 2000,
247+
message: format!("problem solution failed: {}", err),
248+
}),
249+
}),
141250
}
142251
}
143252
}

open-codegen/test/benchmark_open.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,16 @@ def t_benchmark1(solver):
3939
a = np.random.uniform(0.5, 2)
4040
b = np.random.uniform(0.5, 15)
4141
c = np.random.uniform(0.9, 3)
42-
_sol = solver.run([a, b, c])
42+
response = solver.run([a, b, c])
43+
_sol = response.get()
4344

4445

4546
def t_benchmark2(solver):
4647
x0 = np.random.uniform(-3.5, -2)
4748
y0 = np.random.uniform(-2.5, 2.5)
4849
# th0 = np.random.uniform(-0.3, 0.3)
49-
_sol = solver.run([x0, y0, 0])
50+
response = solver.run([x0, y0, 0])
51+
_sol = response.get()
5052

5153

5254
def test_benchmark1(benchmark):

0 commit comments

Comments
 (0)