1use crate::problem::{ConstraintType, SolveStatus};
4use crate::sparse::CscMatrix;
5
6#[derive(Debug, Clone, Default)]
11pub struct QcqpMatrix {
12 pub n: usize,
14 pub triplets: Vec<(usize, usize, f64)>,
16}
17
18impl QcqpMatrix {
19 pub fn new(n: usize) -> Self {
21 Self { n, triplets: Vec::new() }
22 }
23
24 pub fn nnz(&self) -> usize {
26 self.triplets.len()
27 }
28}
29
30#[non_exhaustive]
31#[derive(Debug)]
32pub enum QpProblemError {
33 DimensionMismatch(String),
34 NonFiniteCoefficient { field: &'static str, index: usize },
36 InvalidBounds { index: usize, lb: f64, ub: f64 },
38 TripletIndexOutOfBounds { constraint: usize, row: usize, col: usize, n: usize },
40}
41
42impl std::fmt::Display for QpProblemError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 QpProblemError::DimensionMismatch(msg) => write!(f, "dimension mismatch: {}", msg),
46 QpProblemError::NonFiniteCoefficient { field, index } => {
47 write!(f, "non-finite coefficient in {}: index {}", field, index)
48 }
49 QpProblemError::InvalidBounds { index, lb, ub } => {
50 write!(f, "invalid bounds at index {}: lb={} > ub={} or NaN", index, lb, ub)
51 }
52 QpProblemError::TripletIndexOutOfBounds { constraint, row, col, n } => {
53 write!(f, "triplet index out of bounds in constraint {}: ({},{}) >= n={}", constraint, row, col, n)
54 }
55 }
56 }
57}
58
59impl std::error::Error for QpProblemError {}
60
61#[derive(Debug, Clone)]
68pub struct QpProblem {
69 pub q: CscMatrix,
70 pub c: Vec<f64>,
71 pub a: CscMatrix,
72 pub b: Vec<f64>,
73 pub bounds: Vec<(f64, f64)>,
74 pub num_vars: usize,
75 pub num_constraints: usize,
76 pub constraint_types: Vec<ConstraintType>,
77 pub quadratic_constraints: Vec<QcqpMatrix>,
82 pub obj_offset: f64,
84}
85
86impl QpProblem {
87 pub fn new(
88 q: CscMatrix,
89 c: Vec<f64>,
90 a: CscMatrix,
91 b: Vec<f64>,
92 bounds: Vec<(f64, f64)>,
93 constraint_types: Vec<ConstraintType>,
94 ) -> Result<Self, QpProblemError> {
95 let n = c.len();
96 let m = b.len();
97 if q.nrows != n || q.ncols != n {
98 return Err(QpProblemError::DimensionMismatch(
99 format!("Q must be {}x{}, got {}x{}", n, n, q.nrows, q.ncols)
100 ));
101 }
102 if a.nrows != m || a.ncols != n {
103 return Err(QpProblemError::DimensionMismatch(
104 format!("A must be {}x{}, got {}x{}", m, n, a.nrows, a.ncols)
105 ));
106 }
107 if bounds.len() != n {
108 return Err(QpProblemError::DimensionMismatch(
109 format!("bounds length must be {}, got {}", n, bounds.len())
110 ));
111 }
112 if constraint_types.len() != m {
113 return Err(QpProblemError::DimensionMismatch(
114 format!("constraint_types length must be {}, got {}", m, constraint_types.len())
115 ));
116 }
117 for (i, &v) in c.iter().enumerate() {
118 if !v.is_finite() {
119 return Err(QpProblemError::NonFiniteCoefficient { field: "c", index: i });
120 }
121 }
122 for (i, &v) in b.iter().enumerate() {
123 if !v.is_finite() {
124 return Err(QpProblemError::NonFiniteCoefficient { field: "b", index: i });
125 }
126 }
127 for (i, &v) in q.values.iter().enumerate() {
128 if !v.is_finite() {
129 return Err(QpProblemError::NonFiniteCoefficient { field: "Q", index: i });
130 }
131 }
132 for (i, &v) in a.values.iter().enumerate() {
133 if !v.is_finite() {
134 return Err(QpProblemError::NonFiniteCoefficient { field: "A", index: i });
135 }
136 }
137 for (i, &(lb, ub)) in bounds.iter().enumerate() {
138 if lb.is_nan() || ub.is_nan() || lb > ub {
139 return Err(QpProblemError::InvalidBounds { index: i, lb, ub });
140 }
141 }
142 Ok(QpProblem { q, c, a, b, bounds, num_vars: n, num_constraints: m, constraint_types, quadratic_constraints: vec![], obj_offset: 0.0 })
143 }
144
145 pub fn new_all_le(
147 q: CscMatrix,
148 c: Vec<f64>,
149 a: CscMatrix,
150 b: Vec<f64>,
151 bounds: Vec<(f64, f64)>,
152 ) -> Result<Self, QpProblemError> {
153 let m = b.len();
154 Self::new(q, c, a, b, bounds, vec![ConstraintType::Le; m])
155 }
156
157 pub fn is_zero_q(&self) -> bool {
159 self.q.values.iter().all(|&v| v.abs() < 1e-12)
160 }
161
162 pub fn has_qcqp_constraints(&self) -> bool {
166 self.quadratic_constraints.iter().any(|q| !q.triplets.is_empty())
167 }
168
169 pub fn set_quadratic_constraints(&mut self, qcs: Vec<QcqpMatrix>) -> Result<(), QpProblemError> {
175 if !qcs.is_empty() && qcs.len() != self.num_constraints {
176 return Err(QpProblemError::DimensionMismatch(format!(
177 "quadratic_constraints length must be 0 or {}, got {}",
178 self.num_constraints, qcs.len()
179 )));
180 }
181 for (k, qc) in qcs.iter().enumerate() {
182 if qc.n != self.num_vars {
183 return Err(QpProblemError::DimensionMismatch(format!(
184 "quadratic_constraints[{}].n must be {}, got {}",
185 k, self.num_vars, qc.n
186 )));
187 }
188 for &(row, col, v) in &qc.triplets {
189 if !v.is_finite() {
190 return Err(QpProblemError::NonFiniteCoefficient {
191 field: "quadratic_constraints",
192 index: k,
193 });
194 }
195 if row >= qc.n || col >= qc.n {
196 return Err(QpProblemError::TripletIndexOutOfBounds {
197 constraint: k,
198 row,
199 col,
200 n: qc.n,
201 });
202 }
203 }
204 }
205 self.quadratic_constraints = qcs;
206 Ok(())
207 }
208
209 pub fn is_diagonal_q(&self) -> bool {
211 for col in 0..self.num_vars {
212 let start = self.q.col_ptr[col];
213 let end = self.q.col_ptr[col + 1];
214 for k in start..end {
215 let row = self.q.row_ind[k];
216 if row != col && self.q.values[k].abs() > 1e-12 {
217 return false;
218 }
219 }
220 }
221 true
222 }
223
224}
225
226impl crate::problem::SolverResult {
227 pub fn infeasible() -> Self {
228 crate::problem::SolverResult {
229 status: SolveStatus::Infeasible,
230 objective: f64::INFINITY,
231 solution: vec![],
232 dual_solution: vec![],
233 bound_duals: vec![],
234
235 iterations: 0,
236 ..Default::default()
237 }
238 }
239
240 pub fn unbounded() -> Self {
241 crate::problem::SolverResult {
242 status: SolveStatus::Unbounded,
243 objective: f64::NEG_INFINITY,
244 solution: vec![],
245 dual_solution: vec![],
246 bound_duals: vec![],
247 iterations: 0,
248 ..Default::default()
249 }
250 }
251
252 pub fn max_iterations(x: Vec<f64>, obj: f64, iters: usize) -> Self {
253 crate::problem::SolverResult {
254 status: SolveStatus::MaxIterations,
255 objective: obj,
256 solution: x,
257 dual_solution: vec![],
258 bound_duals: vec![],
259 iterations: iters,
260 ..Default::default()
261 }
262 }
263
264 pub fn numerical_error() -> Self {
265 crate::problem::SolverResult {
266 status: SolveStatus::NumericalError,
267 objective: f64::INFINITY,
268 solution: vec![],
269 dual_solution: vec![],
270 bound_duals: vec![],
271
272 iterations: 0,
273 ..Default::default()
274 }
275 }
276
277 pub fn not_supported(msg: impl Into<String>) -> Self {
278 crate::problem::SolverResult {
279 status: SolveStatus::NotSupported(msg.into()),
280 objective: f64::INFINITY,
281 solution: vec![],
282 dual_solution: vec![],
283 bound_duals: vec![],
284 iterations: 0,
285 ..Default::default()
286 }
287 }
288}
289
290pub use crate::options::QpWarmStart;
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::problem::ConstraintType;
296 use crate::sparse::CscMatrix;
297
298 fn make_qp(
299 c: Vec<f64>,
300 b: Vec<f64>,
301 q_vals: Vec<f64>,
302 a_vals: Vec<f64>,
303 bounds: Vec<(f64, f64)>,
304 ) -> Result<QpProblem, QpProblemError> {
305 let n = c.len();
306 let m = b.len();
307 let q = if q_vals.is_empty() {
308 CscMatrix::new(n, n)
309 } else {
310 let idx: Vec<usize> = (0..n).collect();
311 crate::sparse::CscMatrix::from_triplets(&idx, &idx, &q_vals, n, n).unwrap()
312 };
313 let a = if a_vals.is_empty() {
314 CscMatrix::new(m, n)
315 } else {
316 let rows = vec![0usize; n];
317 let cols: Vec<usize> = (0..n).collect();
318 crate::sparse::CscMatrix::from_triplets(&rows, &cols, &a_vals, m, n).unwrap()
319 };
320 let ct = vec![ConstraintType::Le; m];
321 QpProblem::new(q, c, a, b, bounds, ct)
322 }
323
324 #[test]
325 fn valid_qp_accepted() {
326 let res = make_qp(
327 vec![1.0, 2.0],
328 vec![5.0],
329 vec![],
330 vec![1.0, 1.0],
331 vec![(0.0, f64::INFINITY), (0.0, 10.0)],
332 );
333 assert!(res.is_ok());
334 }
335
336 #[test]
338 fn nan_in_c_rejected() {
339 let cases = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
340 for bad in cases {
341 let res = make_qp(vec![bad, 1.0], vec![5.0], vec![], vec![1.0, 1.0],
342 vec![(0.0, f64::INFINITY); 2]);
343 assert!(
344 matches!(res, Err(QpProblemError::NonFiniteCoefficient { field: "c", .. })),
345 "expected NonFiniteCoefficient for c={bad}"
346 );
347 }
348 }
349
350 #[test]
352 fn nan_in_b_rejected() {
353 let cases = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
354 for bad in cases {
355 let res = make_qp(vec![1.0, 2.0], vec![bad], vec![], vec![1.0, 1.0],
356 vec![(0.0, f64::INFINITY); 2]);
357 assert!(
358 matches!(res, Err(QpProblemError::NonFiniteCoefficient { field: "b", .. })),
359 "expected NonFiniteCoefficient for b={bad}"
360 );
361 }
362 }
363
364 #[test]
368 fn nan_in_q_rejected() {
369 let n = 2;
370 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
371 for bad in bad_vals {
372 let mut q = CscMatrix::from_triplets(&[0], &[0], &[1.0], n, n).unwrap();
373 q.values[0] = bad; let a = CscMatrix::new(1, n);
375 let c = vec![1.0, 2.0];
376 let b = vec![5.0];
377 let bounds = vec![(0.0, f64::INFINITY); n];
378 let ct = vec![ConstraintType::Le];
379 let res = QpProblem::new(q, c, a, b, bounds, ct);
380 assert!(
381 matches!(res, Err(QpProblemError::NonFiniteCoefficient { field: "Q", .. })),
382 "expected NonFiniteCoefficient for Q val={bad}"
383 );
384 }
385 }
386
387 #[test]
389 fn nan_in_a_rejected() {
390 let n = 2;
391 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
392 for bad in bad_vals {
393 let q = CscMatrix::new(n, n);
394 let mut a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, n).unwrap();
395 a.values[0] = bad; let c = vec![1.0, 2.0];
397 let b = vec![5.0];
398 let bounds = vec![(0.0, f64::INFINITY); n];
399 let ct = vec![ConstraintType::Le];
400 let res = QpProblem::new(q, c, a, b, bounds, ct);
401 assert!(
402 matches!(res, Err(QpProblemError::NonFiniteCoefficient { field: "A", .. })),
403 "expected NonFiniteCoefficient for A val={bad}"
404 );
405 }
406 }
407
408 #[test]
410 fn nan_in_bounds_rejected() {
411 let cases: Vec<(f64, f64)> = vec![
412 (f64::NAN, 1.0),
413 (0.0, f64::NAN),
414 (f64::NAN, f64::NAN),
415 ];
416 for (lb, ub) in cases {
417 let res = make_qp(
418 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
419 vec![(lb, ub), (0.0, f64::INFINITY)],
420 );
421 assert!(
422 matches!(res, Err(QpProblemError::InvalidBounds { index: 0, .. })),
423 "expected InvalidBounds for ({lb},{ub})"
424 );
425 }
426 }
427
428 #[test]
430 fn lb_gt_ub_rejected() {
431 let cases: Vec<(f64, f64)> = vec![
432 (5.0, 1.0),
433 (1.0, 0.0),
434 (f64::INFINITY, f64::NEG_INFINITY),
435 (0.1, 0.0),
436 ];
437 for (lb, ub) in cases {
438 let res = make_qp(
439 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
440 vec![(lb, ub), (0.0, f64::INFINITY)],
441 );
442 assert!(
443 matches!(res, Err(QpProblemError::InvalidBounds { .. })),
444 "expected InvalidBounds for lb={lb} ub={ub}"
445 );
446 }
447 }
448
449 #[test]
451 fn inf_bounds_accepted() {
452 let res = make_qp(
453 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
454 vec![(f64::NEG_INFINITY, f64::INFINITY), (0.0, f64::INFINITY)],
455 );
456 assert!(res.is_ok(), "±inf bounds should be valid");
457 }
458
459 #[test]
461 fn dimension_mismatch_still_caught() {
462 let n = 2;
463 let q = CscMatrix::new(n, n);
464 let a = CscMatrix::new(1, n);
465 let c = vec![1.0, 2.0, 3.0]; let b = vec![5.0];
467 let bounds = vec![(0.0, f64::INFINITY); n];
468 let ct = vec![ConstraintType::Le];
469 let res = QpProblem::new(q, c, a, b, bounds, ct);
470 assert!(matches!(res, Err(QpProblemError::DimensionMismatch(_))));
471 }
472
473 #[test]
476 fn set_quadratic_constraints_length_mismatch_rejected() {
477 let mut prob = make_qp(
478 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
479 vec![(0.0, f64::INFINITY); 2],
480 ).unwrap();
481 let qcs = vec![QcqpMatrix::new(2), QcqpMatrix::new(2)];
483 let res = prob.set_quadratic_constraints(qcs);
484 assert!(matches!(res, Err(QpProblemError::DimensionMismatch(_))));
485 }
486
487 #[test]
488 fn set_quadratic_constraints_nan_rejected() {
489 let mut prob = make_qp(
490 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
491 vec![(0.0, f64::INFINITY); 2],
492 ).unwrap();
493 let bad_vals = [f64::NAN, f64::INFINITY, f64::NEG_INFINITY];
494 for bad in bad_vals {
495 let mut qc = QcqpMatrix::new(2);
496 qc.triplets.push((0, 0, bad));
497 let res = prob.set_quadratic_constraints(vec![qc]);
498 assert!(
499 matches!(res, Err(QpProblemError::NonFiniteCoefficient { field: "quadratic_constraints", .. })),
500 "expected NonFiniteCoefficient for val={bad}"
501 );
502 }
503 }
504
505 #[test]
506 fn set_quadratic_constraints_empty_accepted() {
507 let mut prob = make_qp(
508 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
509 vec![(0.0, f64::INFINITY); 2],
510 ).unwrap();
511 assert!(prob.set_quadratic_constraints(vec![]).is_ok());
512 }
513
514 #[test]
515 fn set_quadratic_constraints_valid_accepted() {
516 let mut prob = make_qp(
517 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
518 vec![(0.0, f64::INFINITY); 2],
519 ).unwrap();
520 let mut qc = QcqpMatrix::new(2);
521 qc.triplets.push((0, 0, 2.0));
522 qc.triplets.push((1, 1, 3.0));
523 assert!(prob.set_quadratic_constraints(vec![qc]).is_ok());
524 }
525
526 #[test]
528 fn set_quadratic_constraints_n_mismatch_rejected() {
529 let mut prob = make_qp(
530 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
531 vec![(0.0, f64::INFINITY); 2],
532 ).unwrap();
533 let mut qc = QcqpMatrix::new(3);
535 qc.triplets.push((0, 0, 1.0));
536 let res = prob.set_quadratic_constraints(vec![qc]);
537 assert!(
538 matches!(res, Err(QpProblemError::DimensionMismatch(_))),
539 "n mismatch must be rejected"
540 );
541 }
542
543 #[test]
545 fn set_quadratic_constraints_triplet_row_oob_rejected() {
546 let mut prob = make_qp(
547 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
548 vec![(0.0, f64::INFINITY); 2],
549 ).unwrap();
550 let mut qc = QcqpMatrix::new(2);
551 qc.triplets.push((2, 0, 1.0)); let res = prob.set_quadratic_constraints(vec![qc]);
553 assert!(
554 matches!(res, Err(QpProblemError::TripletIndexOutOfBounds { row: 2, col: 0, n: 2, .. })),
555 "row OOB must be rejected"
556 );
557 }
558
559 #[test]
561 fn set_quadratic_constraints_triplet_col_oob_rejected() {
562 let mut prob = make_qp(
563 vec![1.0, 2.0], vec![5.0], vec![], vec![1.0, 1.0],
564 vec![(0.0, f64::INFINITY); 2],
565 ).unwrap();
566 let mut qc = QcqpMatrix::new(2);
567 qc.triplets.push((0, 5, 1.0)); let res = prob.set_quadratic_constraints(vec![qc]);
569 assert!(
570 matches!(res, Err(QpProblemError::TripletIndexOutOfBounds { col: 5, n: 2, .. })),
571 "col OOB must be rejected"
572 );
573 }
574
575 #[test]
577 fn csc_accessor_returns_correct_data() {
578 let rows = vec![0usize, 1];
579 let cols = vec![0usize, 1];
580 let vals = vec![3.0f64, 7.0];
581 let m = CscMatrix::from_triplets(&rows, &cols, &vals, 2, 2).unwrap();
582 assert_eq!(m.nrows(), 2);
583 assert_eq!(m.ncols(), 2);
584 assert_eq!(m.nnz(), 2);
585 assert_eq!(m.col_ptr().len(), 3);
586 assert_eq!(m.row_ind().len(), 2);
587 assert_eq!(m.values().len(), 2);
588 let (ri, vs) = m.get_column(0).unwrap();
589 assert_eq!(ri, &[0]);
590 assert_eq!(vs, &[3.0]);
591 let (ri, vs) = m.get_column(1).unwrap();
592 assert_eq!(ri, &[1]);
593 assert_eq!(vs, &[7.0]);
594 }
595}