Skip to content

Commit b2fe756

Browse files
committed
merge dev/release and fix conflicts
2 parents 99de93e + ef3dfd6 commit b2fe756

30 files changed

Lines changed: 346 additions & 188 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
@@ -285,7 +285,7 @@ where
285285
.zip(y_lagrange_mult.iter())
286286
.for_each(|(ti, yi)| *ti += *yi / penalty_scale);
287287
s.copy_from_slice(&f1_u_plus_y_over_c);
288-
set_c.project(&mut s);
288+
set_c.project(&mut s)?;
289289
let dist_sq: T = matrix_operations::norm2_squared_diff(&f1_u_plus_y_over_c, &s);
290290
let scaling: T = cast::<T>(0.5) * penalty_parameter;
291291
*cost += scaling * dist_sq;
@@ -403,7 +403,7 @@ where
403403
.zip(y_lagrange_mult.iter())
404404
.for_each(|(ti, yi)| *ti += *yi / c_penalty_parameter);
405405
s_aux_var.copy_from_slice(&f1_u_plus_y_over_c); // s = t
406-
set_c.project(&mut s_aux_var); // s = Proj_C(F1(u) + y/c)
406+
set_c.project(&mut s_aux_var)?; // s = Proj_C(F1(u) + y/c)
407407

408408
// t = F1(u) + y/c - Proj_C(F1(u) + y/c)
409409
f1_u_plus_y_over_c
@@ -519,7 +519,9 @@ mod tests {
519519
let f2 = mapping_f2;
520520
let jac_f2_tr =
521521
|_u: &[f64], _d: &[f64], _res: &mut [f64]| -> Result<(), crate::SolverError> {
522-
Err(SolverError::NotFiniteComputation)
522+
Err(SolverError::NotFiniteComputation(
523+
"mock Jacobian-transpose product returned a non-finite result",
524+
))
523525
};
524526
let factory = AlmFactory::new(
525527
mocks::f0,

src/alm/alm_optimizer.rs

Lines changed: 43 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ where
674674
.for_each(|((y_plus_i, y_i), w_alm_aux_i)| *y_plus_i = *w_alm_aux_i + *y_i / c);
675675

676676
// Step #3: y_plus := Proj_C(y_plus)
677-
alm_set_c.project(y_plus);
677+
alm_set_c.project(y_plus)?;
678678

679679
// Step #4
680680
y_plus
@@ -691,7 +691,7 @@ where
691691
}
692692

693693
/// Project y on set Y
694-
fn project_on_set_y(&mut self) {
694+
fn project_on_set_y(&mut self) -> FunctionCallResult {
695695
let problem = &self.alm_problem;
696696
if let Some(y_set) = &problem.alm_set_y {
697697
// NOTE: as_mut() converts from &mut Option<T> to Option<&mut T>
@@ -700,9 +700,10 @@ where
700700
// * which can be treated as Option<&mut [T]>
701701
// * y_vec is &mut [T]
702702
if let Some(xi_vec) = self.alm_cache.xi.as_mut() {
703-
y_set.project(&mut xi_vec[1..]);
703+
y_set.project(&mut xi_vec[1..])?;
704704
}
705705
}
706+
Ok(())
706707
}
707708

708709
/// Solve inner problem
@@ -764,7 +765,7 @@ where
764765
inner_solver.solve(u)
765766
}
766767

767-
fn is_exit_criterion_satisfied(&self) -> bool {
768+
fn is_exit_criterion_satisfied(&self) -> Result<bool, SolverError> {
768769
let cache = &self.alm_cache;
769770
let problem = &self.alm_problem;
770771
// Criterion 1: ||Delta y|| <= c * delta
@@ -787,9 +788,14 @@ where
787788
// This function will panic is there is no akkt_tolerance
788789
// This should never happen because we set the AKKT tolerance
789790
// in the constructor and can never become `None` again
790-
let criterion_3 =
791-
cache.panoc_cache.akkt_tolerance.unwrap() <= self.epsilon_tolerance + T::epsilon();
792-
criterion_1 && criterion_2 && criterion_3
791+
let criterion_3 = cache
792+
.panoc_cache
793+
.akkt_tolerance
794+
.ok_or(SolverError::InvalidProblemState(
795+
"missing inner AKKT tolerance while checking the exit criterion",
796+
))?
797+
<= self.epsilon_tolerance + T::epsilon();
798+
Ok(criterion_1 && criterion_2 && criterion_3)
793799
}
794800

795801
/// Whether the penalty parameter should not be updated
@@ -826,17 +832,23 @@ where
826832
}
827833
}
828834

