Skip to main content

phasesmith_workflows/
constraints.rs

1//! Typed fixed, affine, and multi-source linear constraint transforms.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::error::Error;
5use std::fmt::{Display, Formatter};
6
7use crate::{ParameterKey, ParameterSet};
8
9/// Set one target parameter to a constant during expansion.
10#[derive(Clone, Debug, PartialEq)]
11pub struct FixedConstraint {
12    /// Constrained parameter.
13    target: ParameterKey,
14    /// Constant physical value.
15    value: f64,
16}
17
18impl FixedConstraint {
19    /// Construct a finite fixed constraint.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`ConstraintError::NonFiniteCoefficient`] for a non-finite value.
24    pub fn new(target: ParameterKey, value: f64) -> Result<Self, ConstraintError> {
25        if !value.is_finite() {
26            return Err(ConstraintError::NonFiniteCoefficient);
27        }
28        Ok(Self { target, value })
29    }
30
31    /// Borrow the constrained parameter.
32    #[must_use]
33    pub const fn target(&self) -> &ParameterKey {
34        &self.target
35    }
36
37    /// Return the constant physical value.
38    #[must_use]
39    pub const fn value(&self) -> f64 {
40        self.value
41    }
42}
43
44/// Define `target = multiplier * source + offset`.
45#[derive(Clone, Debug, PartialEq)]
46pub struct AffineConstraint {
47    /// Constrained parameter.
48    target: ParameterKey,
49    /// Already-resolved source parameter.
50    source: ParameterKey,
51    /// Source multiplier.
52    multiplier: f64,
53    /// Physical offset.
54    offset: f64,
55}
56
57impl AffineConstraint {
58    /// Validate distinct keys and finite coefficients.
59    ///
60    /// # Errors
61    ///
62    /// Returns [`ConstraintError`] for a self-reference or non-finite value.
63    pub fn new(
64        target: ParameterKey,
65        source: ParameterKey,
66        multiplier: f64,
67        offset: f64,
68    ) -> Result<Self, ConstraintError> {
69        if target == source {
70            return Err(ConstraintError::TargetIsSource { target });
71        }
72        if !multiplier.is_finite() || !offset.is_finite() {
73            return Err(ConstraintError::NonFiniteCoefficient);
74        }
75        Ok(Self {
76            target,
77            source,
78            multiplier,
79            offset,
80        })
81    }
82
83    /// Borrow the constrained parameter.
84    #[must_use]
85    pub const fn target(&self) -> &ParameterKey {
86        &self.target
87    }
88
89    /// Borrow the already-resolved source.
90    #[must_use]
91    pub const fn source(&self) -> &ParameterKey {
92        &self.source
93    }
94
95    /// Return the source multiplier.
96    #[must_use]
97    pub const fn multiplier(&self) -> f64 {
98        self.multiplier
99    }
100
101    /// Return the physical offset.
102    #[must_use]
103    pub const fn offset(&self) -> f64 {
104        self.offset
105    }
106}
107
108/// One source/coefficient pair in a linear constraint.
109#[derive(Clone, Debug, PartialEq)]
110pub struct LinearTerm {
111    /// Already-resolved source parameter.
112    source: ParameterKey,
113    /// Source coefficient.
114    coefficient: f64,
115}
116
117impl LinearTerm {
118    /// Construct one finite source term.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`ConstraintError::NonFiniteCoefficient`] for a non-finite coefficient.
123    pub fn new(source: ParameterKey, coefficient: f64) -> Result<Self, ConstraintError> {
124        if !coefficient.is_finite() {
125            return Err(ConstraintError::NonFiniteCoefficient);
126        }
127        Ok(Self {
128            source,
129            coefficient,
130        })
131    }
132
133    /// Borrow the source parameter.
134    #[must_use]
135    pub const fn source(&self) -> &ParameterKey {
136        &self.source
137    }
138
139    /// Return the finite source coefficient.
140    #[must_use]
141    pub const fn coefficient(&self) -> f64 {
142        self.coefficient
143    }
144}
145
146/// Define `target = offset + sum(coefficient * source)`.
147#[derive(Clone, Debug, PartialEq)]
148pub struct LinearConstraint {
149    /// Constrained parameter.
150    target: ParameterKey,
151    /// Ordered, unique source terms.
152    terms: Vec<LinearTerm>,
153    /// Physical offset.
154    offset: f64,
155}
156
157impl LinearConstraint {
158    /// Validate a non-empty list of unique, non-target sources.
159    ///
160    /// # Errors
161    ///
162    /// Returns [`ConstraintError`] for empty/duplicate/self sources or a
163    /// non-finite offset.
164    pub fn new(
165        target: ParameterKey,
166        terms: Vec<LinearTerm>,
167        offset: f64,
168    ) -> Result<Self, ConstraintError> {
169        if terms.is_empty() {
170            return Err(ConstraintError::EmptyLinearTerms);
171        }
172        if !offset.is_finite() {
173            return Err(ConstraintError::NonFiniteCoefficient);
174        }
175        let mut sources = BTreeSet::new();
176        for term in &terms {
177            if term.source == target {
178                return Err(ConstraintError::TargetIsSource {
179                    target: target.clone(),
180                });
181            }
182            if !sources.insert(term.source.clone()) {
183                return Err(ConstraintError::DuplicateLinearSource {
184                    source: term.source.clone(),
185                });
186            }
187        }
188        Ok(Self {
189            target,
190            terms,
191            offset,
192        })
193    }
194
195    /// Borrow the constrained parameter.
196    #[must_use]
197    pub const fn target(&self) -> &ParameterKey {
198        &self.target
199    }
200
201    /// Borrow the ordered unique source terms.
202    #[must_use]
203    pub fn terms(&self) -> &[LinearTerm] {
204        &self.terms
205    }
206
207    /// Return the physical offset.
208    #[must_use]
209    pub const fn offset(&self) -> f64 {
210        self.offset
211    }
212}
213
214/// Supported native scalar constraint records.
215#[derive(Clone, Debug, PartialEq)]
216pub enum Constraint {
217    /// Constant target.
218    Fixed(FixedConstraint),
219    /// One-source affine target.
220    Affine(AffineConstraint),
221    /// Multi-source linear target.
222    Linear(LinearConstraint),
223}
224
225impl Constraint {
226    /// Borrow the constrained target shared by every record variant.
227    #[must_use]
228    pub fn target(&self) -> &ParameterKey {
229        match self {
230            Self::Fixed(value) => &value.target,
231            Self::Affine(value) => &value.target,
232            Self::Linear(value) => &value.target,
233        }
234    }
235
236    fn sources(&self) -> impl Iterator<Item = &ParameterKey> {
237        let sources: Vec<&ParameterKey> = match self {
238            Self::Fixed(_) => Vec::new(),
239            Self::Affine(value) => vec![&value.source],
240            Self::Linear(value) => value.terms.iter().map(|term| &term.source).collect(),
241        };
242        sources.into_iter()
243    }
244}
245
246/// Row-major derivative `d physical values / d scaled free values`.
247#[derive(Clone, Debug, PartialEq)]
248pub struct ConstraintDerivativeMatrix {
249    /// Parameter row count.
250    pub rows: usize,
251    /// Free-parameter column count.
252    pub columns: usize,
253    /// Row-major matrix elements.
254    pub values: Vec<f64>,
255}
256
257impl ConstraintDerivativeMatrix {
258    /// Borrow one parameter row.
259    #[must_use]
260    pub fn row(&self, index: usize) -> Option<&[f64]> {
261        let start = index.checked_mul(self.columns)?;
262        self.values.get(start..start.checked_add(self.columns)?)
263    }
264}
265
266/// Validated ordered mapping between free solver coordinates and all values.
267#[derive(Clone, Debug, PartialEq)]
268pub struct ConstraintTransform {
269    parameters: ParameterSet,
270    constraints: Vec<Constraint>,
271    free_keys: Vec<ParameterKey>,
272}
273
274impl ConstraintTransform {
275    /// Validate target ownership, uniqueness, source ownership, and dependency order.
276    ///
277    /// # Errors
278    ///
279    /// Returns [`ConstraintError`] for unknown keys, duplicate targets, or a
280    /// source that is not already resolved (including cycles).
281    pub fn new(
282        parameters: ParameterSet,
283        constraints: Vec<Constraint>,
284    ) -> Result<Self, ConstraintError> {
285        let known = parameters.key_set();
286        let mut targets = BTreeSet::new();
287        for constraint in &constraints {
288            if !known.contains(constraint.target()) {
289                return Err(ConstraintError::UnknownTarget {
290                    target: constraint.target().clone(),
291                });
292            }
293            if !targets.insert(constraint.target().clone()) {
294                return Err(ConstraintError::DuplicateTarget {
295                    target: constraint.target().clone(),
296                });
297            }
298        }
299        let mut resolved = known.difference(&targets).cloned().collect::<BTreeSet<_>>();
300        for constraint in &constraints {
301            for source in constraint.sources() {
302                if !known.contains(source) {
303                    return Err(ConstraintError::UnknownSource {
304                        source: source.clone(),
305                    });
306                }
307                if !resolved.contains(source) {
308                    return Err(ConstraintError::UnresolvedDependency {
309                        target: Box::new(constraint.target().clone()),
310                        source: Box::new(source.clone()),
311                    });
312                }
313            }
314            resolved.insert(constraint.target().clone());
315        }
316        let free_keys = parameters
317            .specs()
318            .iter()
319            .filter(|spec| spec.refine() && !targets.contains(spec.key()))
320            .map(|spec| spec.key().clone())
321            .collect();
322        Ok(Self {
323            parameters,
324            constraints,
325            free_keys,
326        })
327    }
328
329    /// Borrow the validated parameter set.
330    #[must_use]
331    pub const fn parameters(&self) -> &ParameterSet {
332        &self.parameters
333    }
334
335    /// Borrow constraints in required dependency order.
336    #[must_use]
337    pub fn constraints(&self) -> &[Constraint] {
338        &self.constraints
339    }
340
341    /// Borrow free identities in stable packing order.
342    #[must_use]
343    pub fn free_keys(&self) -> &[ParameterKey] {
344        &self.free_keys
345    }
346
347    /// Pack current physical values into scaled free solver coordinates.
348    ///
349    /// # Errors
350    ///
351    /// Returns [`ConstraintError`] if a stored value unexpectedly becomes
352    /// non-finite.
353    pub fn pack(&self) -> Result<Vec<f64>, ConstraintError> {
354        self.pack_values(&self.parameters.values())
355    }
356
357    /// Pack caller-supplied physical values into scaled free coordinates.
358    ///
359    /// # Errors
360    ///
361    /// Returns [`ConstraintError`] for a missing or non-finite free value.
362    pub fn pack_values(
363        &self,
364        values: &BTreeMap<ParameterKey, f64>,
365    ) -> Result<Vec<f64>, ConstraintError> {
366        self.free_keys
367            .iter()
368            .map(|key| {
369                let value = values
370                    .get(key)
371                    .copied()
372                    .ok_or_else(|| ConstraintError::MissingValue { key: key.clone() })?;
373                let spec =
374                    self.parameters
375                        .spec(key)
376                        .ok_or_else(|| ConstraintError::UnknownSource {
377                            source: key.clone(),
378                        })?;
379                let scaled = value / spec.scale();
380                if !scaled.is_finite() {
381                    return Err(ConstraintError::NonFiniteVector);
382                }
383                Ok(scaled)
384            })
385            .collect()
386    }
387
388    /// Expand scaled free coordinates into all bounded physical values.
389    ///
390    /// When `clip` is true, free physical values are projected to their bounds
391    /// before constraints are evaluated. Constraint results are never clipped.
392    ///
393    /// # Errors
394    ///
395    /// Returns [`ConstraintError`] for shape, finiteness, or final-bound failures.
396    pub fn unpack(
397        &self,
398        vector: &[f64],
399        clip: bool,
400    ) -> Result<BTreeMap<ParameterKey, f64>, ConstraintError> {
401        if vector.len() != self.free_keys.len() {
402            return Err(ConstraintError::VectorLengthMismatch {
403                expected: self.free_keys.len(),
404                actual: vector.len(),
405            });
406        }
407        if vector.iter().any(|value| !value.is_finite()) {
408            return Err(ConstraintError::NonFiniteVector);
409        }
410        let mut values = self.parameters.values();
411        for (key, scaled) in self.free_keys.iter().zip(vector) {
412            let spec = self
413                .parameters
414                .spec(key)
415                .ok_or_else(|| ConstraintError::UnknownSource {
416                    source: key.clone(),
417                })?;
418            let physical = scaled * spec.scale();
419            values.insert(
420                key.clone(),
421                if clip {
422                    spec.bounds().clip(physical)
423                } else {
424                    physical
425                },
426            );
427        }
428        for constraint in &self.constraints {
429            let value = match constraint {
430                Constraint::Fixed(value) => value.value,
431                Constraint::Affine(value) => {
432                    value.multiplier
433                        * values.get(&value.source).copied().ok_or_else(|| {
434                            ConstraintError::MissingValue {
435                                key: value.source.clone(),
436                            }
437                        })?
438                        + value.offset
439                }
440                Constraint::Linear(value) => {
441                    let mut result = value.offset;
442                    for term in &value.terms {
443                        result += term.coefficient
444                            * values.get(&term.source).copied().ok_or_else(|| {
445                                ConstraintError::MissingValue {
446                                    key: term.source.clone(),
447                                }
448                            })?;
449                    }
450                    result
451                }
452            };
453            values.insert(constraint.target().clone(), value);
454        }
455        for spec in self.parameters.specs() {
456            let value =
457                values
458                    .get(spec.key())
459                    .copied()
460                    .ok_or_else(|| ConstraintError::MissingValue {
461                        key: spec.key().clone(),
462                    })?;
463            if !value.is_finite() || !spec.bounds().contains(value) {
464                return Err(ConstraintError::ExpandedValueOutsideBounds {
465                    key: spec.key().clone(),
466                    value,
467                });
468            }
469        }
470        Ok(values)
471    }
472
473    /// Build the exact row-major physical-to-scaled-free derivative matrix.
474    ///
475    /// # Errors
476    ///
477    /// Returns [`ConstraintError::MatrixSizeOverflow`] if the matrix element
478    /// count cannot be represented, or [`ConstraintError::InternalInvariant`]
479    /// if validated transform state is unexpectedly inconsistent.
480    pub fn derivative_matrix(&self) -> Result<ConstraintDerivativeMatrix, ConstraintError> {
481        let rows = self.parameters.specs().len();
482        let columns = self.free_keys.len();
483        let element_count = rows
484            .checked_mul(columns)
485            .ok_or(ConstraintError::MatrixSizeOverflow)?;
486        let mut values = vec![0.0; element_count];
487        for (column, key) in self.free_keys.iter().enumerate() {
488            let row = self
489                .parameters
490                .index_of(key)
491                .ok_or(ConstraintError::InternalInvariant)?;
492            let spec = self
493                .parameters
494                .spec(key)
495                .ok_or(ConstraintError::InternalInvariant)?;
496            *values
497                .get_mut(row * columns + column)
498                .ok_or(ConstraintError::InternalInvariant)? = spec.scale();
499        }
500        for constraint in &self.constraints {
501            let target_row = self
502                .parameters
503                .index_of(constraint.target())
504                .ok_or(ConstraintError::InternalInvariant)?;
505            match constraint {
506                Constraint::Fixed(_) => {}
507                Constraint::Affine(constraint) => {
508                    let source_row = self
509                        .parameters
510                        .index_of(&constraint.source)
511                        .ok_or(ConstraintError::InternalInvariant)?;
512                    for column in 0..columns {
513                        let source_value = values
514                            .get(source_row * columns + column)
515                            .copied()
516                            .ok_or(ConstraintError::InternalInvariant)?;
517                        *values
518                            .get_mut(target_row * columns + column)
519                            .ok_or(ConstraintError::InternalInvariant)? =
520                            constraint.multiplier * source_value;
521                    }
522                }
523                Constraint::Linear(constraint) => {
524                    for column in 0..columns {
525                        let mut target_value = 0.0;
526                        for term in &constraint.terms {
527                            let source_row = self
528                                .parameters
529                                .index_of(&term.source)
530                                .ok_or(ConstraintError::InternalInvariant)?;
531                            target_value += term.coefficient
532                                * values
533                                    .get(source_row * columns + column)
534                                    .copied()
535                                    .ok_or(ConstraintError::InternalInvariant)?;
536                        }
537                        *values
538                            .get_mut(target_row * columns + column)
539                            .ok_or(ConstraintError::InternalInvariant)? = target_value;
540                    }
541                }
542            }
543        }
544        Ok(ConstraintDerivativeMatrix {
545            rows,
546            columns,
547            values,
548        })
549    }
550}
551
552/// Invalid native scalar constraint or transform input.
553#[derive(Clone, Debug, PartialEq)]
554pub enum ConstraintError {
555    /// A coefficient, offset, or fixed value is non-finite.
556    NonFiniteCoefficient,
557    /// A target is also used directly as its source.
558    TargetIsSource {
559        /// Invalid target.
560        target: ParameterKey,
561    },
562    /// A linear constraint has no sources.
563    EmptyLinearTerms,
564    /// A linear constraint repeats one source.
565    DuplicateLinearSource {
566        /// Repeated source.
567        source: ParameterKey,
568    },
569    /// A target does not belong to the parameter set.
570    UnknownTarget {
571        /// Missing target.
572        target: ParameterKey,
573    },
574    /// More than one constraint owns a target.
575    DuplicateTarget {
576        /// Repeated target.
577        target: ParameterKey,
578    },
579    /// A source does not belong to the parameter set.
580    UnknownSource {
581        /// Missing source.
582        source: ParameterKey,
583    },
584    /// A source is constrained later or belongs to a dependency cycle.
585    UnresolvedDependency {
586        /// Target currently being resolved.
587        target: Box<ParameterKey>,
588        /// Unavailable source.
589        source: Box<ParameterKey>,
590    },
591    /// A caller-supplied value map omits a free key.
592    MissingValue {
593        /// Missing key.
594        key: ParameterKey,
595    },
596    /// A solver vector has the wrong length.
597    VectorLengthMismatch {
598        /// Required length.
599        expected: usize,
600        /// Received length.
601        actual: usize,
602    },
603    /// A packed or unpacked solver value is non-finite.
604    NonFiniteVector,
605    /// Derivative matrix dimensions overflow the platform element count.
606    MatrixSizeOverflow,
607    /// Private validated transform state is unexpectedly inconsistent.
608    InternalInvariant,
609    /// One expanded physical value violates its declared bounds.
610    ExpandedValueOutsideBounds {
611        /// Invalid parameter.
612        key: ParameterKey,
613        /// Expanded value.
614        value: f64,
615    },
616}
617
618impl Display for ConstraintError {
619    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
620        match self {
621            Self::NonFiniteCoefficient => {
622                formatter.write_str("constraint values and coefficients must be finite")
623            }
624            Self::TargetIsSource { target } => {
625                write!(
626                    formatter,
627                    "constraint target {target} cannot be its own source"
628                )
629            }
630            Self::EmptyLinearTerms => {
631                formatter.write_str("linear constraints require at least one source")
632            }
633            Self::DuplicateLinearSource { source } => {
634                write!(formatter, "linear constraint repeats source {source}")
635            }
636            Self::UnknownTarget { target } => {
637                write!(formatter, "constraint target {target} is not a parameter")
638            }
639            Self::DuplicateTarget { target } => {
640                write!(
641                    formatter,
642                    "parameter {target} is constrained more than once"
643                )
644            }
645            Self::UnknownSource { source } => {
646                write!(formatter, "constraint source {source} is not a parameter")
647            }
648            Self::UnresolvedDependency { target, source } => write!(
649                formatter,
650                "constraint for {target} depends on unresolved source {source}"
651            ),
652            Self::MissingValue { key } => {
653                write!(formatter, "missing value for free parameter {key}")
654            }
655            Self::VectorLengthMismatch { expected, actual } => write!(
656                formatter,
657                "free vector length {actual} does not match expected length {expected}"
658            ),
659            Self::NonFiniteVector => formatter.write_str("free parameter values must be finite"),
660            Self::MatrixSizeOverflow => {
661                formatter.write_str("constraint derivative matrix size overflow")
662            }
663            Self::InternalInvariant => {
664                formatter.write_str("validated constraint transform state is inconsistent")
665            }
666            Self::ExpandedValueOutsideBounds { key, value } => write!(
667                formatter,
668                "expanded value {value} for {key} lies outside its bounds"
669            ),
670        }
671    }
672}
673
674impl Error for ConstraintError {}