Skip to main content

oxiui_core/
solver.rs

1//! Pure-Rust Cassowary linear constraint solver.
2//!
3//! Implements the dual-phase simplex restricted tableau (Badros & Borning 1997),
4//! faithful to the cassowary-rs / kiwi reference structure. No external crates
5//! are used.
6//!
7//! ## Quick start
8//!
9//! ```rust
10//! use oxiui_core::solver::{Solver, Variable, Constraint, Expression, Term, RelOp, Strength};
11//!
12//! let mut solver = Solver::new();
13//! let x = Variable::new();
14//! solver.add_constraint(Constraint::new(
15//!     Expression::new(vec![Term { variable: x.clone(), coefficient: 1.0 }], -42.0),
16//!     RelOp::Equal,
17//!     Strength::REQUIRED,
18//! )).unwrap();
19//! solver.update_variables();
20//! assert!((solver.value_of(&x) - 42.0).abs() < 1e-6);
21//! ```
22
23use std::collections::HashMap;
24use std::sync::atomic::{AtomicU64, Ordering};
25
26// ── Atomic id generators ────────────────────────────────────────────────────
27
28static NEXT_VAR_ID: AtomicU64 = AtomicU64::new(1);
29static NEXT_SYM_ID: AtomicU64 = AtomicU64::new(1);
30static NEXT_CONSTRAINT_ID: AtomicU64 = AtomicU64::new(1);
31
32/// Coefficients below this magnitude are treated as zero.
33const NEAR_ZERO: f64 = 1.0e-8;
34
35fn near_zero(v: f64) -> bool {
36    v.abs() < NEAR_ZERO
37}
38
39// ── Private tableau types ───────────────────────────────────────────────────
40
41#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
42enum SymKind {
43    Invalid,
44    External,
45    Slack,
46    Error,
47    Dummy,
48}
49
50#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
51struct Sym(u64, SymKind);
52
53impl Sym {
54    fn invalid() -> Self {
55        Sym(0, SymKind::Invalid)
56    }
57
58    fn is_invalid(self) -> bool {
59        self.1 == SymKind::Invalid
60    }
61
62    fn is_external(self) -> bool {
63        self.1 == SymKind::External
64    }
65
66    fn is_error(self) -> bool {
67        self.1 == SymKind::Error
68    }
69
70    fn is_dummy(self) -> bool {
71        self.1 == SymKind::Dummy
72    }
73
74    fn is_pivotable(self) -> bool {
75        matches!(self.1, SymKind::Slack | SymKind::Error)
76    }
77}
78
79fn new_sym(kind: SymKind) -> Sym {
80    Sym(NEXT_SYM_ID.fetch_add(1, Ordering::Relaxed), kind)
81}
82
83// ── Tableau Row ─────────────────────────────────────────────────────────────
84
85/// A linear row: `key_symbol = constant + Σ cells[s] * s`.
86#[derive(Clone, Debug, Default)]
87struct Row {
88    cells: HashMap<Sym, f64>,
89    constant: f64,
90}
91
92impl Row {
93    fn new(constant: f64) -> Self {
94        Row {
95            cells: HashMap::new(),
96            constant,
97        }
98    }
99
100    /// Add `v` to the constant; return the new constant.
101    fn add_constant(&mut self, v: f64) -> f64 {
102        self.constant += v;
103        self.constant
104    }
105
106    fn insert_symbol(&mut self, s: Sym, coeff: f64) {
107        let entry = self.cells.entry(s).or_insert(0.0);
108        *entry += coeff;
109        if near_zero(*entry) {
110            self.cells.remove(&s);
111        }
112    }
113
114    fn insert_row(&mut self, other: &Row, coeff: f64) {
115        self.constant += other.constant * coeff;
116        for (&s, &c) in &other.cells {
117            let entry = self.cells.entry(s).or_insert(0.0);
118            *entry += c * coeff;
119            if near_zero(*entry) {
120                self.cells.remove(&s);
121            }
122        }
123    }
124
125    fn remove(&mut self, s: Sym) {
126        self.cells.remove(&s);
127    }
128
129    fn reverse_sign(&mut self) {
130        self.constant = -self.constant;
131        for v in self.cells.values_mut() {
132            *v = -*v;
133        }
134    }
135
136    /// Isolate `s` on the left-hand side: divide by `-coeff[s]`, remove `s`.
137    fn solve_for_symbol(&mut self, s: Sym) {
138        let c = self.cells.remove(&s).unwrap_or(1.0);
139        let factor = -1.0 / c;
140        self.constant *= factor;
141        for v in self.cells.values_mut() {
142            *v *= factor;
143        }
144    }
145
146    /// Pivot `lhs` (leaving) and `rhs` (entering).
147    ///
148    /// Precondition: `lhs` is NOT in `self.cells` (it is the implicit key).
149    /// Postcondition: `self` can be stored as `rows[rhs]`.
150    ///
151    /// Algorithm (matches cassowary-rs verbatim):
152    ///   1. Insert `lhs` with coefficient -1.
153    ///   2. Call `solve_for_symbol(rhs)`.
154    fn solve_for_symbols(&mut self, lhs: Sym, rhs: Sym) {
155        self.insert_symbol(lhs, -1.0);
156        self.solve_for_symbol(rhs);
157    }
158
159    fn coefficient_for(&self, s: Sym) -> f64 {
160        *self.cells.get(&s).unwrap_or(&0.0)
161    }
162
163    fn substitute(&mut self, s: Sym, row: &Row) {
164        if let Some(coeff) = self.cells.remove(&s) {
165            self.insert_row(row, coeff);
166        }
167    }
168}
169
170// ── Public API types ────────────────────────────────────────────────────────
171
172/// A layout variable whose value is determined by the constraint solver.
173#[derive(Clone, Debug)]
174pub struct Variable {
175    id: u64,
176}
177
178impl Variable {
179    /// Create a new unique variable.
180    pub fn new() -> Self {
181        Variable {
182            id: NEXT_VAR_ID.fetch_add(1, Ordering::Relaxed),
183        }
184    }
185}
186
187impl Default for Variable {
188    fn default() -> Self {
189        Self::new()
190    }
191}
192
193impl PartialEq for Variable {
194    fn eq(&self, other: &Self) -> bool {
195        self.id == other.id
196    }
197}
198impl Eq for Variable {}
199impl std::hash::Hash for Variable {
200    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
201        self.id.hash(state);
202    }
203}
204
205/// A variable multiplied by a coefficient.
206#[derive(Clone, Debug)]
207pub struct Term {
208    /// The variable.
209    pub variable: Variable,
210    /// The coefficient applied to the variable.
211    pub coefficient: f64,
212}
213
214/// A linear expression: `Σ coefficient·variable + constant`.
215#[derive(Clone, Debug, Default)]
216pub struct Expression {
217    /// Weighted variable terms.
218    pub terms: Vec<Term>,
219    /// Constant offset.
220    pub constant: f64,
221}
222
223impl Expression {
224    /// Construct from an explicit list of terms and a constant.
225    pub fn new(terms: Vec<Term>, constant: f64) -> Self {
226        Expression { terms, constant }
227    }
228
229    /// A constant expression with no variables.
230    pub fn from_constant(c: f64) -> Self {
231        Expression {
232            terms: Vec::new(),
233            constant: c,
234        }
235    }
236
237    /// An expression representing a single variable with coefficient `1.0`.
238    pub fn from_variable(v: Variable) -> Self {
239        Expression {
240            terms: vec![Term {
241                variable: v,
242                coefficient: 1.0,
243            }],
244            constant: 0.0,
245        }
246    }
247}
248
249/// Relational operator for a constraint.
250#[derive(Clone, Copy, Debug, PartialEq)]
251pub enum RelOp {
252    /// `lhs ≤ rhs`.
253    LessThanOrEq,
254    /// `lhs ≥ rhs`.
255    GreaterThanOrEq,
256    /// `lhs = rhs`.
257    Equal,
258}
259
260/// Predefined constraint strengths.
261pub struct Strength;
262
263impl Strength {
264    /// Required constraint — must be satisfied or the solver returns an error.
265    pub const REQUIRED: f64 = 1_001_001_000.0;
266    /// Strong soft constraint.
267    pub const STRONG: f64 = 1_000_000.0;
268    /// Medium soft constraint.
269    pub const MEDIUM: f64 = 1_000.0;
270    /// Weak soft constraint.
271    pub const WEAK: f64 = 1.0;
272
273    /// Clamp `s` to be at most [`Strength::REQUIRED`].
274    pub fn clip(s: f64) -> f64 {
275        s.min(Self::REQUIRED)
276    }
277}
278
279/// A linear constraint with a relational operator and a priority strength.
280#[derive(Clone, Debug)]
281pub struct Constraint {
282    expression: Expression,
283    op: RelOp,
284    strength: f64,
285    id: u64,
286}
287
288impl Constraint {
289    /// Create a new constraint.
290    pub fn new(expression: Expression, op: RelOp, strength: f64) -> Self {
291        let id = NEXT_CONSTRAINT_ID.fetch_add(1, Ordering::Relaxed);
292        Constraint {
293            expression,
294            op,
295            strength: Strength::clip(strength),
296            id,
297        }
298    }
299}
300
301impl PartialEq for Constraint {
302    fn eq(&self, o: &Self) -> bool {
303        self.id == o.id
304    }
305}
306impl Eq for Constraint {}
307impl std::hash::Hash for Constraint {
308    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
309        self.id.hash(state);
310    }
311}
312
313/// Errors produced by the solver.
314#[derive(Clone, Debug, PartialEq)]
315pub enum SolverError {
316    /// The constraint was already added.
317    DuplicateConstraint,
318    /// The constraint set is over-constrained (REQUIRED constraints conflict).
319    UnsatisfiableConstraint,
320    /// The constraint was not previously added.
321    UnknownConstraint,
322    /// The variable was not registered as an edit variable.
323    UnknownEditVariable,
324    /// An internal solver invariant was violated.
325    InternalError(String),
326}
327
328impl std::fmt::Display for SolverError {
329    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
330        match self {
331            SolverError::DuplicateConstraint => write!(f, "duplicate constraint"),
332            SolverError::UnsatisfiableConstraint => {
333                write!(f, "unsatisfiable constraint (over-constrained)")
334            }
335            SolverError::UnknownConstraint => write!(f, "unknown constraint"),
336            SolverError::UnknownEditVariable => write!(f, "unknown edit variable"),
337            SolverError::InternalError(s) => write!(f, "internal solver error: {s}"),
338        }
339    }
340}
341
342impl std::error::Error for SolverError {}
343
344// ── Internal bookkeeping ────────────────────────────────────────────────────
345
346struct Tag {
347    marker: Sym,
348    other: Sym,
349}
350
351struct EditInfo {
352    tag: Tag,
353    constant: f64,
354}
355
356// ── Solver ──────────────────────────────────────────────────────────────────
357
358/// The Cassowary incremental constraint solver.
359///
360/// Call [`update_variables`](Solver::update_variables) after all constraints
361/// and suggestions have been applied, then read values with
362/// [`value_of`](Solver::value_of).
363pub struct Solver {
364    rows: HashMap<Sym, Row>,
365    /// Variable id → its External symbol.
366    vars: HashMap<u64, Sym>,
367    /// Constraint id → tag.
368    constraints: HashMap<u64, Tag>,
369    /// Edit variable id → bookkeeping.
370    edits: HashMap<u64, EditInfo>,
371    /// Restricted basic variables with a negative constant.
372    infeasible_rows: Vec<Sym>,
373    /// Objective row: minimise weighted sum of error symbols.
374    objective: Row,
375    /// Cached variable values (populated by `update_variables`).
376    values: HashMap<u64, f64>,
377}
378
379impl Solver {
380    /// Create a new empty solver.
381    pub fn new() -> Self {
382        Solver {
383            rows: HashMap::new(),
384            vars: HashMap::new(),
385            constraints: HashMap::new(),
386            edits: HashMap::new(),
387            infeasible_rows: Vec::new(),
388            objective: Row::new(0.0),
389            values: HashMap::new(),
390        }
391    }
392
393    // ── Public API ────────────────────────────────────────────────────────
394
395    /// Add a constraint to the solver.
396    pub fn add_constraint(&mut self, constraint: Constraint) -> Result<(), SolverError> {
397        if self.constraints.contains_key(&constraint.id) {
398            return Err(SolverError::DuplicateConstraint);
399        }
400
401        let (mut row, tag) = self.create_row(&constraint);
402        let subject = Self::choose_subject(&row, &tag);
403
404        if subject.is_invalid() {
405            // All-dummy row: linearly dependent REQUIRED constraint.
406            if Self::all_dummies(&row) && !near_zero(row.constant) {
407                return Err(SolverError::UnsatisfiableConstraint);
408            }
409            // Use an artificial variable for phase-1 feasibility.
410            if !self.add_with_artificial(&row)? {
411                return Err(SolverError::UnsatisfiableConstraint);
412            }
413        } else {
414            row.solve_for_symbol(subject);
415            self.substitute_all(subject, &row);
416            self.rows.insert(subject, row);
417        }
418
419        self.constraints.insert(constraint.id, tag);
420        self.optimize()?;
421        Ok(())
422    }
423
424    /// Remove a previously added constraint.
425    pub fn remove_constraint(&mut self, constraint: &Constraint) -> Result<(), SolverError> {
426        let tag = self
427            .constraints
428            .remove(&constraint.id)
429            .ok_or(SolverError::UnknownConstraint)?;
430
431        self.remove_constraint_effects(&tag, constraint.strength);
432
433        // If marker is basic, just remove that row.
434        if self.rows.remove(&tag.marker).is_none() {
435            // Marker is non-basic: pivot it into the basis, then remove.
436            let (leaving, mut row) = self
437                .get_marker_leaving_row(tag.marker)
438                .ok_or_else(|| SolverError::InternalError("no leaving row for marker".into()))?;
439            row.solve_for_symbols(leaving, tag.marker);
440            self.substitute_all(tag.marker, &row);
441            // Now marker IS basic — remove it.
442            self.rows.remove(&tag.marker);
443        }
444
445        self.optimize()?;
446        Ok(())
447    }
448
449    /// Register `variable` as an edit variable at the given `strength`.
450    ///
451    /// Must be called before [`Solver::suggest_value`]. Strength must not be
452    /// [`Strength::REQUIRED`].
453    pub fn add_edit_variable(
454        &mut self,
455        variable: &Variable,
456        strength: f64,
457    ) -> Result<(), SolverError> {
458        if self.edits.contains_key(&variable.id) {
459            return Err(SolverError::DuplicateConstraint);
460        }
461        let strength = Strength::clip(strength);
462        if (strength - Strength::REQUIRED).abs() < NEAR_ZERO {
463            return Err(SolverError::InternalError(
464                "edit variables cannot be REQUIRED".into(),
465            ));
466        }
467
468        let expr = Expression::from_variable(variable.clone());
469        let c = Constraint::new(expr, RelOp::Equal, strength);
470        self.add_constraint(c.clone())?;
471
472        let tag = self
473            .constraints
474            .get(&c.id)
475            .ok_or_else(|| SolverError::InternalError("tag not found after add".into()))?;
476        let tag = Tag {
477            marker: tag.marker,
478            other: tag.other,
479        };
480        self.edits
481            .insert(variable.id, EditInfo { tag, constant: 0.0 });
482        Ok(())
483    }
484
485    /// Suggest a value for a registered edit variable.
486    pub fn suggest_value(&mut self, variable: &Variable, value: f64) -> Result<(), SolverError> {
487        let (delta, marker, other) = {
488            let info = self
489                .edits
490                .get_mut(&variable.id)
491                .ok_or(SolverError::UnknownEditVariable)?;
492            let delta = value - info.constant;
493            info.constant = value;
494            (delta, info.tag.marker, info.tag.other)
495        };
496
497        // Mirror cassowary-rs's three-case suggest logic.
498        // Case 1: marker is basic.
499        if let Some(row) = self.rows.get_mut(&marker) {
500            if row.add_constant(-delta) < 0.0 {
501                self.infeasible_rows.push(marker);
502            }
503            return self.dual_optimize();
504        }
505        // Case 2: other is basic.
506        if !other.is_invalid() {
507            if let Some(row) = self.rows.get_mut(&other) {
508                if row.add_constant(delta) < 0.0 {
509                    self.infeasible_rows.push(other);
510                }
511                return self.dual_optimize();
512            }
513        }
514        // Case 3: neither basic — adjust all rows by coefficient.
515        let keys: Vec<Sym> = self.rows.keys().cloned().collect();
516        for sym in keys {
517            let row = self.rows.get_mut(&sym).expect("row present");
518            let coeff = row.coefficient_for(marker);
519            let diff = delta * coeff;
520            if !near_zero(diff) {
521                row.add_constant(diff);
522                if !sym.is_external() && row.constant < 0.0 {
523                    self.infeasible_rows.push(sym);
524                }
525            }
526        }
527        self.dual_optimize()
528    }
529
530    /// Populate cached variable values.
531    ///
532    /// Call after all constraints and suggestions have been applied, then
533    /// read values with [`Solver::value_of`].
534    pub fn update_variables(&mut self) {
535        let pairs: Vec<(u64, Sym)> = self.vars.iter().map(|(&id, &s)| (id, s)).collect();
536        for (var_id, sym) in pairs {
537            let v = self.rows.get(&sym).map(|r| r.constant).unwrap_or(0.0);
538            self.values.insert(var_id, v);
539        }
540    }
541
542    /// Return the last computed value for `variable` (0.0 if not yet solved).
543    pub fn value_of(&self, variable: &Variable) -> f64 {
544        *self.values.get(&variable.id).unwrap_or(&0.0)
545    }
546
547    /// Reset the solver to the empty starting state.
548    pub fn reset(&mut self) {
549        self.rows.clear();
550        self.vars.clear();
551        self.constraints.clear();
552        self.edits.clear();
553        self.infeasible_rows.clear();
554        self.objective = Row::new(0.0);
555        self.values.clear();
556    }
557
558    // ── Internal helpers ──────────────────────────────────────────────────
559
560    fn get_var_sym(&mut self, variable: &Variable) -> Sym {
561        *self
562            .vars
563            .entry(variable.id)
564            .or_insert_with(|| new_sym(SymKind::External))
565    }
566
567    fn create_row(&mut self, constraint: &Constraint) -> (Row, Tag) {
568        let expr = &constraint.expression;
569        let mut row = Row::new(expr.constant);
570        for term in &expr.terms {
571            if !near_zero(term.coefficient) {
572                let sym = self.get_var_sym(&term.variable);
573                if let Some(basic) = self.rows.get(&sym).cloned() {
574                    row.insert_row(&basic, term.coefficient);
575                } else {
576                    row.insert_symbol(sym, term.coefficient);
577                }
578            }
579        }
580
581        let is_req = (constraint.strength - Strength::REQUIRED).abs() < NEAR_ZERO;
582
583        let tag = match constraint.op {
584            RelOp::GreaterThanOrEq | RelOp::LessThanOrEq => {
585                let coeff = if constraint.op == RelOp::LessThanOrEq {
586                    1.0
587                } else {
588                    -1.0
589                };
590                let slack = new_sym(SymKind::Slack);
591                row.insert_symbol(slack, coeff);
592                if !is_req {
593                    let error = new_sym(SymKind::Error);
594                    row.insert_symbol(error, -coeff);
595                    self.objective.insert_symbol(error, constraint.strength);
596                    Tag {
597                        marker: slack,
598                        other: error,
599                    }
600                } else {
601                    Tag {
602                        marker: slack,
603                        other: Sym::invalid(),
604                    }
605                }
606            }
607            RelOp::Equal => {
608                if is_req {
609                    let dummy = new_sym(SymKind::Dummy);
610                    row.insert_symbol(dummy, 1.0);
611                    Tag {
612                        marker: dummy,
613                        other: Sym::invalid(),
614                    }
615                } else {
616                    let ep = new_sym(SymKind::Error);
617                    let em = new_sym(SymKind::Error);
618                    row.insert_symbol(ep, -1.0);
619                    row.insert_symbol(em, 1.0);
620                    self.objective.insert_symbol(ep, constraint.strength);
621                    self.objective.insert_symbol(em, constraint.strength);
622                    Tag {
623                        marker: ep,
624                        other: em,
625                    }
626                }
627            }
628        };
629
630        if row.constant < 0.0 {
631            row.reverse_sign();
632        }
633        (row, tag)
634    }
635
636    fn choose_subject(row: &Row, tag: &Tag) -> Sym {
637        for &sym in row.cells.keys() {
638            if sym.is_external() {
639                return sym;
640            }
641        }
642        if tag.marker.is_pivotable() && row.coefficient_for(tag.marker) < 0.0 {
643            return tag.marker;
644        }
645        if !tag.other.is_invalid()
646            && tag.other.is_pivotable()
647            && row.coefficient_for(tag.other) < 0.0
648        {
649            return tag.other;
650        }
651        Sym::invalid()
652    }
653
654    fn all_dummies(row: &Row) -> bool {
655        row.cells.keys().all(|s| s.is_dummy())
656    }
657
658    fn add_with_artificial(&mut self, row: &Row) -> Result<bool, SolverError> {
659        let art = new_sym(SymKind::Slack);
660        // The artificial objective row is a copy of the constraint row.
661        let mut art_obj = row.clone();
662        self.rows.insert(art, row.clone());
663
664        // Optimise the artificial objective.
665        self.optimize_row(&mut art_obj)?;
666
667        let success = near_zero(art_obj.constant);
668
669        // If art is still in the basis, pivot it out.
670        if let Some(mut art_row) = self.rows.remove(&art) {
671            if !art_row.cells.is_empty() {
672                let entering = Self::any_pivotable_sym(&art_row);
673                if !entering.is_invalid() {
674                    art_row.solve_for_symbols(art, entering);
675                    self.substitute_all(entering, &art_row);
676                    self.rows.insert(entering, art_row);
677                }
678            }
679        }
680
681        // Remove art from all remaining rows and objective.
682        for row in self.rows.values_mut() {
683            row.remove(art);
684        }
685        self.objective.remove(art);
686
687        Ok(success)
688    }
689
690    fn any_pivotable_sym(row: &Row) -> Sym {
691        row.cells
692            .keys()
693            .find(|&&s| s.is_pivotable())
694            .cloned()
695            .unwrap_or_else(Sym::invalid)
696    }
697
698    fn substitute_all(&mut self, sym: Sym, row: &Row) {
699        let keys: Vec<Sym> = self.rows.keys().cloned().collect();
700        for key in keys {
701            let r = self.rows.get_mut(&key).expect("row present");
702            r.substitute(sym, row);
703            if !key.is_external() && r.constant < 0.0 {
704                self.infeasible_rows.push(key);
705            }
706        }
707        self.objective.substitute(sym, row);
708    }
709
710    fn optimize(&mut self) -> Result<(), SolverError> {
711        loop {
712            let entering = Self::get_entering_sym(&self.objective);
713            if entering.is_invalid() {
714                return Ok(());
715            }
716            let (leaving, mut row) = self
717                .get_leaving_row(entering)
718                .ok_or_else(|| SolverError::InternalError("objective unbounded".into()))?;
719            row.solve_for_symbols(leaving, entering);
720            self.substitute_all(entering, &row);
721            self.rows.insert(entering, row);
722        }
723    }
724
725    fn optimize_row(&mut self, obj: &mut Row) -> Result<(), SolverError> {
726        loop {
727            let entering = Self::get_entering_sym(obj);
728            if entering.is_invalid() {
729                return Ok(());
730            }
731            let (leaving, mut row) = self
732                .get_leaving_row(entering)
733                .ok_or_else(|| SolverError::InternalError("art objective unbounded".into()))?;
734            row.solve_for_symbols(leaving, entering);
735            self.substitute_all(entering, &row);
736            obj.substitute(entering, &row);
737            self.rows.insert(entering, row);
738        }
739    }
740
741    fn dual_optimize(&mut self) -> Result<(), SolverError> {
742        while let Some(leaving) = self.infeasible_rows.pop() {
743            // Only process if the row is still infeasible.
744            let constant = match self.rows.get(&leaving) {
745                Some(r) => r.constant,
746                None => continue,
747            };
748            if constant >= 0.0 {
749                continue;
750            }
751
752            // Get entering: symbol with positive coeff in the infeasible row
753            // and minimum ratio of obj_coeff / row_coeff.
754            let entering = {
755                let row = self.rows.get(&leaving).expect("infeasible row");
756                let mut best = Sym::invalid();
757                let mut ratio = f64::INFINITY;
758                for (&sym, &coeff) in &row.cells {
759                    if coeff > 0.0 && !sym.is_dummy() {
760                        let obj_c = self.objective.coefficient_for(sym);
761                        let r = obj_c / coeff;
762                        if r < ratio {
763                            ratio = r;
764                            best = sym;
765                        }
766                    }
767                }
768                best
769            };
770
771            if entering.is_invalid() {
772                return Err(SolverError::InternalError("dual optimize failed".into()));
773            }
774
775            let mut row = self
776                .rows
777                .remove(&leaving)
778                .ok_or_else(|| SolverError::InternalError("leaving row missing".into()))?;
779            row.solve_for_symbols(leaving, entering);
780            self.substitute_all(entering, &row);
781            self.rows.insert(entering, row);
782        }
783        Ok(())
784    }
785
786    fn get_entering_sym(obj: &Row) -> Sym {
787        for (&sym, &coeff) in &obj.cells {
788            if !sym.is_dummy() && coeff < 0.0 {
789                return sym;
790            }
791        }
792        Sym::invalid()
793    }
794
795    fn get_leaving_row(&mut self, entering: Sym) -> Option<(Sym, Row)> {
796        let mut ratio = f64::INFINITY;
797        let mut found = None;
798        for (&sym, row) in &self.rows {
799            if sym.is_external() {
800                continue;
801            }
802            let c = row.coefficient_for(entering);
803            if c < 0.0 {
804                let r = -row.constant / c;
805                if r < ratio {
806                    ratio = r;
807                    found = Some(sym);
808                }
809            }
810        }
811        found.map(|s| (s, self.rows.remove(&s).expect("row present")))
812    }
813
814    fn get_marker_leaving_row(&mut self, marker: Sym) -> Option<(Sym, Row)> {
815        let mut r1 = f64::INFINITY;
816        let mut r2 = f64::INFINITY;
817        let mut first: Option<Sym> = None;
818        let mut second: Option<Sym> = None;
819        let mut third: Option<Sym> = None;
820
821        for (&sym, row) in &self.rows {
822            let c = row.coefficient_for(marker);
823            if near_zero(c) {
824                continue;
825            }
826            if sym.is_external() {
827                third = Some(sym);
828            } else if c < 0.0 {
829                let r = -row.constant / c;
830                if r < r1 {
831                    r1 = r;
832                    first = Some(sym);
833                }
834            } else {
835                let r = row.constant / c;
836                if r < r2 {
837                    r2 = r;
838                    second = Some(sym);
839                }
840            }
841        }
842
843        first
844            .or(second)
845            .or(third)
846            .map(|s| (s, self.rows.remove(&s).expect("row present")))
847    }
848
849    fn remove_constraint_effects(&mut self, tag: &Tag, strength: f64) {
850        if tag.marker.is_error() {
851            self.remove_marker_effects(tag.marker, strength);
852        } else if tag.other.is_error() {
853            self.remove_marker_effects(tag.other, strength);
854        }
855    }
856
857    fn remove_marker_effects(&mut self, marker: Sym, strength: f64) {
858        if let Some(row) = self.rows.get(&marker).cloned() {
859            self.objective.insert_row(&row, -strength);
860        } else {
861            self.objective.insert_symbol(marker, -strength);
862        }
863    }
864}
865
866impl Default for Solver {
867    fn default() -> Self {
868        Self::new()
869    }
870}
871
872// ── Tests ────────────────────────────────────────────────────────────────────
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877
878    fn approx(a: f64, b: f64) -> bool {
879        (a - b).abs() < 1e-4
880    }
881
882    /// 1. Two REQUIRED equality constraints: x == 10, y == x + 5  →  x=10, y=15.
883    #[test]
884    fn two_equality_constraints() {
885        let mut s = Solver::new();
886        let x = Variable::new();
887        let y = Variable::new();
888
889        // x - 10 == 0
890        s.add_constraint(Constraint::new(
891            Expression::new(
892                vec![Term {
893                    variable: x.clone(),
894                    coefficient: 1.0,
895                }],
896                -10.0,
897            ),
898            RelOp::Equal,
899            Strength::REQUIRED,
900        ))
901        .unwrap();
902        // y - x - 5 == 0
903        s.add_constraint(Constraint::new(
904            Expression::new(
905                vec![
906                    Term {
907                        variable: y.clone(),
908                        coefficient: 1.0,
909                    },
910                    Term {
911                        variable: x.clone(),
912                        coefficient: -1.0,
913                    },
914                ],
915                -5.0,
916            ),
917            RelOp::Equal,
918            Strength::REQUIRED,
919        ))
920        .unwrap();
921
922        s.update_variables();
923        assert!(approx(s.value_of(&x), 10.0), "x={}", s.value_of(&x));
924        assert!(approx(s.value_of(&y), 15.0), "y={}", s.value_of(&y));
925    }
926
927    /// 2. suggest_value and update_variables: edit x toward 30.
928    #[test]
929    fn suggest_value_and_update() {
930        let mut s = Solver::new();
931        let x = Variable::new();
932
933        // Weak stay: x == 0.
934        s.add_constraint(Constraint::new(
935            Expression::from_variable(x.clone()),
936            RelOp::Equal,
937            Strength::WEAK,
938        ))
939        .unwrap();
940
941        s.add_edit_variable(&x, Strength::STRONG).unwrap();
942        s.suggest_value(&x, 30.0).unwrap();
943        s.update_variables();
944
945        assert!(approx(s.value_of(&x), 30.0), "x={}", s.value_of(&x));
946    }
947
948    /// 3. Over-constrained REQUIRED → UnsatisfiableConstraint.
949    #[test]
950    fn over_constrained_required() {
951        let mut s = Solver::new();
952        let x = Variable::new();
953
954        s.add_constraint(Constraint::new(
955            Expression::new(
956                vec![Term {
957                    variable: x.clone(),
958                    coefficient: 1.0,
959                }],
960                -10.0,
961            ),
962            RelOp::Equal,
963            Strength::REQUIRED,
964        ))
965        .unwrap();
966
967        let result = s.add_constraint(Constraint::new(
968            Expression::new(
969                vec![Term {
970                    variable: x.clone(),
971                    coefficient: 1.0,
972                }],
973                -20.0,
974            ),
975            RelOp::Equal,
976            Strength::REQUIRED,
977        ));
978        assert_eq!(result, Err(SolverError::UnsatisfiableConstraint));
979    }
980
981    /// 4. Proportional: a == 2*b, a + b == 300  →  a=200, b=100.
982    #[test]
983    fn proportional_constraints() {
984        let mut s = Solver::new();
985        let a = Variable::new();
986        let b = Variable::new();
987
988        // a - 2*b == 0
989        s.add_constraint(Constraint::new(
990            Expression::new(
991                vec![
992                    Term {
993                        variable: a.clone(),
994                        coefficient: 1.0,
995                    },
996                    Term {
997                        variable: b.clone(),
998                        coefficient: -2.0,
999                    },
1000                ],
1001                0.0,
1002            ),
1003            RelOp::Equal,
1004            Strength::REQUIRED,
1005        ))
1006        .unwrap();
1007        // a + b - 300 == 0
1008        s.add_constraint(Constraint::new(
1009            Expression::new(
1010                vec![
1011                    Term {
1012                        variable: a.clone(),
1013                        coefficient: 1.0,
1014                    },
1015                    Term {
1016                        variable: b.clone(),
1017                        coefficient: 1.0,
1018                    },
1019                ],
1020                -300.0,
1021            ),
1022            RelOp::Equal,
1023            Strength::REQUIRED,
1024        ))
1025        .unwrap();
1026
1027        s.update_variables();
1028        assert!(approx(s.value_of(&a), 200.0), "a={}", s.value_of(&a));
1029        assert!(approx(s.value_of(&b), 100.0), "b={}", s.value_of(&b));
1030    }
1031
1032    /// 5. Anchoring: 50 <= x <= 200, suggest x=75 → value in [50,200] ≈ 75.
1033    #[test]
1034    fn anchoring_with_bounds() {
1035        let mut s = Solver::new();
1036        let x = Variable::new();
1037
1038        // x - 50 >= 0
1039        s.add_constraint(Constraint::new(
1040            Expression::new(
1041                vec![Term {
1042                    variable: x.clone(),
1043                    coefficient: 1.0,
1044                }],
1045                -50.0,
1046            ),
1047            RelOp::GreaterThanOrEq,
1048            Strength::REQUIRED,
1049        ))
1050        .unwrap();
1051        // x - 200 <= 0
1052        s.add_constraint(Constraint::new(
1053            Expression::new(
1054                vec![Term {
1055                    variable: x.clone(),
1056                    coefficient: 1.0,
1057                }],
1058                -200.0,
1059            ),
1060            RelOp::LessThanOrEq,
1061            Strength::REQUIRED,
1062        ))
1063        .unwrap();
1064
1065        s.add_edit_variable(&x, Strength::STRONG).unwrap();
1066        s.suggest_value(&x, 75.0).unwrap();
1067        s.update_variables();
1068
1069        let v = s.value_of(&x);
1070        assert!(v >= 50.0 - 1e-4, "x={v} < 50");
1071        assert!(v <= 200.0 + 1e-4, "x={v} > 200");
1072        assert!(approx(v, 75.0), "x={v} ≠ 75");
1073    }
1074
1075    /// 6. Remove constraint then re-solve.
1076    #[test]
1077    fn remove_constraint_and_resolv() {
1078        let mut s = Solver::new();
1079        let x = Variable::new();
1080
1081        let c1 = Constraint::new(
1082            Expression::new(
1083                vec![Term {
1084                    variable: x.clone(),
1085                    coefficient: 1.0,
1086                }],
1087                -10.0,
1088            ),
1089            RelOp::Equal,
1090            Strength::REQUIRED,
1091        );
1092        let c2 = Constraint::new(
1093            Expression::new(
1094                vec![Term {
1095                    variable: x.clone(),
1096                    coefficient: 1.0,
1097                }],
1098                -20.0,
1099            ),
1100            RelOp::Equal,
1101            Strength::REQUIRED,
1102        );
1103
1104        s.add_constraint(c1.clone()).unwrap();
1105        s.update_variables();
1106        assert!(
1107            approx(s.value_of(&x), 10.0),
1108            "before remove: x={}",
1109            s.value_of(&x)
1110        );
1111
1112        s.remove_constraint(&c1).unwrap();
1113        s.add_constraint(c2).unwrap();
1114        s.update_variables();
1115        assert!(
1116            approx(s.value_of(&x), 20.0),
1117            "after remove: x={}",
1118            s.value_of(&x)
1119        );
1120    }
1121
1122    /// 7. Strength ordering: REQUIRED beats STRONG.
1123    #[test]
1124    fn strength_ordering() {
1125        let mut s = Solver::new();
1126        let x = Variable::new();
1127
1128        // REQUIRED: x == 100
1129        s.add_constraint(Constraint::new(
1130            Expression::new(
1131                vec![Term {
1132                    variable: x.clone(),
1133                    coefficient: 1.0,
1134                }],
1135                -100.0,
1136            ),
1137            RelOp::Equal,
1138            Strength::REQUIRED,
1139        ))
1140        .unwrap();
1141        // STRONG: x == 200 (will lose to REQUIRED)
1142        s.add_constraint(Constraint::new(
1143            Expression::new(
1144                vec![Term {
1145                    variable: x.clone(),
1146                    coefficient: 1.0,
1147                }],
1148                -200.0,
1149            ),
1150            RelOp::Equal,
1151            Strength::STRONG,
1152        ))
1153        .unwrap();
1154
1155        s.update_variables();
1156        assert!(approx(s.value_of(&x), 100.0), "x={}", s.value_of(&x));
1157    }
1158
1159    /// 8. Reset: after reset, add x==5 and check value.
1160    #[test]
1161    fn reset_and_readd() {
1162        let mut s = Solver::new();
1163        let x = Variable::new();
1164
1165        s.add_constraint(Constraint::new(
1166            Expression::new(
1167                vec![Term {
1168                    variable: x.clone(),
1169                    coefficient: 1.0,
1170                }],
1171                -99.0,
1172            ),
1173            RelOp::Equal,
1174            Strength::REQUIRED,
1175        ))
1176        .unwrap();
1177        s.update_variables();
1178        assert!(approx(s.value_of(&x), 99.0));
1179
1180        s.reset();
1181
1182        s.add_constraint(Constraint::new(
1183            Expression::new(
1184                vec![Term {
1185                    variable: x.clone(),
1186                    coefficient: 1.0,
1187                }],
1188                -5.0,
1189            ),
1190            RelOp::Equal,
1191            Strength::REQUIRED,
1192        ))
1193        .unwrap();
1194        s.update_variables();
1195        assert!(approx(s.value_of(&x), 5.0), "x={}", s.value_of(&x));
1196    }
1197
1198    // ── Property-based tests ─────────────────────────────────────────────
1199
1200    use proptest::prelude::*;
1201
1202    proptest! {
1203        #[test]
1204        fn no_panic_random_constraints(
1205            vals in prop::collection::vec(
1206                (0.0f64..100.0, 0.0f64..100.0), 1..10
1207            )
1208        ) {
1209            let mut s = Solver::new();
1210            let x = Variable::new();
1211            for (lo, hi) in &vals {
1212                let lo = lo.min(*hi);
1213                let hi = hi.max(lo);
1214                let _ = s.add_constraint(Constraint::new(
1215                    Expression::new(
1216                        vec![Term { variable: x.clone(), coefficient: 1.0 }],
1217                        -lo,
1218                    ),
1219                    RelOp::GreaterThanOrEq,
1220                    Strength::WEAK,
1221                ));
1222                let _ = s.add_constraint(Constraint::new(
1223                    Expression::new(
1224                        vec![Term { variable: x.clone(), coefficient: 1.0 }],
1225                        -hi,
1226                    ),
1227                    RelOp::LessThanOrEq,
1228                    Strength::WEAK,
1229                ));
1230            }
1231            s.update_variables();
1232        }
1233    }
1234}