Skip to main content

basin/core/constraint/
model.rs

1use crate::core::math::Scalar;
2
3#[cfg(test)]
4#[path = "model/integer_domain_api_prototype.rs"]
5mod integer_domain_api_prototype;
6
7/// An optional constraint declaration for objective and residual problems.
8///
9/// Constrained solvers require this declaration and support only the block
10/// combinations they implement. Unconstrained solvers do not inspect it.
11/// Objectives, residuals, and constraint callbacks can be implemented without
12/// this trait. Adding derivatives preserves an existing declaration; an
13/// explicit problem transformation may replace it.
14///
15/// # Contract
16///
17/// The getter must not evaluate the objective or constraints. Bound endpoints,
18/// linear data, and nonlinear counts must remain fixed during a solve. Fresh
19/// initialization may use new dimensions or data. Solvers validate the shapes
20/// they consume and report a changed nonlinear count as [`ConstraintError::LayoutChanged`].
21pub trait Constraints {
22    /// The complete set whose type determines compatibility with constrained solvers.
23    type Set: ConstraintDescription;
24    /// Borrow the complete set without evaluating the problem.
25    fn constraints(&self)
26    -> <Self::Set as ConstraintDescription>::Borrowed<'_>;
27}
28
29mod sealed {
30    pub trait Set {}
31}
32
33/// A canonical constraint-set description. Implementations are sealed.
34pub trait ConstraintDescription: sealed::Set {
35    /// A view that borrows the block data without cloning it.
36    type Borrowed<'a>
37    where
38        Self: 'a;
39    /// Borrow every block in the set.
40    fn borrowed(&self) -> Self::Borrowed<'_>;
41}
42/// Borrow a canonical block while preserving its static presence.
43pub trait BorrowConstraint {
44    /// Borrowed representation of this block.
45    type Borrowed<'a>
46    where
47        Self: 'a;
48    /// Borrow this block's data.
49    fn borrowed(&self) -> Self::Borrowed<'_>;
50}
51
52/// A complete set of problem-side constraints.
53///
54/// Equality and inequality blocks remain distinct. Builders change the set's
55/// type, so constrained solvers cannot ignore unsupported blocks.
56#[derive(Clone, Debug)]
57#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
58pub struct ConstraintSet<
59    B = NoBounds,
60    L = NoLinearConstraints,
61    N = NoNonlinearConstraints,
62> {
63    pub(crate) bounds: B,
64    pub(crate) linear: L,
65    pub(crate) nonlinear: N,
66}
67impl<B, L, N> sealed::Set for ConstraintSet<B, L, N> {}
68impl<B: BorrowConstraint, L: BorrowConstraint, N: BorrowConstraint>
69    ConstraintDescription for ConstraintSet<B, L, N>
70{
71    type Borrowed<'a>
72        = ConstraintSet<B::Borrowed<'a>, L::Borrowed<'a>, N::Borrowed<'a>>
73    where
74        Self: 'a;
75    fn borrowed(&self) -> Self::Borrowed<'_> {
76        ConstraintSet {
77            bounds: self.bounds.borrowed(),
78            linear: self.linear.borrowed(),
79            nonlinear: self.nonlinear.borrowed(),
80        }
81    }
82}
83
84/// A problem with no mathematical constraints.
85pub type NoConstraints = ConstraintSet;
86/// No parameter bounds.
87#[derive(Clone, Copy, Debug, Default)]
88#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
89pub struct NoBounds;
90/// An absent linear block, requiring no matrix type or operations.
91#[derive(Clone, Copy, Debug, Default)]
92#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93pub struct NoLinearBlock;
94/// An absent nonlinear block, requiring no callback or Jacobian.
95#[derive(Clone, Copy, Debug, Default)]
96#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
97pub struct NoNonlinearBlock;
98/// No linear equalities or inequalities.
99pub type NoLinearConstraints = LinearBlocks<NoLinearBlock, NoLinearBlock>;
100/// No nonlinear equalities or inequalities.
101pub type NoNonlinearConstraints =
102    NonlinearBlocks<NoNonlinearBlock, NoNonlinearBlock>;
103
104/// Optional lower and upper bounds, represented in the parameter's type.
105///
106/// Only negative infinity denotes an absent lower bound, and only positive
107/// infinity denotes an absent upper bound. NaN is invalid on either side.
108#[derive(Clone, Debug)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110pub struct BoxBounds<V> {
111    lower: Option<V>,
112    upper: Option<V>,
113}
114impl<V> BoxBounds<V> {
115    /// Store bounds for validation against the parameter at initialization.
116    pub fn new(lower: Option<V>, upper: Option<V>) -> Self {
117        Self { lower, upper }
118    }
119    /// Borrow the lower bounds, when supplied.
120    pub fn lower(&self) -> Option<&V> {
121        self.lower.as_ref()
122    }
123    /// Borrow the upper bounds, when supplied.
124    pub fn upper(&self) -> Option<&V> {
125        self.upper.as_ref()
126    }
127}
128
129/// One linear block. Its location in [`LinearBlocks`] determines equality or inequality semantics.
130///
131/// The matrix and right-hand side must either both be present or both be absent.
132/// Deserialization rejects blocks with only one of them present.
133#[derive(Clone, Debug)]
134#[cfg_attr(feature = "serde", derive(serde::Serialize))]
135pub struct LinearBlock<M, R> {
136    pub(crate) matrix: Option<M>,
137    pub(crate) rhs: Option<R>,
138}
139impl<M, R> LinearBlock<M, R> {
140    /// Store a matrix and right-hand side for checked initialization.
141    pub fn new(matrix: M, rhs: R) -> Self {
142        Self {
143            matrix: Some(matrix),
144            rhs: Some(rhs),
145        }
146    }
147    /// Store an optional block while retaining its kind in the set type.
148    pub fn optional(data: Option<(M, R)>) -> Self {
149        match data {
150            Some((m, r)) => Self::new(m, r),
151            None => Self {
152                matrix: None,
153                rhs: None,
154            },
155        }
156    }
157    /// Borrow the constraint matrix.
158    pub fn matrix(&self) -> Option<&M> {
159        self.matrix.as_ref()
160    }
161    /// Borrow the right-hand side, independently of the parameter representation.
162    pub fn rhs(&self) -> Option<&R> {
163        self.rhs.as_ref()
164    }
165}
166
167#[cfg(feature = "serde")]
168impl<'de, M, R> serde::Deserialize<'de> for LinearBlock<M, R>
169where
170    M: serde::Deserialize<'de>,
171    R: serde::Deserialize<'de>,
172{
173    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
174    where
175        D: serde::Deserializer<'de>,
176    {
177        #[derive(serde::Deserialize)]
178        #[serde(rename = "LinearBlock")]
179        struct StoredLinearBlock<M, R> {
180            matrix: Option<M>,
181            rhs: Option<R>,
182        }
183
184        let stored = StoredLinearBlock::<M, R>::deserialize(deserializer)?;
185        if stored.matrix.is_some() != stored.rhs.is_some() {
186            return Err(serde::de::Error::custom(
187                "linear block matrix and rhs must both be present or both be absent",
188            ));
189        }
190        Ok(Self {
191            matrix: stored.matrix,
192            rhs: stored.rhs,
193        })
194    }
195}
196
197/// Distinct linear equality and inequality blocks.
198#[derive(Clone, Debug)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
200pub struct LinearBlocks<E = NoLinearBlock, I = NoLinearBlock> {
201    pub(crate) equalities: E,
202    pub(crate) inequalities: I,
203}
204impl<E, I> LinearBlocks<E, I> {
205    /// Borrow the equality block.
206    pub fn equalities(&self) -> &E {
207        &self.equalities
208    }
209    /// Borrow the inequality block.
210    pub fn inequalities(&self) -> &I {
211        &self.inequalities
212    }
213}
214
215/// A declared nonlinear block with a fixed callback output length.
216#[derive(Clone, Copy, Debug)]
217#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
218pub struct NonlinearBlock {
219    pub(crate) count: usize,
220}
221impl NonlinearBlock {
222    /// The declared number of scalar constraint values.
223    pub fn count(&self) -> usize {
224        self.count
225    }
226}
227/// Distinct nonlinear equality and inequality declarations.
228#[derive(Clone, Debug)]
229#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
230pub struct NonlinearBlocks<E = NoNonlinearBlock, I = NoNonlinearBlock> {
231    pub(crate) equalities: E,
232    pub(crate) inequalities: I,
233}
234impl<E, I> NonlinearBlocks<E, I> {
235    /// Borrow the equality declaration.
236    pub fn equalities(&self) -> &E {
237        &self.equalities
238    }
239    /// Borrow the inequality declaration.
240    pub fn inequalities(&self) -> &I {
241        &self.inequalities
242    }
243}
244
245impl NoConstraints {
246    /// A shared empty declaration for problems without stored constraints.
247    pub const EMPTY: Self = Self {
248        bounds: NoBounds,
249        linear: LinearBlocks {
250            equalities: NoLinearBlock,
251            inequalities: NoLinearBlock,
252        },
253        nonlinear: NonlinearBlocks {
254            equalities: NoNonlinearBlock,
255            inequalities: NoNonlinearBlock,
256        },
257    };
258    /// Begin an empty set and add its supported blocks with the builders.
259    pub const fn new() -> Self {
260        Self::EMPTY
261    }
262}
263impl Default for NoConstraints {
264    fn default() -> Self {
265        Self::new()
266    }
267}
268impl<B, L, N> ConstraintSet<B, L, N> {
269    /// Borrow the bounds block.
270    pub fn bounds(&self) -> &B {
271        &self.bounds
272    }
273    /// Borrow both linear blocks.
274    pub fn linear(&self) -> &L {
275        &self.linear
276    }
277    /// Borrow both nonlinear declarations.
278    pub fn nonlinear(&self) -> &N {
279        &self.nonlinear
280    }
281    /// Set the bounds without changing other blocks.
282    pub fn with_bounds<V>(
283        self,
284        bounds: BoxBounds<V>,
285    ) -> ConstraintSet<BoxBounds<V>, L, N> {
286        ConstraintSet {
287            bounds,
288            linear: self.linear,
289            nonlinear: self.nonlinear,
290        }
291    }
292}
293impl<B, E, I, N> ConstraintSet<B, LinearBlocks<E, I>, N> {
294    /// Set the linear equalities `A x = b`.
295    pub fn with_linear_equalities<M, R>(
296        self,
297        block: LinearBlock<M, R>,
298    ) -> ConstraintSet<B, LinearBlocks<LinearBlock<M, R>, I>, N> {
299        ConstraintSet {
300            bounds: self.bounds,
301            linear: LinearBlocks {
302                equalities: block,
303                inequalities: self.linear.inequalities,
304            },
305            nonlinear: self.nonlinear,
306        }
307    }
308    /// Set the linear inequalities `A x <= b`.
309    pub fn with_linear_inequalities<M, R>(
310        self,
311        block: LinearBlock<M, R>,
312    ) -> ConstraintSet<B, LinearBlocks<E, LinearBlock<M, R>>, N> {
313        ConstraintSet {
314            bounds: self.bounds,
315            linear: LinearBlocks {
316                equalities: self.linear.equalities,
317                inequalities: block,
318            },
319            nonlinear: self.nonlinear,
320        }
321    }
322}
323impl<B, L, E, I> ConstraintSet<B, L, NonlinearBlocks<E, I>> {
324    /// Declare a nonlinear equality callback `h(x) = 0` with `count` values.
325    pub fn with_nonlinear_equalities(
326        self,
327        count: usize,
328    ) -> ConstraintSet<B, L, NonlinearBlocks<NonlinearBlock, I>> {
329        ConstraintSet {
330            bounds: self.bounds,
331            linear: self.linear,
332            nonlinear: NonlinearBlocks {
333                equalities: NonlinearBlock { count },
334                inequalities: self.nonlinear.inequalities,
335            },
336        }
337    }
338    /// Declare a nonlinear inequality callback `c(x) <= 0` with `count` values.
339    pub fn with_nonlinear_inequalities(
340        self,
341        count: usize,
342    ) -> ConstraintSet<B, L, NonlinearBlocks<E, NonlinearBlock>> {
343        ConstraintSet {
344            bounds: self.bounds,
345            linear: self.linear,
346            nonlinear: NonlinearBlocks {
347                equalities: self.nonlinear.equalities,
348                inequalities: NonlinearBlock { count },
349            },
350        }
351    }
352}
353
354/// The block responsible for an invalid declaration or evaluation.
355#[derive(Clone, Copy, Debug, PartialEq, Eq)]
356#[non_exhaustive]
357pub enum ConstraintBlock {
358    /// Parameter bounds.
359    Bounds,
360    /// Linear equality rows.
361    LinearEqualities,
362    /// Linear inequality rows.
363    LinearInequalities,
364    /// Nonlinear equality residuals.
365    NonlinearEqualities,
366    /// Nonlinear inequality values.
367    NonlinearInequalities,
368    /// Nonlinear constraint Jacobian.
369    Jacobian,
370}
371/// The side of a parameter bound.
372#[derive(Clone, Copy, Debug, PartialEq, Eq)]
373pub enum BoundSide {
374    /// Lower bound.
375    Lower,
376    /// Upper bound.
377    Upper,
378}
379/// A dimension in a constraint shape.
380#[derive(Clone, Copy, Debug, PartialEq, Eq)]
381pub enum ConstraintAxis {
382    /// Vector length.
383    Length,
384    /// Matrix rows.
385    Rows,
386    /// Matrix columns.
387    Columns,
388}
389/// Invalid constraint data or a violation of a callback's shape contract.
390#[derive(Clone, Debug, PartialEq, Eq)]
391#[non_exhaustive]
392pub enum ConstraintError {
393    /// NaN is never an absent-bound sentinel.
394    NaNBound {
395        /// The affected bound side.
396        side: BoundSide,
397        /// The zero-based parameter coordinate.
398        coordinate: usize,
399    },
400    /// An infinity has the wrong sign for this bound side.
401    InvalidInfinity {
402        /// The affected bound side.
403        side: BoundSide,
404        /// The zero-based parameter coordinate.
405        coordinate: usize,
406    },
407    /// A lower bound exceeds its upper bound.
408    InvertedBounds {
409        /// The zero-based parameter coordinate.
410        coordinate: usize,
411    },
412    /// A block has an unexpected dimension.
413    DimensionMismatch {
414        /// The affected constraint block.
415        block: ConstraintBlock,
416        /// The dimension being checked.
417        axis: ConstraintAxis,
418        /// The required dimension.
419        expected: usize,
420        /// The supplied dimension.
421        actual: usize,
422    },
423    /// A solver requires both sides of the box.
424    MissingBound {
425        /// The affected bound side.
426        side: BoundSide,
427    },
428    /// A solver requires a finite sampling or search interval.
429    NonFiniteInterval {
430        /// The zero-based parameter coordinate.
431        coordinate: usize,
432    },
433    /// Linear coefficients or right-hand sides must be finite.
434    NonFiniteLinearData {
435        /// The affected constraint block.
436        block: ConstraintBlock,
437    },
438    /// Constraint layout changed after initialization.
439    LayoutChanged,
440    /// The total number of folded constraints overflowed.
441    CountOverflow,
442}
443impl std::fmt::Display for ConstraintError {
444    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
445        write!(f, "invalid constraints: {self:?}")
446    }
447}
448impl std::error::Error for ConstraintError {}
449/// A checked constrained solve preserves the original callback error in `Evaluation`.
450#[derive(Debug, PartialEq, Eq)]
451#[non_exhaustive]
452pub enum ConstraintSolveError<E> {
453    /// Invalid declarations or malformed callback outputs.
454    InvalidConstraints(ConstraintError),
455    /// The original user-chosen callback error.
456    Evaluation(E),
457}
458impl<E> From<E> for ConstraintSolveError<E> {
459    fn from(error: E) -> Self {
460        Self::Evaluation(error)
461    }
462}
463impl<E: std::fmt::Display> std::fmt::Display for ConstraintSolveError<E> {
464    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
465        match self {
466            Self::InvalidConstraints(e) => e.fmt(f),
467            Self::Evaluation(e) => write!(f, "problem evaluation failed: {e}"),
468        }
469    }
470}
471impl<E: std::error::Error + 'static> std::error::Error
472    for ConstraintSolveError<E>
473{
474    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
475        match self {
476            Self::InvalidConstraints(e) => Some(e),
477            Self::Evaluation(e) => Some(e),
478        }
479    }
480}
481
482/// Values of the nonlinear inequalities declared in the canonical set.
483///
484/// The parameter, value, and error carriers remain independent of objective and
485/// residual traits. Solvers require the matching parameter and error types.
486/// Statically absent and zero-count blocks skip this callback. Non-finite values
487/// retain the consuming solver's numerical failure or moderation policy.
488pub trait NonlinearInequalityValues {
489    /// Parameter type.
490    type Param;
491    /// Readable vector of constraint values, independent of the parameter type.
492    type Values;
493    /// Original callback error type.
494    type Error;
495    /// Evaluate the inequalities in the public `c(x) <= 0` convention.
496    fn nonlinear_inequalities(
497        &self,
498        x: &Self::Param,
499    ) -> Result<Self::Values, ConstraintSolveError<Self::Error>>;
500}
501/// Values of the nonlinear equalities declared in the canonical set.
502pub trait NonlinearEqualityValues {
503    /// Parameter type.
504    type Param;
505    /// Readable vector of equality residuals.
506    type Values;
507    /// Original callback error type.
508    type Error;
509    /// Evaluate `h(x) = 0`.
510    fn nonlinear_equalities(
511        &self,
512        x: &Self::Param,
513    ) -> Result<Self::Values, ConstraintSolveError<Self::Error>>;
514}
515
516pub(crate) fn dimension(
517    block: ConstraintBlock,
518    axis: ConstraintAxis,
519    expected: usize,
520    actual: usize,
521) -> Result<(), ConstraintError> {
522    if expected == actual {
523        Ok(())
524    } else {
525        Err(ConstraintError::DimensionMismatch {
526            block,
527            axis,
528            expected,
529            actual,
530        })
531    }
532}
533pub(crate) fn validate_bound<F: Scalar>(
534    value: F,
535    side: BoundSide,
536    coordinate: usize,
537) -> Result<(), ConstraintError> {
538    if value.is_nan() {
539        return Err(ConstraintError::NaNBound { side, coordinate });
540    }
541    if (side == BoundSide::Lower && value == F::infinity())
542        || (side == BoundSide::Upper && value == F::neg_infinity())
543    {
544        return Err(ConstraintError::InvalidInfinity { side, coordinate });
545    }
546    Ok(())
547}
548macro_rules! borrow_copy {
549    ($($ty:ty),* $(,)?) => {$ (
550        impl BorrowConstraint for $ty {
551            type Borrowed<'a> = Self where Self: 'a;
552            fn borrowed(&self) -> Self { *self }
553        }
554    )*};
555}
556borrow_copy!(NoBounds, NoLinearBlock, NoNonlinearBlock, NonlinearBlock);
557impl<V> BorrowConstraint for BoxBounds<V> {
558    type Borrowed<'a>
559        = BoxBounds<&'a V>
560    where
561        Self: 'a;
562    fn borrowed(&self) -> Self::Borrowed<'_> {
563        BoxBounds::new(self.lower(), self.upper())
564    }
565}
566impl<M, R> BorrowConstraint for LinearBlock<M, R> {
567    type Borrowed<'a>
568        = LinearBlock<&'a M, &'a R>
569    where
570        Self: 'a;
571    fn borrowed(&self) -> Self::Borrowed<'_> {
572        LinearBlock::optional(self.matrix.as_ref().zip(self.rhs.as_ref()))
573    }
574}
575impl<E: BorrowConstraint, I: BorrowConstraint> BorrowConstraint
576    for LinearBlocks<E, I>
577{
578    type Borrowed<'a>
579        = LinearBlocks<E::Borrowed<'a>, I::Borrowed<'a>>
580    where
581        Self: 'a;
582    fn borrowed(&self) -> Self::Borrowed<'_> {
583        LinearBlocks {
584            equalities: self.equalities.borrowed(),
585            inequalities: self.inequalities.borrowed(),
586        }
587    }
588}
589impl<E: BorrowConstraint, I: BorrowConstraint> BorrowConstraint
590    for NonlinearBlocks<E, I>
591{
592    type Borrowed<'a>
593        = NonlinearBlocks<E::Borrowed<'a>, I::Borrowed<'a>>
594    where
595        Self: 'a;
596    fn borrowed(&self) -> Self::Borrowed<'_> {
597        NonlinearBlocks {
598            equalities: self.equalities.borrowed(),
599            inequalities: self.inequalities.borrowed(),
600        }
601    }
602}