Skip to main content

dotloom_constraints/
solution.rs

1//! Solve results, statuses and structured diagnostics.
2
3use serde::{Deserialize, Serialize};
4
5use crate::VarId;
6
7/// Which backend handled a component.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub enum Backend {
11    /// No rules: targets/stays only, or constant rules.
12    Trivial,
13    /// kasuari (Cassowary) for purely linear components.
14    Linear,
15    /// Hierarchical damped Gauss–Newton for nonlinear/mixed components.
16    Numeric,
17}
18
19/// Status of one component (or the aggregate solve).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(tag = "status", rename_all = "camelCase")]
22pub enum Status {
23    /// All hard rules hold and no degrees of freedom remain.
24    Solved,
25    /// All hard rules hold; `dof` independent motions remain. This is a valid state.
26    Underconstrained {
27        /// Remaining degrees of freedom.
28        dof: usize,
29    },
30    /// Proven contradiction between hard rules (see diagnostics for the evidence).
31    Conflicting,
32    /// The solver stopped without satisfying hard rules. This is *not* a proof of
33    /// infeasibility; `suspected_conflict` marks a stationary point with residual left.
34    NotConverged {
35        /// The hard residual stopped decreasing at a non-zero value.
36        suspected_conflict: bool,
37    },
38    /// The job was cancelled before completion.
39    Cancelled,
40    /// A rule could not be expressed by the supported equation classes.
41    Unsupported,
42}
43
44impl Status {
45    /// Whether the resulting values satisfy all hard rules and may be committed.
46    #[must_use]
47    pub const fn is_acceptable(self) -> bool {
48        matches!(self, Self::Solved | Self::Underconstrained { .. })
49    }
50
51    fn rank(self) -> u8 {
52        match self {
53            Self::Solved => 0,
54            Self::Underconstrained { .. } => 1,
55            Self::NotConverged { .. } => 3,
56            Self::Conflicting => 4,
57            Self::Unsupported => 5,
58            Self::Cancelled => 6,
59        }
60    }
61
62    /// Combine two statuses (worst wins; DOF add up).
63    #[must_use]
64    pub fn combine(self, o: Self) -> Self {
65        match (self, o) {
66            (Self::Underconstrained { dof: a }, Self::Underconstrained { dof: b }) => {
67                Self::Underconstrained { dof: a + b }
68            }
69            (Self::NotConverged { suspected_conflict: a }, Self::NotConverged { suspected_conflict: b }) => {
70                Self::NotConverged { suspected_conflict: a || b }
71            }
72            _ => {
73                if o.rank() > self.rank() {
74                    o
75                } else {
76                    self
77                }
78            }
79        }
80    }
81}
82
83/// Diagnostic category.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
85#[serde(rename_all = "camelCase")]
86pub enum DiagnosticKind {
87    /// Hard rules contradict each other (certain).
88    Conflict,
89    /// Hard rules could not be satisfied; likely but not proven contradictory.
90    SuspectedConflict,
91    /// Iteration budget exhausted.
92    NotConverged,
93    /// Rules are redundant (consistent duplicates); informational.
94    Redundant,
95    /// Rule class not supported.
96    Unsupported,
97    /// A preference could not be met because harder rules prevent it.
98    PreferenceUnmet,
99    /// The job was cancelled.
100    Cancelled,
101}
102
103/// Certainty of a diagnostic.
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
105#[serde(rename_all = "camelCase")]
106pub enum Certainty {
107    /// Proven (e.g. Cassowary required-failure, all-constant rule violated).
108    Certain,
109    /// Heuristic evidence (stationary point of the residual).
110    Suspected,
111}
112
113/// A structured diagnostic.
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
115#[serde(rename_all = "camelCase")]
116pub struct Diagnostic {
117    /// Category.
118    pub kind: DiagnosticKind,
119    /// Certainty.
120    pub certainty: Certainty,
121    /// Rule IDs involved.
122    pub rules: Vec<u64>,
123    /// Rule labels (same order).
124    pub labels: Vec<String>,
125    /// Rule sources (same order).
126    pub sources: Vec<String>,
127    /// Related entity IDs (deduplicated).
128    pub entities: Vec<u64>,
129    /// Largest absolute residual among the rules, in model units.
130    pub residual: Option<f64>,
131    /// English message for developers; UIs build their own text from the fields.
132    pub message: String,
133}
134
135/// Report of one solved component.
136#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
137#[serde(rename_all = "camelCase")]
138pub struct ComponentReport {
139    /// Free variables of the component.
140    pub vars: Vec<VarId>,
141    /// Rule IDs of the component.
142    pub rules: Vec<u64>,
143    /// Backend used.
144    pub backend: Backend,
145    /// Status.
146    pub status: Status,
147    /// Iterations used (numeric backend) or 1.
148    pub iterations: u32,
149    /// Largest hard residual relative to its row scale.
150    pub max_hard_residual: f64,
151}
152
153/// Complete solve result.
154#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct Solution {
157    /// Values for every variable (unchanged input values when not acceptable).
158    pub values: Vec<f64>,
159    /// Aggregate status.
160    pub status: Status,
161    /// Per-component reports.
162    pub components: Vec<ComponentReport>,
163    /// Diagnostics.
164    pub diagnostics: Vec<Diagnostic>,
165    /// Total iterations.
166    pub iterations: u32,
167}
168
169impl Solution {
170    /// Whether the values may be committed.
171    #[must_use]
172    pub fn accepted(&self) -> bool {
173        self.status.is_acceptable()
174    }
175}