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}