829-
fn update_inner_akkt_tolerance(&mut self) {
835+
fn update_inner_akkt_tolerance(&mut self) -> FunctionCallResult {
830836
let cache = &mut self.alm_cache;
831837
// epsilon_{nu+1} := max(epsilon, beta*epsilon_nu)
832-
let next_tolerance = cache.panoc_cache.akkt_tolerance.unwrap() * self.epsilon_update_factor;
833-
cache
834-
.panoc_cache
835-
.set_akkt_tolerance(if next_tolerance > self.epsilon_tolerance {
836-
next_tolerance
837-
} else {
838-
self.epsilon_tolerance
839-
});
838+
let akkt_tolerance =
839+
cache
840+
.panoc_cache
841+
.akkt_tolerance
842+
.ok_or(SolverError::InvalidProblemState(
843+
"missing inner AKKT tolerance while updating it",
844+
))?;
845+
let next_tolerance = akkt_tolerance * self.epsilon_update_factor;
846+
cache.panoc_cache.set_akkt_tolerance(if next_tolerance > self.epsilon_tolerance {
847+
next_tolerance
848+
} else {
849+
self.epsilon_tolerance
850+
});
851+
Ok(())
840852
}
841853

842854
fn final_cache_update(&mut self) {
@@ -871,7 +883,7 @@ where
871883
let mut inner_exit_status: ExitStatus = ExitStatus::Converged;
872884

873885
// Project y on Y
874-
self.project_on_set_y();
886+
self.project_on_set_y()?;
875887

876888
// If the inner problem fails miserably, the failure should be propagated
877889
// upstream (using `?`). If the inner problem has not converged, that is fine,
@@ -895,7 +907,7 @@ where
895907
self.compute_alm_infeasibility()?; // ALM: ||y_plus - y||
896908

897909
// Check exit criterion
898-
if self.is_exit_criterion_satisfied() {
910+
if self.is_exit_criterion_satisfied()? {
899911
// Do not continue the outer iteration
900912
// An (epsilon, delta)-AKKT point has been found
901913
return Ok(InnerProblemStatus::new(false, inner_exit_status));
@@ -904,7 +916,7 @@ where
904916
}
905917

906918
// Update inner problem tolerance
907-
self.update_inner_akkt_tolerance();
919+
self.update_inner_akkt_tolerance()?;
908920

909921
// conclusive step: updated iteration count, resets PANOC cache,
910922
// sets f2_norm = f2_norm_plus etc
@@ -1010,12 +1022,11 @@ where
10101022
.with_penalty(c)
10111023
.with_cost(cost);
10121024
if self.alm_problem.n1 > 0 {
1013-
let status = status.with_lagrange_multipliers(
1014-
self.alm_cache
1015-
.y_plus
1016-
.as_ref()
1017-
.expect("Although n1 > 0, there is no vector y (Lagrange multipliers)"),
1018-
);
1025+
let status = status.with_lagrange_multipliers(self.alm_cache.y_plus.as_ref().ok_or(
1026+
SolverError::InvalidProblemState(
1027+
"missing Lagrange multipliers at the ALM solution",
1028+
),
1029+
)?);
10191030
Ok(status)
10201031
} else {
10211032
Ok(status)
@@ -1163,7 +1174,7 @@ mod tests {
11631174
.with_initial_penalty(25.0)
11641175
.with_initial_lagrange_multipliers(&[2., 3., 4., 10.]);
11651176

1166-
alm_optimizer.project_on_set_y();
1177+
alm_optimizer.project_on_set_y().unwrap();
11671178
if let Some(xi_after_proj) = &alm_optimizer.alm_cache.xi {
11681179
println!("xi = {:#?}", xi_after_proj);
11691180
let y_projected_correct = [
@@ -1316,7 +1327,7 @@ mod tests {
13161327
.with_initial_inner_tolerance(1e-1)
13171328
.with_inner_tolerance_update_factor(0.2);
13181329

1319-
alm_optimizer.update_inner_akkt_tolerance();
1330+
alm_optimizer.update_inner_akkt_tolerance().unwrap();
13201331

13211332
unit_test_utils::assert_nearly_equal(
13221333
0.1,
@@ -1339,7 +1350,7 @@ mod tests {
13391350
);
13401351

13411352
for _i in 1..=5 {
1342-
alm_optimizer.update_inner_akkt_tolerance();
1353+
alm_optimizer.update_inner_akkt_tolerance().unwrap();
13431354
}
13441355
unit_test_utils::assert_nearly_equal(
13451356
2e-5,
@@ -1445,20 +1456,20 @@ mod tests {
14451456

14461457
// should not exit yet...
14471458
assert!(
1448-
!alm_optimizer.is_exit_criterion_satisfied(),
1459+
!alm_optimizer.is_exit_criterion_satisfied().unwrap(),
14491460
"exists right away"
14501461
);
14511462

14521463
let alm_optimizer = alm_optimizer
14531464
.with_initial_inner_tolerance(1e-3)
14541465
.with_epsilon_tolerance(1e-3);
1455-
assert!(!alm_optimizer.is_exit_criterion_satisfied());
1466+
assert!(!alm_optimizer.is_exit_criterion_satisfied().unwrap());
14561467

14571468
alm_optimizer.alm_cache.delta_y_norm_plus = 1e-3;
1458-
assert!(!alm_optimizer.is_exit_criterion_satisfied());
1469+
assert!(!alm_optimizer.is_exit_criterion_satisfied().unwrap());
14591470

14601471
alm_optimizer.alm_cache.f2_norm_plus = 1e-3;
1461-
assert!(alm_optimizer.is_exit_criterion_satisfied());
1472+
assert!(alm_optimizer.is_exit_criterion_satisfied().unwrap());
14621473
}
14631474

14641475
#[test]

src/constraints/affine_space.rs

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

56
use ndarray::{ArrayView1, ArrayView2, LinalgScalar};
67
use num::Float;
@@ -148,26 +149,30 @@ where
148149
/// ```
149150
///
150151
/// The result is stored in `x` and it can be verified that $Ax = b$.
151-
fn project(&self, x: &mut [T]) {
152+
fn project(&self, x: &mut [T]) -> FunctionCallResult {
152153
let n = self.n_cols;
153154
assert!(x.len() == n, "x has wrong dimension");
154155

155156
// Step 1: Compute e = Ax - b
156-
let a = ArrayView2::from_shape((self.n_rows, self.n_cols), &self.a_mat)
157-
.expect("invalid A shape");
157+
let a = ArrayView2::from_shape((self.n_rows, self.n_cols), &self.a_mat).map_err(|_| {
158+
SolverError::InvalidProblemState("failed to construct the affine-space matrix view")
159+
})?;
158160
let x_view = ArrayView1::from(&x[..]);
159161
let b = ArrayView1::from(&self.b_vec[..]);
160162
let e = a.dot(&x_view) - b;
161-
let e_slice: &[T] = e.as_slice().unwrap();
163+
let e_slice: &[T] = e.as_slice().ok_or(SolverError::InvalidProblemState(
164+
"affine-space residual vector is not stored contiguously",
165+
))?;
162166

163167
// Step 2: Solve AA' z = e and compute z
164-
let z = self.factorizer.solve(e_slice).unwrap();
168+
let z = self.factorizer.solve(e_slice)?;
165169

166170
// Step 3: Compute x = x - A'z
167171
let at_z = a.t().dot(&ArrayView1::from(&z[..]));
168172
for (xi, corr) in x.iter_mut().zip(at_z.iter()) {
169173
*xi = *xi - *corr;
170174
}
175+
Ok(())
171176
}
172177

173178
/// Affine sets are convex.

src/constraints/ball1.rs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::Constraint;
22
use super::Simplex;
33
use num::Float;
44
use std::iter::Sum;
5+
use crate::FunctionCallResult;
56

67
#[derive(Copy, Clone)]
78
/// A norm-1 ball, that is, a set given by $B_1^r = \\{x \in \mathbb{R}^n {}:{} \Vert{}x{}\Vert_1 \leq r\\}$
@@ -35,7 +36,7 @@ impl<'a, T: Float> Ball1<'a, T> {
3536
}
3637
}
3738

38-
fn project_on_ball1_centered_at_origin(&self, x: &mut [T])
39+
fn project_on_ball1_centered_at_origin(&self, x: &mut [T]) -> FunctionCallResult
3940
where
4041
T: Sum<T>,
4142
{
@@ -46,19 +47,20 @@ impl<'a, T: Float> Ball1<'a, T> {
4647
.zip(x.iter())
4748
.for_each(|(ui, &xi)| *ui = xi.abs());
4849
// u = P_simplex(u)
49-
self.simplex.project(&mut u);
50+
self.simplex.project(&mut u)?;
5051
x.iter_mut()
5152
.zip(u.iter())
5253
.for_each(|(xi, &ui)| *xi = xi.signum() * ui);
5354
}
55+
Ok(())
5456
}
5557
}
5658

5759
impl<'a, T> Constraint<T> for Ball1<'a, T>
5860
where
5961
T: Float + Sum<T>,
6062
{
61-
fn project(&self, x: &mut [T]) {
63+
fn project(&self, x: &mut [T]) -> FunctionCallResult {
6264
if let Some(center) = &self.center {
6365
assert_eq!(
6466
x.len(),
@@ -68,13 +70,14 @@ where
6870
x.iter_mut()
6971
.zip(center.iter())
7072
.for_each(|(xi, &ci)| *xi = *xi - ci);
71-
self.project_on_ball1_centered_at_origin(x);
73+
self.project_on_ball1_centered_at_origin(x)?;
7274
x.iter_mut()
7375
.zip(center.iter())
7476
.for_each(|(xi, &ci)| *xi = *xi + ci);
7577
} else {
76-
self.project_on_ball1_centered_at_origin(x);
78+
self.project_on_ball1_centered_at_origin(x)?;
7779
}
80+
Ok(())
7881
}
7982

8083
fn is_convex(&self) -> bool {

0 commit comments

Comments
 (0)