Skip to main content

dotloom_constraints/
problem.rs

1//! Problem description handed to the solver.
2
3use serde::{Deserialize, Serialize};
4
5use crate::{Expr, VarId};
6
7/// Constraint strength. `Required` rules are hard: a solution violating them is
8/// never accepted. The others are preferences ordered by priority.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub enum Strength {
12    /// Weak preference.
13    Weak,
14    /// Medium preference.
15    Medium,
16    /// Strong preference (drag targets use this).
17    Strong,
18    /// Hard rule.
19    #[default]
20    Required,
21}
22
23impl Strength {
24    /// Least-squares weight for the numeric backend (not squared). Hard rules are
25    /// constraints, not weights; the bounded 10× ladder keeps the KKT system well
26    /// conditioned (ADR-0004).
27    #[must_use]
28    pub const fn weight(self) -> f64 {
29        match self {
30            Self::Required => f64::INFINITY,
31            Self::Strong => 1.0,
32            Self::Medium => 0.1,
33            Self::Weak => 0.01,
34        }
35    }
36}
37
38/// Weight of the implicit "stay near the previous valid value" preference. It is
39/// below every user strength so any explicit preference wins over staying put.
40pub const STAY_WEIGHT: f64 = 1e-3;
41
42/// Relation of a row: `expr = 0` or `expr ≤ 0`.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub enum Relation {
46    /// `expr = 0`.
47    Eq,
48    /// `expr ≤ 0`.
49    Le,
50}
51
52/// One scalar residual of a rule.
53#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct Row {
55    /// Residual expression.
56    pub expr: Expr,
57    /// Relation.
58    pub relation: Relation,
59    /// Characteristic magnitude of the residual (mm for lengths, 1 for angles).
60    /// Tolerances are relative to it.
61    pub scale: f64,
62}
63
64/// A rule: one or more rows with shared metadata.
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
66pub struct Rule {
67    /// Stable rule ID (the document constraint ID or a synthetic ID).
68    pub id: u64,
69    /// Rows.
70    pub rows: Vec<Row>,
71    /// Strength.
72    pub strength: Strength,
73    /// Human readable label used in diagnostics (`"distance 50 mm"`).
74    pub label: String,
75    /// Source label (`"user"`, `"edit"`, `"plugin:acme.wall"`, ...).
76    pub source: String,
77    /// Related entity IDs for diagnostics.
78    pub entities: Vec<u64>,
79    /// When set, the rule cannot be expressed; components containing it report
80    /// `unsupported` instead of being solved.
81    #[serde(default, skip_serializing_if = "Option::is_none")]
82    pub unsupported: Option<String>,
83}
84
85impl Rule {
86    /// Create a rule with default metadata.
87    #[must_use]
88    pub fn new(id: u64, rows: Vec<Row>, strength: Strength) -> Self {
89        Self {
90            id,
91            rows,
92            strength,
93            label: String::new(),
94            source: String::new(),
95            entities: Vec::new(),
96            unsupported: None,
97        }
98    }
99
100    /// Builder: label.
101    #[must_use]
102    pub fn label(mut self, l: impl Into<String>) -> Self {
103        self.label = l.into();
104        self
105    }
106
107    /// Builder: source.
108    #[must_use]
109    pub fn source(mut self, s: impl Into<String>) -> Self {
110        self.source = s.into();
111        self
112    }
113
114    /// Builder: related entities.
115    #[must_use]
116    pub fn entities(mut self, e: Vec<u64>) -> Self {
117        self.entities = e;
118        self
119    }
120
121    /// Variables referenced by any row.
122    #[must_use]
123    pub fn vars(&self) -> Vec<VarId> {
124        let mut v: Vec<VarId> = self.rows.iter().flat_map(|r| r.expr.vars()).collect();
125        v.sort_unstable();
126        v.dedup();
127        v
128    }
129
130    /// Whether this is a hard rule.
131    #[must_use]
132    pub fn is_hard(&self) -> bool {
133        self.strength == Strength::Required
134    }
135}
136
137/// A solver variable.
138#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
139pub struct Variable {
140    /// Current (starting) value.
141    pub value: f64,
142    /// Fixed variables are constants for this solve (locks, `fix` rules on params).
143    #[serde(default)]
144    pub fixed: bool,
145    /// Characteristic magnitude used to scale steps (mm for lengths, 1 for angles).
146    pub scale: f64,
147    /// Diagnostic label (`"e12.width"`).
148    #[serde(default)]
149    pub label: String,
150    /// Multiplier of the stay preference (how strongly this variable keeps its
151    /// previous value relative to others), clamped to `[0.1, 5]`.
152    #[serde(default = "one")]
153    pub stay: f64,
154}
155
156fn one() -> f64 {
157    1.0
158}
159
160/// Clamp a stay multiplier to the supported range.
161#[must_use]
162pub fn stay_factor(v: f64) -> f64 {
163    if v.is_finite() { v.clamp(0.1, 5.0) } else { 1.0 }
164}
165
166impl Variable {
167    /// Free variable with scale 1.
168    #[must_use]
169    pub fn new(value: f64) -> Self {
170        Self { value, fixed: false, scale: 1.0, label: String::new(), stay: 1.0 }
171    }
172
173    /// Builder: stay multiplier.
174    #[must_use]
175    pub fn stay(mut self, s: f64) -> Self {
176        self.stay = stay_factor(s);
177        self
178    }
179
180    /// Builder: scale.
181    #[must_use]
182    pub fn scale(mut self, s: f64) -> Self {
183        self.scale = s;
184        self
185    }
186
187    /// Builder: fixed flag.
188    #[must_use]
189    pub fn fixed(mut self, f: bool) -> Self {
190        self.fixed = f;
191        self
192    }
193
194    /// Builder: label.
195    #[must_use]
196    pub fn label(mut self, l: impl Into<String>) -> Self {
197        self.label = l.into();
198        self
199    }
200}
201
202/// A desired value for a variable (drag target, typed preference). Targets are never
203/// hard; hard edits are expressed as `Required` rules.
204#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
205pub struct Target {
206    /// Variable.
207    pub var: VarId,
208    /// Desired value.
209    pub value: f64,
210    /// Priority (must not be `Required`; treated as `Strong` if it is).
211    pub strength: Strength,
212}
213
214/// A complete solve request.
215#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
216pub struct Problem {
217    /// Variables indexed by [`VarId`].
218    pub vars: Vec<Variable>,
219    /// Rules in priority order of insertion (structural rules first, edit rules last).
220    pub rules: Vec<Rule>,
221    /// Targets.
222    pub targets: Vec<Target>,
223}
224
225impl Problem {
226    /// Add a variable and return its ID.
227    pub fn add_var(&mut self, v: Variable) -> VarId {
228        let id = VarId(u32::try_from(self.vars.len()).unwrap_or(u32::MAX));
229        self.vars.push(v);
230        id
231    }
232
233    /// Current values.
234    #[must_use]
235    pub fn values(&self) -> Vec<f64> {
236        self.vars.iter().map(|v| v.value).collect()
237    }
238
239    /// Whether `v` is fixed.
240    #[must_use]
241    pub fn is_fixed(&self, v: VarId) -> bool {
242        self.vars.get(v.index()).is_none_or(|x| x.fixed)
243    }
244}
245
246/// Solver options.
247#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
248pub struct SolveOptions {
249    /// Relative residual tolerance for hard rows: `|r| ≤ tol · row.scale`.
250    pub tolerance: f64,
251    /// Maximum Gauss–Newton iterations per numeric component.
252    pub max_iterations: u32,
253    /// Maximum number of rules examined when minimizing a linear conflict set.
254    pub conflict_search_limit: usize,
255    /// Compute degrees of freedom and redundancy (rank analysis) for accepted
256    /// solutions. Interactive previews may skip it; commits keep it on.
257    pub analyze: bool,
258}
259
260impl Default for SolveOptions {
261    fn default() -> Self {
262        Self { tolerance: 1e-9, max_iterations: 100, conflict_search_limit: 64, analyze: true }
263    }
264}