Skip to main content

otspot_core/mip/
problem.rs

1//! MILP / MIQP problem definitions.
2//!
3//! A MILP/MIQP is a continuous LP/QP **relaxation** plus a set of variables
4//! constrained to integer values. We deliberately wrap the existing
5//! [`LpProblem`] / [`QpProblem`] rather than adding an integrality field to them:
6//! the continuous solvers stay untouched (zero regression surface), and each
7//! branch-and-bound node solves the relaxation by swapping `bounds` — the same
8//! mechanism the spatial QP B&B already uses for box subproblems.
9
10use super::Relaxation;
11use crate::linalg::ldl::is_q_psd_by_cholesky;
12use crate::options::SolverOptions;
13use crate::problem::{ConstraintType, LpProblem, SolveStatus, SolverResult};
14use crate::qp::QpProblem;
15use crate::tolerances::{FIXED_POINT_FEAS_TOL, INT_ROUND_TOL};
16
17/// Construction error for [`MilpProblem`] / [`MiqpProblem`].
18#[non_exhaustive]
19#[derive(Debug, PartialEq, Eq)]
20pub enum MipProblemError {
21    /// An integer-variable index is out of range for the relaxation.
22    InvalidIntegerVar { index: usize, num_vars: usize },
23}
24
25impl std::fmt::Display for MipProblemError {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            MipProblemError::InvalidIntegerVar { index, num_vars } => write!(
29                f,
30                "integer variable index {} out of range (num_vars = {})",
31                index, num_vars
32            ),
33        }
34    }
35}
36
37impl std::error::Error for MipProblemError {}
38
39/// Sort, de-duplicate and range-check the integer-variable index set.
40fn normalize_integer_vars(
41    mut integer_vars: Vec<usize>,
42    num_vars: usize,
43) -> Result<Vec<usize>, MipProblemError> {
44    integer_vars.sort_unstable();
45    integer_vars.dedup();
46    if let Some(&j) = integer_vars.last() {
47        if j >= num_vars {
48            return Err(MipProblemError::InvalidIntegerVar { index: j, num_vars });
49        }
50    }
51    Ok(integer_vars)
52}
53
54/// Mixed-Integer Linear Program: minimize `c^T x` over the [`LpProblem`]
55/// feasible region with `x[j]` integral for every `j` in `integer_vars`.
56#[derive(Debug, Clone)]
57pub struct MilpProblem {
58    /// Continuous LP relaxation (objective, constraints, bounds).
59    pub lp: LpProblem,
60    /// Sorted, de-duplicated indices of variables required to be integral.
61    pub integer_vars: Vec<usize>,
62}
63
64impl MilpProblem {
65    /// Build a MILP from an LP relaxation and the integer-variable indices.
66    ///
67    /// `integer_vars` is sorted and de-duplicated. An empty set is permitted and
68    /// means the problem is a plain LP (the solver falls back to the LP path).
69    pub fn new(lp: LpProblem, integer_vars: Vec<usize>) -> Result<Self, MipProblemError> {
70        let integer_vars = normalize_integer_vars(integer_vars, lp.num_vars)?;
71        Ok(Self { lp, integer_vars })
72    }
73
74    /// Number of decision variables.
75    pub fn num_vars(&self) -> usize {
76        self.lp.num_vars
77    }
78}
79
80impl Relaxation for MilpProblem {
81    fn num_vars(&self) -> usize {
82        self.lp.num_vars
83    }
84    fn root_bounds(&self) -> &[(f64, f64)] {
85        &self.lp.bounds
86    }
87    fn integer_vars(&self) -> &[usize] {
88        &self.integer_vars
89    }
90    fn solve(&self, bounds: &[(f64, f64)], opts: &SolverOptions) -> SolverResult {
91        let mut sub = self.lp.clone();
92        sub.bounds = bounds.to_vec();
93        crate::lp::solve_lp_with(&sub, opts)
94    }
95    fn skip_node_presolve(&self) -> bool {
96        true
97    }
98}
99
100/// Mixed-Integer **convex** Quadratic Program: minimize `1/2 x^T Q x + c^T x`
101/// over the [`QpProblem`] feasible region with `x[j]` integral for every `j` in
102/// `integer_vars`. Only convex (`Q` PSD) instances are in scope — see
103/// [`MiqpProblem::is_convex`].
104#[derive(Debug, Clone)]
105pub struct MiqpProblem {
106    /// Continuous QP relaxation (objective, constraints, bounds).
107    pub qp: QpProblem,
108    /// Sorted, de-duplicated indices of variables required to be integral.
109    pub integer_vars: Vec<usize>,
110}
111
112impl MiqpProblem {
113    /// Build an MIQP from a QP relaxation and the integer-variable indices.
114    ///
115    /// Convexity is **not** enforced here; [`solve_miqp`] calls [`MiqpProblem::is_convex`]
116    /// and rejects non-PSD `Q`. `integer_vars` is sorted and de-duplicated; an empty
117    /// set means a plain QP (the solver falls back to the QP path).
118    ///
119    /// [`solve_miqp`]: crate::solve_miqp
120    pub fn new(qp: QpProblem, integer_vars: Vec<usize>) -> Result<Self, MipProblemError> {
121        let integer_vars = normalize_integer_vars(integer_vars, qp.num_vars)?;
122        Ok(Self { qp, integer_vars })
123    }
124
125    /// Number of decision variables.
126    pub fn num_vars(&self) -> usize {
127        self.qp.num_vars
128    }
129
130    /// Whether the objective is convex (`Q` positive semidefinite). The QP
131    /// relaxation is a valid lower bound only when this holds; a non-convex MIQP
132    /// is out of scope.
133    pub fn is_convex(&self) -> bool {
134        is_q_psd_by_cholesky(&self.qp.q)
135    }
136}
137
138impl Relaxation for MiqpProblem {
139    fn num_vars(&self) -> usize {
140        self.qp.num_vars
141    }
142    fn root_bounds(&self) -> &[(f64, f64)] {
143        &self.qp.bounds
144    }
145    fn integer_vars(&self) -> &[usize] {
146        &self.integer_vars
147    }
148    fn solve(&self, bounds: &[(f64, f64)], opts: &SolverOptions) -> SolverResult {
149        // When integer branching fixes every variable to a point, the QP interior
150        // point method has no interior and fails (NumericalError). Evaluate the
151        // single candidate directly instead.
152        if let Some(r) = solve_fixed_point(&self.qp, bounds) {
153            return r;
154        }
155        let mut sub = self.qp.clone();
156        sub.bounds = bounds.to_vec();
157        crate::qp::solve_qp_with(&sub, opts)
158    }
159}
160
161/// Solve the relaxation when **every** variable is fixed to a point (zero-width
162/// box). The QP IPM cannot (no interior), so evaluate the single candidate `x`
163/// directly: check linear-constraint feasibility, then return its exact objective
164/// `1/2 x'Qx + c'x + offset`. Returns `None` when any variable is still free
165/// (the IPM handles those, including partially-fixed boxes).
166fn solve_fixed_point(qp: &QpProblem, bounds: &[(f64, f64)]) -> Option<SolverResult> {
167    // INT_ROUND_TOL: a fixed variable has width 0; this small tolerance guards float round-off.
168    if !bounds.iter().all(|&(l, u)| u - l <= INT_ROUND_TOL) {
169        return None;
170    }
171    // Empty box (lower meaningfully above upper) → infeasible subproblem.
172    if bounds.iter().any(|&(l, u)| l - u > INT_ROUND_TOL) {
173        return Some(SolverResult::infeasible());
174    }
175    let x: Vec<f64> = bounds.iter().map(|&(l, u)| 0.5 * (l + u)).collect();
176
177    if qp.num_constraints > 0 {
178        let lhs = match qp.a.mat_vec_mul(&x) {
179            Ok(v) => v,
180            Err(_) => return Some(SolverResult::numerical_error()),
181        };
182        for ((&lhs_k, &ct), &b_k) in lhs.iter().zip(&qp.constraint_types).zip(&qp.b) {
183            let feasible = match ct {
184                ConstraintType::Le => lhs_k <= b_k + FIXED_POINT_FEAS_TOL,
185                ConstraintType::Ge => lhs_k >= b_k - FIXED_POINT_FEAS_TOL,
186                ConstraintType::Eq => (lhs_k - b_k).abs() <= FIXED_POINT_FEAS_TOL,
187            };
188            if !feasible {
189                return Some(SolverResult::infeasible());
190            }
191        }
192    }
193
194    // Objective 1/2 x'Qx + c'x + offset (Q is full-symmetric CSC storage).
195    let qx = match qp.q.mat_vec_mul(&x) {
196        Ok(v) => v,
197        Err(_) => return Some(SolverResult::numerical_error()),
198    };
199    let quad: f64 = 0.5 * x.iter().zip(&qx).map(|(xi, qxi)| xi * qxi).sum::<f64>();
200    let lin: f64 = qp.c.iter().zip(&x).map(|(ci, xi)| ci * xi).sum::<f64>();
201    Some(SolverResult {
202        status: SolveStatus::Optimal,
203        objective: quad + lin + qp.obj_offset,
204        solution: x,
205        ..Default::default()
206    })
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use crate::problem::ConstraintType;
213    use crate::sparse::CscMatrix;
214
215    fn lp_2var() -> LpProblem {
216        // trivial 2-var LP, bounds [0,5]^2, one <= constraint
217        let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
218        LpProblem::new_general(
219            vec![1.0, 1.0],
220            a,
221            vec![5.0],
222            vec![ConstraintType::Le],
223            vec![(0.0, 5.0), (0.0, 5.0)],
224            None,
225        )
226        .unwrap()
227    }
228
229    fn qp_diag(diag: &[f64]) -> QpProblem {
230        let n = diag.len();
231        let idx: Vec<usize> = (0..n).collect();
232        let q = CscMatrix::from_triplets(&idx, &idx, diag, n, n).unwrap();
233        let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
234        QpProblem::new_all_le(q, vec![0.0; n], a, vec![], vec![(0.0, 5.0); n]).unwrap()
235    }
236
237    /// `skip_node_presolve` must return `true` unconditionally for all `MilpProblem` instances.
238    ///
239    /// Per-node presolve is disabled to preserve warm-start basis propagation.
240    /// Sentinel: reintroducing the Le-only condition (Bug 2) returns `false` for Ge
241    /// problems, re-enabling per-node presolve, which regresses mas76 nocuts from a
242    /// valid incumbent (~67932) to garbage (~1e12).
243    #[test]
244    fn skip_node_presolve_unconditional() {
245        let a_le = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
246        let lp_le = LpProblem::new_general(
247            vec![1.0, 1.0],
248            a_le,
249            vec![5.0],
250            vec![ConstraintType::Le],
251            vec![(0.0, 5.0), (0.0, 5.0)],
252            None,
253        )
254        .unwrap();
255        let milp_le = MilpProblem::new(lp_le, vec![0]).unwrap();
256        assert!(milp_le.skip_node_presolve(), "Le: skip must be true");
257
258        let a_ge = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
259        let lp_ge = LpProblem::new_general(
260            vec![1.0, 1.0],
261            a_ge,
262            vec![1.0],
263            vec![ConstraintType::Ge],
264            vec![(0.0, 5.0), (0.0, 5.0)],
265            None,
266        )
267        .unwrap();
268        let milp_ge = MilpProblem::new(lp_ge, vec![0]).unwrap();
269        assert!(
270            milp_ge.skip_node_presolve(),
271            "Ge: skip must also be true (Bug 2 regression guard)"
272        );
273    }
274
275    #[test]
276    fn new_sorts_and_dedups_integer_vars() {
277        let m = MilpProblem::new(lp_2var(), vec![1, 0, 1]).unwrap();
278        assert_eq!(m.integer_vars, vec![0, 1]);
279    }
280
281    #[test]
282    fn new_rejects_out_of_range_index() {
283        let err = MilpProblem::new(lp_2var(), vec![0, 2]).unwrap_err();
284        assert_eq!(
285            err,
286            MipProblemError::InvalidIntegerVar {
287                index: 2,
288                num_vars: 2
289            }
290        );
291    }
292
293    #[test]
294    fn empty_integer_vars_allowed() {
295        let m = MilpProblem::new(lp_2var(), vec![]).unwrap();
296        assert!(m.integer_vars.is_empty());
297    }
298
299    #[test]
300    fn miqp_new_validates_indices() {
301        let err = MiqpProblem::new(qp_diag(&[2.0, 2.0]), vec![2]).unwrap_err();
302        assert_eq!(
303            err,
304            MipProblemError::InvalidIntegerVar {
305                index: 2,
306                num_vars: 2
307            }
308        );
309    }
310
311    #[test]
312    fn psd_diagonal_q_is_convex() {
313        let m = MiqpProblem::new(qp_diag(&[2.0, 4.0]), vec![0, 1]).unwrap();
314        assert!(m.is_convex(), "positive diagonal Q must be PSD");
315    }
316
317    #[test]
318    fn indefinite_q_is_not_convex() {
319        let m = MiqpProblem::new(qp_diag(&[2.0, -3.0]), vec![0, 1]).unwrap();
320        assert!(
321            !m.is_convex(),
322            "negative eigenvalue must be detected as non-convex"
323        );
324    }
325
326    #[test]
327    fn zero_q_is_convex() {
328        // Q = 0 (LP-like) is trivially PSD.
329        let m = MiqpProblem::new(qp_diag(&[0.0, 0.0]), vec![0]).unwrap();
330        assert!(m.is_convex());
331    }
332
333    #[test]
334    fn fixed_point_evaluates_objective_exactly() {
335        // min x^2 (Q=2) with x fixed to 2 → obj = 1/2·2·4 = 4, no IPM needed.
336        let qp = qp_diag(&[2.0]);
337        let r = solve_fixed_point(&qp, &[(2.0, 2.0)]).expect("all fixed → Some");
338        assert_eq!(r.status, SolveStatus::Optimal);
339        assert!((r.objective - 4.0).abs() < 1e-12, "obj={}", r.objective);
340        assert_eq!(r.solution, vec![2.0]);
341    }
342
343    #[test]
344    fn fixed_point_returns_none_when_a_var_is_free() {
345        let qp = qp_diag(&[2.0, 2.0]);
346        assert!(solve_fixed_point(&qp, &[(2.0, 2.0), (0.0, 5.0)]).is_none());
347    }
348
349    #[test]
350    fn fixed_point_infeasible_constraint() {
351        // x fixed to 2 but constraint x >= 5 → infeasible.
352        let q = CscMatrix::from_triplets(&[0], &[0], &[2.0], 1, 1).unwrap();
353        let a = CscMatrix::from_triplets(&[0], &[0], &[1.0], 1, 1).unwrap();
354        let qp = QpProblem::new(
355            q,
356            vec![0.0],
357            a,
358            vec![5.0],
359            vec![(2.0, 2.0)],
360            vec![ConstraintType::Ge],
361        )
362        .unwrap();
363        let r = solve_fixed_point(&qp, &[(2.0, 2.0)]).expect("all fixed → Some");
364        assert_eq!(r.status, SolveStatus::Infeasible);
365    }
366
367    #[test]
368    fn fixed_point_empty_box_infeasible() {
369        let qp = qp_diag(&[2.0]);
370        let r = solve_fixed_point(&qp, &[(3.0, 2.0)]).expect("empty box → Some");
371        assert_eq!(r.status, SolveStatus::Infeasible);
372    }
373
374    #[test]
375    fn fixed_point_dim_mismatch_q_returns_numerical_error() {
376        // 2-var QP but only 1 bound: x.len()=1 != Q.ncols=2 → mat_vec_mul error.
377        // Must return Some(NumericalError), not None (which would silently fall to IPM).
378        let qp = qp_diag(&[2.0, 2.0]);
379        let r = solve_fixed_point(&qp, &[(1.0, 1.0)]);
380        assert!(
381            r.is_some(),
382            "dim mismatch must not return None (IPM fallback)"
383        );
384        assert_eq!(r.unwrap().status, SolveStatus::NumericalError);
385    }
386
387    #[test]
388    fn fixed_point_dim_mismatch_a_returns_numerical_error() {
389        // 2-var QP with a constraint: x.len()=1 but A.ncols=2 → A mat_vec_mul error.
390        let q = CscMatrix::from_triplets(&[0, 1], &[0, 1], &[2.0, 2.0], 2, 2).unwrap();
391        let a = CscMatrix::from_triplets(&[0, 0], &[0, 1], &[1.0, 1.0], 1, 2).unwrap();
392        let qp =
393            QpProblem::new_all_le(q, vec![0.0, 0.0], a, vec![5.0], vec![(0.0, 5.0); 2]).unwrap();
394        let r = solve_fixed_point(&qp, &[(1.0, 1.0)]);
395        assert!(
396            r.is_some(),
397            "dim mismatch must not return None (IPM fallback)"
398        );
399        assert_eq!(r.unwrap().status, SolveStatus::NumericalError);
400    }
401
402    #[test]
403    fn psd_with_off_diagonal_detected() {
404        // Q = [[2,1],[1,2]] (full-symmetric storage) is PSD (eigenvalues 1, 3).
405        let q = CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[2.0, 1.0, 1.0, 2.0], 2, 2)
406            .unwrap();
407        let a = CscMatrix::from_triplets(&[], &[], &[], 0, 2).unwrap();
408        let qp = QpProblem::new_all_le(q, vec![0.0, 0.0], a, vec![], vec![(0.0, 5.0); 2]).unwrap();
409        assert!(MiqpProblem::new(qp, vec![0, 1]).unwrap().is_convex());
410
411        // Q = [[1,2],[2,1]] is indefinite (eigenvalues -1, 3).
412        let q2 =
413            CscMatrix::from_triplets(&[0, 0, 1, 1], &[0, 1, 0, 1], &[1.0, 2.0, 2.0, 1.0], 2, 2)
414                .unwrap();
415        let a2 = CscMatrix::from_triplets(&[], &[], &[], 0, 2).unwrap();
416        let qp2 =
417            QpProblem::new_all_le(q2, vec![0.0, 0.0], a2, vec![], vec![(0.0, 5.0); 2]).unwrap();
418        assert!(!MiqpProblem::new(qp2, vec![0, 1]).unwrap().is_convex());
419    }
420
421    // ── Large-n (n > former PSD_DENSE_LIMIT=1000) sentinels ──────────────────
422
423    /// Build an n×n MIQP with diagonal 1.0 and Q[0,1]=Q[1,0]=2.0.
424    /// The top-left 2×2 block has eigenvalues {-1, 3} → matrix is indefinite.
425    fn large_n_indefinite_miqp(n: usize) -> MiqpProblem {
426        let mut rows = Vec::new();
427        let mut cols = Vec::new();
428        let mut vals = Vec::new();
429        for i in 0..n {
430            rows.push(i);
431            cols.push(i);
432            vals.push(1.0_f64);
433        }
434        // off-diagonal: Q[0,1]=Q[1,0]=2 makes the 2×2 block eigenvalues {-1,3}
435        rows.push(0);
436        cols.push(1);
437        vals.push(2.0);
438        rows.push(1);
439        cols.push(0);
440        vals.push(2.0);
441        let q = CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap();
442        let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
443        let qp = QpProblem::new_all_le(q, vec![0.0; n], a, vec![], vec![(0.0, 5.0); n]).unwrap();
444        MiqpProblem::new(qp, vec![0]).unwrap()
445    }
446
447    /// Build an n×n MIQP with strictly positive diagonal Q (identity × 2) → PSD.
448    fn large_n_psd_miqp(n: usize) -> MiqpProblem {
449        let idx: Vec<usize> = (0..n).collect();
450        let vals: Vec<f64> = vec![2.0; n];
451        let q = CscMatrix::from_triplets(&idx, &idx, &vals, n, n).unwrap();
452        let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
453        let qp = QpProblem::new_all_le(q, vec![0.0; n], a, vec![], vec![(0.0, 5.0); n]).unwrap();
454        MiqpProblem::new(qp, vec![0]).unwrap()
455    }
456
457    /// **Sentinel**: n>1000, diagonal≥0, off-diagonal indefinite → nonconvex.
458    ///
459    /// No-op proof: if `is_convex` reverts to `return true` for n > 1000
460    /// (the old PSD_DENSE_LIMIT path), this assertion fails, exposing the
461    /// false-Optimal bug.
462    #[test]
463    fn large_n_off_diag_indefinite_is_not_convex() {
464        let m = large_n_indefinite_miqp(1001);
465        assert!(
466            !m.is_convex(),
467            "n=1001 indefinite MIQP (diag≥0, off-diag λ_min=-1) must be detected as \
468             nonconvex; `return true` for n>1000 produces false-Optimal (sentinel)"
469        );
470    }
471
472    /// **Regression guard**: large-n truly PSD Q must still pass the convexity gate.
473    /// Verifies no over-rejection (false negatives) after the fix.
474    #[test]
475    fn large_n_diagonal_psd_is_convex() {
476        // n=1001 diagonal-2 Q (strictly PD) must be convex.
477        let m = large_n_psd_miqp(1001);
478        assert!(
479            m.is_convex(),
480            "large-n diagonal PSD Q must not be over-rejected"
481        );
482    }
483
484    /// Regression guard: large-n Q=0 (LP case) is trivially convex.
485    #[test]
486    fn large_n_zero_q_is_convex() {
487        let n = 1001usize;
488        let q = CscMatrix::from_triplets(&[], &[], &[], n, n).unwrap();
489        let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
490        let qp = QpProblem::new_all_le(q, vec![0.0; n], a, vec![], vec![(0.0, 5.0); n]).unwrap();
491        let m = MiqpProblem::new(qp, vec![0]).unwrap();
492        assert!(m.is_convex(), "large-n zero Q (LP) must be convex");
493    }
494
495    /// Regression guard: large-n PSD-but-singular Q (rank-deficient PSD) is convex.
496    #[test]
497    fn large_n_psd_singular_is_convex() {
498        // Q: identity except last diagonal is 0 → rank n-1, λ_min=0 (PSD).
499        let n = 1001usize;
500        let mut rows: Vec<usize> = Vec::new();
501        let mut cols: Vec<usize> = Vec::new();
502        let mut vals: Vec<f64> = Vec::new();
503        for i in 0..n - 1 {
504            rows.push(i);
505            cols.push(i);
506            vals.push(1.0);
507        }
508        let q = CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap();
509        let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
510        let qp = QpProblem::new_all_le(q, vec![0.0; n], a, vec![], vec![(0.0, 5.0); n]).unwrap();
511        let m = MiqpProblem::new(qp, vec![0]).unwrap();
512        assert!(
513            m.is_convex(),
514            "large-n PSD-singular Q must be accepted as convex"
515        );
516    }
517
518    /// `is_convex` detects indefinite Q (n=1001) via Cholesky.
519    ///
520    /// **Sentinel**: replacing `is_q_psd_by_cholesky` with `true` causes this to FAIL.
521    #[test]
522    fn is_convex_detects_indefinite_n1001() {
523        let n = 1001_usize;
524        let mut rows = vec![];
525        let mut cols = vec![];
526        let mut vals = vec![];
527        for i in 0..n {
528            rows.push(i);
529            cols.push(i);
530            vals.push(1.0_f64);
531        }
532        rows.push(0);
533        cols.push(1);
534        vals.push(2.0_f64);
535        let q = CscMatrix::from_triplets(&rows, &cols, &vals, n, n).unwrap();
536        let a = CscMatrix::from_triplets(&[], &[], &[], 0, n).unwrap();
537        let qp = QpProblem::new_all_le(q, vec![0.0; n], a, vec![], vec![(0.0, 5.0); n]).unwrap();
538        let m = MiqpProblem::new(qp, vec![0]).unwrap();
539        assert!(
540            !m.is_convex(),
541            "n=1001 indefinite Q must be detected as non-PSD"
542        );
543    }
544}