Skip to content

Commit ef3dfd6

Browse files
authored
Merge pull request #407 from alphaville/feature/405-error-reporting-rust
OpEn: good error handling
2 parents 9981368 + bfa6624 commit ef3dfd6

30 files changed

Lines changed: 338 additions & 179 deletions

examples/panoc_ex1.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ fn main() {
2929
// define the cost function and its gradient
3030
let df = |u: &[f64], grad: &mut [f64]| -> Result<(), SolverError> {
3131
if a < 0.0 || b < 0.0 {
32-
Err(SolverError::Cost)
32+
Err(SolverError::Cost(
33+
"Rosenbrock parameters must be nonnegative",
34+
))
3335
} else {
3436
rosenbrock_grad(a, b, u, grad);
3537
Ok(())
@@ -38,7 +40,9 @@ fn main() {
3840

3941
let f = |u: &[f64], c: &mut f64| -> Result<(), SolverError> {
4042
if a < 0.0 || b < 0.0 {
41-
Err(SolverError::Cost)
43+
Err(SolverError::Cost(
44+
"Rosenbrock parameters must be nonnegative",
45+
))
4246
} else {
4347
*c = rosenbrock_cost(a, b, u);
4448
Ok(())

open-codegen/opengen/templates/c/optimizer_cinterface.rs.jinja

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -179,8 +179,11 @@ pub unsafe extern "C" fn {{meta.optimizer_name|lower}}_solve(
179179
},
180180
Err(e) => {{meta.optimizer_name}}SolverStatus {
181181
exit_status: match e {
182-
SolverError::Cost => {{meta.optimizer_name}}ExitStatus::{{meta.optimizer_name}}NotConvergedCost,
183-
SolverError::NotFiniteComputation => {{meta.optimizer_name}}ExitStatus::{{meta.optimizer_name}}NotConvergedNotFiniteComputation,
182+
SolverError::Cost(_)
183+
| SolverError::ProjectionFailed(_)
184+
| SolverError::LinearAlgebraFailure(_)
185+
| SolverError::InvalidProblemState(_) => {{meta.optimizer_name}}ExitStatus::{{meta.optimizer_name}}NotConvergedCost,
186+
SolverError::NotFiniteComputation(_) => {{meta.optimizer_name}}ExitStatus::{{meta.optimizer_name}}NotConvergedNotFiniteComputation,
184187
},
185188
num_outer_iterations: u64::MAX as c_ulong,
186189
num_inner_iterations: u64::MAX as c_ulong,
@@ -209,4 +212,4 @@ pub unsafe extern "C" fn {{meta.optimizer_name|lower}}_free(instance: *mut {{met
209212
assert!(!instance.is_null());
210213
drop(Box::from_raw(instance));
211214
}
212-
{% endif %}
215+
{% endif %}

open-codegen/opengen/templates/tcp/tcp_server.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ fn execution_handler(
210210
return_solution_to_client(ok_status, u, stream);
211211
}
212212
Err(err) => {
213-
let error_message = format!("problem solution failed: {:?}", err);
213+
let error_message = format!("problem solution failed: {}", err);
214214
write_error_message(stream, 2000, &error_message);
215215
}
216216
}

open-codegen/test/test.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def setUpSolverError(cls):
267267
anchor +
268268
'\n'
269269
' if p[0] < 0.0 {\n'
270-
' return Err(SolverError::Cost);\n'
270+
' return Err(SolverError::Cost("forced solver error for TCP test"));\n'
271271
' }\n'
272272
)
273273
if anchor not in solver_lib:
@@ -544,7 +544,9 @@ def test_rust_build_solver_error_details(self):
544544
self.assertFalse(response.is_ok())
545545
status = response.get()
546546
self.assertEqual(2000, status.code)
547-
self.assertEqual("problem solution failed: Cost", status.message)
547+
self.assertEqual(
548+
"problem solution failed: cost or gradient evaluation failed: forced solver error for TCP test",
549+
status.message)
548550

549551
def test_rust_build_parametric_f2(self):
550552
# introduced to tackle issue #123

src/alm/alm_factory.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ where
233233
.zip(y_lagrange_mult.iter())
234234
.for_each(|(ti, yi)| *ti += yi / f64::max(penalty_parameter, 1.0));
235235
s.copy_from_slice(&f1_u_plus_y_over_c);
236-
set_c.project(&mut s);
236+
set_c.project(&mut s)?;
237237
*cost += 0.5
238238
* penalty_parameter
239239
* matrix_operations::norm2_squared_diff(&f1_u_plus_y_over_c, &s);
@@ -296,7 +296,7 @@ where
296296
.zip(y_lagrange_mult.iter())
297297
.for_each(|(ti, yi)| *ti += yi / c_penalty_parameter);
298298
s_aux_var.copy_from_slice(&f1_u_plus_y_over_c); // s = t
299-
set_c.project(&mut s_aux_var); // s = Proj_C(F1(u) + y/c)
299+
set_c.project(&mut s_aux_var)?; // s = Proj_C(F1(u) + y/c)
300300

301301
// t = F1(u) + y/c - Proj_C(F1(u) + y/c)
302302
f1_u_plus_y_over_c
@@ -412,7 +412,9 @@ mod tests {
412412
let f2 = mapping_f2;
413413
let jac_f2_tr =
414414
|_u: &[f64], _d: &[f64], _res: &mut [f64]| -> Result<(), crate::SolverError> {
415-
Err(SolverError::NotFiniteComputation)
415+
Err(SolverError::NotFiniteComputation(
416+
"mock Jacobian-transpose product returned a non-finite result",
417+
))
416418
};
417419
let factory = AlmFactory::new(
418420
mocks::f0,

src/alm/alm_optimizer.rs

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -650,7 +650,7 @@ where
650650
.for_each(|((y_plus_i, y_i), w_alm_aux_i)| *y_plus_i = w_alm_aux_i + y_i / c);
651651

652652
// Step #3: y_plus := Proj_C(y_plus)
653-
alm_set_c.project(y_plus);
653+
alm_set_c.project(y_plus)?;
654654

655655
// Step #4
656656
y_plus
@@ -667,7 +667,7 @@ where
667667
}
668668

669669
/// Project y on set Y
670-
fn project_on_set_y(&mut self) {
670+
fn project_on_set_y(&mut self) -> FunctionCallResult {
671671
let problem = &self.alm_problem;
672672
if let Some(y_set) = &problem.alm_set_y {
673673
// NOTE: as_mut() converts from &mut Option<T> to Option<&mut T>
@@ -676,9 +676,10 @@ where
676676
// * which can be treated as Option<&mut [f64]>
677677
// * y_vec is &mut [f64]
678678
if let Some(xi_vec) = self.alm_cache.xi.as_mut() {
679-
y_set.project(&mut xi_vec[1..]);
679+
y_set.project(&mut xi_vec[1..])?;
680680
}
681681
}
682+
Ok(())
682683
}
683684

684685
/// Solve inner problem
@@ -740,7 +741,7 @@ where
740741
inner_solver.solve(u)
741742
}
742743

743-
fn is_exit_criterion_satisfied(&self) -> bool {
744+
fn is_exit_criterion_satisfied(&self) -> Result<bool, SolverError> {
744745
let cache = &self.alm_cache;
745746
let problem = &self.alm_problem;
746747
// Criterion 1: ||Delta y|| <= c * delta
@@ -764,8 +765,14 @@ where
764765
// This should never happen because we set the AKKT tolerance
765766
// in the constructor and can never become `None` again
766767
let criterion_3 =
767-
cache.panoc_cache.akkt_tolerance.unwrap() <= self.epsilon_tolerance + SMALL_EPSILON;
768-
criterion_1 && criterion_2 && criterion_3
768+
cache
769+
.panoc_cache
770+
.akkt_tolerance
771+
.ok_or(SolverError::InvalidProblemState(
772+
"missing inner AKKT tolerance while checking the exit criterion",
773+
))?
774+
<= self.epsilon_tolerance + SMALL_EPSILON;
775+
Ok(criterion_1 && criterion_2 && criterion_3)
769776
}
770777

771778
/// Whether the penalty parameter should not be updated
@@ -802,13 +809,21 @@ where
802809
}
803810
}
804811

805-
fn update_inner_akkt_tolerance(&mut self) {
812+
fn update_inner_akkt_tolerance(&mut self) -> FunctionCallResult {
806813
let cache = &mut self.alm_cache;
807814
// epsilon_{nu+1} := max(epsilon, beta*epsilon_nu)
815+
let akkt_tolerance =
816+
cache
817+
.panoc_cache
818+
.akkt_tolerance
819+
.ok_or(SolverError::InvalidProblemState(
820+
"missing inner AKKT tolerance while updating it",
821+
))?;
808822
cache.panoc_cache.set_akkt_tolerance(f64::max(
809-
cache.panoc_cache.akkt_tolerance.unwrap() * self.epsilon_update_factor,
823+
akkt_tolerance * self.epsilon_update_factor,
810824
self.epsilon_tolerance,
811825
));
826+
Ok(())
812827
}
813828

814829
fn final_cache_update(&mut self) {
@@ -843,7 +858,7 @@ where
843858
let mut inner_exit_status: ExitStatus = ExitStatus::Converged;
844859

845860
// Project y on Y
846-
self.project_on_set_y();
861+
self.project_on_set_y()?;
847862

848863
// If the inner problem fails miserably, the failure should be propagated
849864
// upstream (using `?`). If the inner problem has not converged, that is fine,
@@ -867,7 +882,7 @@ where
867882
self.compute_alm_infeasibility()?; // ALM: ||y_plus - y||
868883

869884
// Check exit criterion
870-
if self.is_exit_criterion_satisfied() {
885+
if self.is_exit_criterion_satisfied()? {
871886
// Do not continue the outer iteration
872887
// An (epsilon, delta)-AKKT point has been found
873888
return Ok(InnerProblemStatus::new(false, inner_exit_status));
@@ -876,7 +891,7 @@ where
876891
}
877892

878893
// Update inner problem tolerance
879-
self.update_inner_akkt_tolerance();
894+
self.update_inner_akkt_tolerance()?;
880895

881896
// conclusive step: updated iteration count, resets PANOC cache,
882897
// sets f2_norm = f2_norm_plus etc
@@ -979,12 +994,11 @@ where
979994
.with_penalty(c)
980995
.with_cost(cost);
981996
if self.alm_problem.n1 > 0 {
982-
let status = status.with_lagrange_multipliers(
983-
self.alm_cache
984-
.y_plus
985-
.as_ref()
986-
.expect("Although n1 > 0, there is no vector y (Lagrange multipliers)"),
987-
);
997+
let status = status.with_lagrange_multipliers(self.alm_cache.y_plus.as_ref().ok_or(
998+
SolverError::InvalidProblemState(
999+
"missing Lagrange multipliers at the ALM solution",
1000+
),
1001+
)?);
9881002
Ok(status)
9891003
} else {
9901004
Ok(status)
@@ -1129,7 +1143,7 @@ mod tests {
11291143
.with_initial_penalty(25.0)
11301144
.with_initial_lagrange_multipliers(&[2., 3., 4., 10.]);
11311145

1132-
alm_optimizer.project_on_set_y();
1146+
alm_optimizer.project_on_set_y().unwrap();
11331147
if let Some(xi_after_proj) = &alm_optimizer.alm_cache.xi {
11341148
println!("xi = {:#?}", xi_after_proj);
11351149
let y_projected_correct = [
@@ -1282,7 +1296,7 @@ mod tests {
12821296
.with_initial_inner_tolerance(1e-1)
12831297
.with_inner_tolerance_update_factor(0.2);
12841298

1285-
alm_optimizer.update_inner_akkt_tolerance();
1299+
alm_optimizer.update_inner_akkt_tolerance().unwrap();
12861300

12871301
unit_test_utils::assert_nearly_equal(
12881302
0.1,
@@ -1305,7 +1319,7 @@ mod tests {
13051319
);
13061320

13071321
for _i in 1..=5 {
1308-
alm_optimizer.update_inner_akkt_tolerance();
1322+
alm_optimizer.update_inner_akkt_tolerance().unwrap();
13091323
}
13101324
unit_test_utils::assert_nearly_equal(
13111325
2e-5,
@@ -1411,20 +1425,20 @@ mod tests {
14111425

14121426
// should not exit yet...
14131427
assert!(
1414-
!alm_optimizer.is_exit_criterion_satisfied(),
1428+
!alm_optimizer.is_exit_criterion_satisfied().unwrap(),
14151429
"exists right away"
14161430
);
14171431

14181432
let alm_optimizer = alm_optimizer
14191433
.with_initial_inner_tolerance(1e-3)
14201434
.with_epsilon_tolerance(1e-3);
1421-
assert!(!alm_optimizer.is_exit_criterion_satisfied());
1435+
assert!(!alm_optimizer.is_exit_criterion_satisfied().unwrap());
14221436

14231437
alm_optimizer.alm_cache.delta_y_norm_plus = 1e-3;
1424-
assert!(!alm_optimizer.is_exit_criterion_satisfied());
1438+
assert!(!alm_optimizer.is_exit_criterion_satisfied().unwrap());
14251439

14261440
alm_optimizer.alm_cache.f2_norm_plus = 1e-3;
1427-
assert!(alm_optimizer.is_exit_criterion_satisfied());
1441+
assert!(alm_optimizer.is_exit_criterion_satisfied().unwrap());
14281442
}
14291443

14301444
#[test]

src/constraints/affine_space.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use super::Constraint;
2-
use crate::matrix_operations;
3-
use crate::CholeskyFactorizer;
2+
use crate::{matrix_operations, CholeskyFactorizer, FunctionCallResult, SolverError};
43

54
use ndarray::{ArrayView1, ArrayView2};
65

@@ -82,26 +81,30 @@ impl Constraint for AffineSpace {
8281
/// ```
8382
///
8483
/// The result is stored in `x` and it can be verified that $Ax = b$.
85-
fn project(&self, x: &mut [f64]) {
84+
fn project(&self, x: &mut [f64]) -> FunctionCallResult {
8685
let n = self.n_cols;
8786
assert!(x.len() == n, "x has wrong dimension");
8887

8988
// Step 1: Compute e = Ax - b
90-
let a = ArrayView2::from_shape((self.n_rows, self.n_cols), &self.a_mat)
91-
.expect("invalid A shape");
89+
let a = ArrayView2::from_shape((self.n_rows, self.n_cols), &self.a_mat).map_err(|_| {
90+
SolverError::InvalidProblemState("failed to construct the affine-space matrix view")
91+
})?;
9292
let x_view = ArrayView1::from(&x[..]);
9393
let b = ArrayView1::from(&self.b_vec[..]);
9494
let e = a.dot(&x_view) - b;
95-
let e_slice: &[f64] = e.as_slice().unwrap();
95+
let e_slice: &[f64] = e.as_slice().ok_or(SolverError::InvalidProblemState(
96+
"affine-space residual vector is not stored contiguously",
97+
))?;
9698

9799
// Step 2: Solve AA' z = e and compute z
98-
let z = self.factorizer.solve(e_slice).unwrap();
100+
let z = self.factorizer.solve(e_slice)?;
99101

100102
// Step 3: Compute x = x - A'z
101103
let at_z = a.t().dot(&ArrayView1::from(&z[..]));
102104
for (xi, corr) in x.iter_mut().zip(at_z.iter()) {
103105
*xi -= *corr;
104106
}
107+
Ok(())
105108
}
106109

107110
/// Affine sets are convex.

src/constraints/ball1.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use super::Constraint;
22
use super::Simplex;
3+
use crate::FunctionCallResult;
34

45
#[derive(Copy, Clone)]
56
/// A norm-1 ball, that is, a set given by $B_1^r = \\{x \in \mathbb{R}^n {}:{} \Vert{}x{}\Vert_1 \leq r\\}$
@@ -23,24 +24,25 @@ impl<'a> Ball1<'a> {
2324
}
2425
}
2526

26-
fn project_on_ball1_centered_at_origin(&self, x: &mut [f64]) {
27+
fn project_on_ball1_centered_at_origin(&self, x: &mut [f64]) -> FunctionCallResult {
2728
if crate::matrix_operations::norm1(x) > self.radius {
2829
// u = |x| (copied)
2930
let mut u = vec![0.0; x.len()];
3031
u.iter_mut()
3132
.zip(x.iter())
3233
.for_each(|(ui, &xi)| *ui = f64::abs(xi));
3334
// u = P_simplex(u)
34-
self.simplex.project(&mut u);
35+
self.simplex.project(&mut u)?;
3536
x.iter_mut()
3637
.zip(u.iter())
3738
.for_each(|(xi, &ui)| *xi = f64::signum(*xi) * ui);
3839
}
40+
Ok(())
3941
}
4042
}
4143

4244
impl<'a> Constraint for Ball1<'a> {
43-
fn project(&self, x: &mut [f64]) {
45+
fn project(&self, x: &mut [f64]) -> FunctionCallResult {
4446
if let Some(center) = &self.center {
4547
assert_eq!(
4648
x.len(),
@@ -50,13 +52,14 @@ impl<'a> Constraint for Ball1<'a> {
5052
x.iter_mut()
5153
.zip(center.iter())
5254
.for_each(|(xi, &ci)| *xi -= ci);
53-
self.project_on_ball1_centered_at_origin(x);
55+
self.project_on_ball1_centered_at_origin(x)?;
5456
x.iter_mut()
5557
.zip(center.iter())
5658
.for_each(|(xi, &ci)| *xi += ci);
5759
} else {
58-
self.project_on_ball1_centered_at_origin(x);
60+
self.project_on_ball1_centered_at_origin(x)?;
5961
}
62+
Ok(())
6063
}
6164

6265
fn is_convex(&self) -> bool {

src/constraints/ball2.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::Constraint;
2+
use crate::FunctionCallResult;
23

34
#[derive(Copy, Clone)]
45
/// A Euclidean ball, that is, a set given by $B_2^r = \\{x \in \mathbb{R}^n {}:{} \Vert{}x{}\Vert \leq r\\}$
@@ -19,7 +20,7 @@ impl<'a> Ball2<'a> {
1920
}
2021

2122
impl<'a> Constraint for Ball2<'a> {
22-
fn project(&self, x: &mut [f64]) {
23+
fn project(&self, x: &mut [f64]) -> FunctionCallResult {
2324
if let Some(center) = &self.center {
2425
assert_eq!(
2526
x.len(),
@@ -46,6 +47,7 @@ impl<'a> Constraint for Ball2<'a> {
4647
x.iter_mut().for_each(|x_| *x_ /= norm_over_radius);
4748
}
4849
}
50+
Ok(())
4951
}
5052

5153
fn is_convex(&self) -> bool {

0 commit comments

Comments
 (0)