use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT_VAR_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_SYM_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_CONSTRAINT_ID: AtomicU64 = AtomicU64::new(1);
const NEAR_ZERO: f64 = 1.0e-8;
fn near_zero(v: f64) -> bool {
v.abs() < NEAR_ZERO
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum SymKind {
Invalid,
External,
Slack,
Error,
Dummy,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
struct Sym(u64, SymKind);
impl Sym {
fn invalid() -> Self {
Sym(0, SymKind::Invalid)
}
fn is_invalid(self) -> bool {
self.1 == SymKind::Invalid
}
fn is_external(self) -> bool {
self.1 == SymKind::External
}
fn is_error(self) -> bool {
self.1 == SymKind::Error
}
fn is_dummy(self) -> bool {
self.1 == SymKind::Dummy
}
fn is_pivotable(self) -> bool {
matches!(self.1, SymKind::Slack | SymKind::Error)
}
}
fn new_sym(kind: SymKind) -> Sym {
Sym(NEXT_SYM_ID.fetch_add(1, Ordering::Relaxed), kind)
}
#[derive(Clone, Debug, Default)]
struct Row {
cells: HashMap<Sym, f64>,
constant: f64,
}
impl Row {
fn new(constant: f64) -> Self {
Row {
cells: HashMap::new(),
constant,
}
}
fn add_constant(&mut self, v: f64) -> f64 {
self.constant += v;
self.constant
}
fn insert_symbol(&mut self, s: Sym, coeff: f64) {
let entry = self.cells.entry(s).or_insert(0.0);
*entry += coeff;
if near_zero(*entry) {
self.cells.remove(&s);
}
}
fn insert_row(&mut self, other: &Row, coeff: f64) {
self.constant += other.constant * coeff;
for (&s, &c) in &other.cells {
let entry = self.cells.entry(s).or_insert(0.0);
*entry += c * coeff;
if near_zero(*entry) {
self.cells.remove(&s);
}
}
}
fn remove(&mut self, s: Sym) {
self.cells.remove(&s);
}
fn reverse_sign(&mut self) {
self.constant = -self.constant;
for v in self.cells.values_mut() {
*v = -*v;
}
}
fn solve_for_symbol(&mut self, s: Sym) {
let c = self.cells.remove(&s).unwrap_or(1.0);
let factor = -1.0 / c;
self.constant *= factor;
for v in self.cells.values_mut() {
*v *= factor;
}
}
fn solve_for_symbols(&mut self, lhs: Sym, rhs: Sym) {
self.insert_symbol(lhs, -1.0);
self.solve_for_symbol(rhs);
}
fn coefficient_for(&self, s: Sym) -> f64 {
*self.cells.get(&s).unwrap_or(&0.0)
}
fn substitute(&mut self, s: Sym, row: &Row) {
if let Some(coeff) = self.cells.remove(&s) {
self.insert_row(row, coeff);
}
}
}
#[derive(Clone, Debug)]
pub struct Variable {
id: u64,
}
impl Variable {
pub fn new() -> Self {
Variable {
id: NEXT_VAR_ID.fetch_add(1, Ordering::Relaxed),
}
}
}
impl Default for Variable {
fn default() -> Self {
Self::new()
}
}
impl PartialEq for Variable {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for Variable {}
impl std::hash::Hash for Variable {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Clone, Debug)]
pub struct Term {
pub variable: Variable,
pub coefficient: f64,
}
#[derive(Clone, Debug, Default)]
pub struct Expression {
pub terms: Vec<Term>,
pub constant: f64,
}
impl Expression {
pub fn new(terms: Vec<Term>, constant: f64) -> Self {
Expression { terms, constant }
}
pub fn from_constant(c: f64) -> Self {
Expression {
terms: Vec::new(),
constant: c,
}
}
pub fn from_variable(v: Variable) -> Self {
Expression {
terms: vec![Term {
variable: v,
coefficient: 1.0,
}],
constant: 0.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum RelOp {
LessThanOrEq,
GreaterThanOrEq,
Equal,
}
pub struct Strength;
impl Strength {
pub const REQUIRED: f64 = 1_001_001_000.0;
pub const STRONG: f64 = 1_000_000.0;
pub const MEDIUM: f64 = 1_000.0;
pub const WEAK: f64 = 1.0;
pub fn clip(s: f64) -> f64 {
s.min(Self::REQUIRED)
}
}
#[derive(Clone, Debug)]
pub struct Constraint {
expression: Expression,
op: RelOp,
strength: f64,
id: u64,
}
impl Constraint {
pub fn new(expression: Expression, op: RelOp, strength: f64) -> Self {
let id = NEXT_CONSTRAINT_ID.fetch_add(1, Ordering::Relaxed);
Constraint {
expression,
op,
strength: Strength::clip(strength),
id,
}
}
}
impl PartialEq for Constraint {
fn eq(&self, o: &Self) -> bool {
self.id == o.id
}
}
impl Eq for Constraint {}
impl std::hash::Hash for Constraint {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum SolverError {
DuplicateConstraint,
UnsatisfiableConstraint,
UnknownConstraint,
UnknownEditVariable,
InternalError(String),
}
impl std::fmt::Display for SolverError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SolverError::DuplicateConstraint => write!(f, "duplicate constraint"),
SolverError::UnsatisfiableConstraint => {
write!(f, "unsatisfiable constraint (over-constrained)")
}
SolverError::UnknownConstraint => write!(f, "unknown constraint"),
SolverError::UnknownEditVariable => write!(f, "unknown edit variable"),
SolverError::InternalError(s) => write!(f, "internal solver error: {s}"),
}
}
}
impl std::error::Error for SolverError {}
struct Tag {
marker: Sym,
other: Sym,
}
struct EditInfo {
tag: Tag,
constant: f64,
}
pub struct Solver {
rows: HashMap<Sym, Row>,
vars: HashMap<u64, Sym>,
constraints: HashMap<u64, Tag>,
edits: HashMap<u64, EditInfo>,
infeasible_rows: Vec<Sym>,
objective: Row,
values: HashMap<u64, f64>,
}
impl Solver {
pub fn new() -> Self {
Solver {
rows: HashMap::new(),
vars: HashMap::new(),
constraints: HashMap::new(),
edits: HashMap::new(),
infeasible_rows: Vec::new(),
objective: Row::new(0.0),
values: HashMap::new(),
}
}
pub fn add_constraint(&mut self, constraint: Constraint) -> Result<(), SolverError> {
if self.constraints.contains_key(&constraint.id) {
return Err(SolverError::DuplicateConstraint);
}
let (mut row, tag) = self.create_row(&constraint);
let subject = Self::choose_subject(&row, &tag);
if subject.is_invalid() {
if Self::all_dummies(&row) && !near_zero(row.constant) {
return Err(SolverError::UnsatisfiableConstraint);
}
if !self.add_with_artificial(&row)? {
return Err(SolverError::UnsatisfiableConstraint);
}
} else {
row.solve_for_symbol(subject);
self.substitute_all(subject, &row);
self.rows.insert(subject, row);
}
self.constraints.insert(constraint.id, tag);
self.optimize()?;
Ok(())
}
pub fn remove_constraint(&mut self, constraint: &Constraint) -> Result<(), SolverError> {
let tag = self
.constraints
.remove(&constraint.id)
.ok_or(SolverError::UnknownConstraint)?;
self.remove_constraint_effects(&tag, constraint.strength);
if self.rows.remove(&tag.marker).is_none() {
let (leaving, mut row) = self
.get_marker_leaving_row(tag.marker)
.ok_or_else(|| SolverError::InternalError("no leaving row for marker".into()))?;
row.solve_for_symbols(leaving, tag.marker);
self.substitute_all(tag.marker, &row);
self.rows.remove(&tag.marker);
}
self.optimize()?;
Ok(())
}
pub fn add_edit_variable(
&mut self,
variable: &Variable,
strength: f64,
) -> Result<(), SolverError> {
if self.edits.contains_key(&variable.id) {
return Err(SolverError::DuplicateConstraint);
}
let strength = Strength::clip(strength);
if (strength - Strength::REQUIRED).abs() < NEAR_ZERO {
return Err(SolverError::InternalError(
"edit variables cannot be REQUIRED".into(),
));
}
let expr = Expression::from_variable(variable.clone());
let c = Constraint::new(expr, RelOp::Equal, strength);
self.add_constraint(c.clone())?;
let tag = self
.constraints
.get(&c.id)
.ok_or_else(|| SolverError::InternalError("tag not found after add".into()))?;
let tag = Tag {
marker: tag.marker,
other: tag.other,
};
self.edits
.insert(variable.id, EditInfo { tag, constant: 0.0 });
Ok(())
}
pub fn suggest_value(&mut self, variable: &Variable, value: f64) -> Result<(), SolverError> {
let (delta, marker, other) = {
let info = self
.edits
.get_mut(&variable.id)
.ok_or(SolverError::UnknownEditVariable)?;
let delta = value - info.constant;
info.constant = value;
(delta, info.tag.marker, info.tag.other)
};
if let Some(row) = self.rows.get_mut(&marker) {
if row.add_constant(-delta) < 0.0 {
self.infeasible_rows.push(marker);
}
return self.dual_optimize();
}
if !other.is_invalid() {
if let Some(row) = self.rows.get_mut(&other) {
if row.add_constant(delta) < 0.0 {
self.infeasible_rows.push(other);
}
return self.dual_optimize();
}
}
let keys: Vec<Sym> = self.rows.keys().cloned().collect();
for sym in keys {
let row = self.rows.get_mut(&sym).expect("row present");
let coeff = row.coefficient_for(marker);
let diff = delta * coeff;
if !near_zero(diff) {
row.add_constant(diff);
if !sym.is_external() && row.constant < 0.0 {
self.infeasible_rows.push(sym);
}
}
}
self.dual_optimize()
}
pub fn update_variables(&mut self) {
let pairs: Vec<(u64, Sym)> = self.vars.iter().map(|(&id, &s)| (id, s)).collect();
for (var_id, sym) in pairs {
let v = self.rows.get(&sym).map(|r| r.constant).unwrap_or(0.0);
self.values.insert(var_id, v);
}
}
pub fn value_of(&self, variable: &Variable) -> f64 {
*self.values.get(&variable.id).unwrap_or(&0.0)
}
pub fn reset(&mut self) {
self.rows.clear();
self.vars.clear();
self.constraints.clear();
self.edits.clear();
self.infeasible_rows.clear();
self.objective = Row::new(0.0);
self.values.clear();
}
fn get_var_sym(&mut self, variable: &Variable) -> Sym {
*self
.vars
.entry(variable.id)
.or_insert_with(|| new_sym(SymKind::External))
}
fn create_row(&mut self, constraint: &Constraint) -> (Row, Tag) {
let expr = &constraint.expression;
let mut row = Row::new(expr.constant);
for term in &expr.terms {
if !near_zero(term.coefficient) {
let sym = self.get_var_sym(&term.variable);
if let Some(basic) = self.rows.get(&sym).cloned() {
row.insert_row(&basic, term.coefficient);
} else {
row.insert_symbol(sym, term.coefficient);
}
}
}
let is_req = (constraint.strength - Strength::REQUIRED).abs() < NEAR_ZERO;
let tag = match constraint.op {
RelOp::GreaterThanOrEq | RelOp::LessThanOrEq => {
let coeff = if constraint.op == RelOp::LessThanOrEq {
1.0
} else {
-1.0
};
let slack = new_sym(SymKind::Slack);
row.insert_symbol(slack, coeff);
if !is_req {
let error = new_sym(SymKind::Error);
row.insert_symbol(error, -coeff);
self.objective.insert_symbol(error, constraint.strength);
Tag {
marker: slack,
other: error,
}
} else {
Tag {
marker: slack,
other: Sym::invalid(),
}
}
}
RelOp::Equal => {
if is_req {
let dummy = new_sym(SymKind::Dummy);
row.insert_symbol(dummy, 1.0);
Tag {
marker: dummy,
other: Sym::invalid(),
}
} else {
let ep = new_sym(SymKind::Error);
let em = new_sym(SymKind::Error);
row.insert_symbol(ep, -1.0);
row.insert_symbol(em, 1.0);
self.objective.insert_symbol(ep, constraint.strength);
self.objective.insert_symbol(em, constraint.strength);
Tag {
marker: ep,
other: em,
}
}
}
};
if row.constant < 0.0 {
row.reverse_sign();
}
(row, tag)
}
fn choose_subject(row: &Row, tag: &Tag) -> Sym {
for &sym in row.cells.keys() {
if sym.is_external() {
return sym;
}
}
if tag.marker.is_pivotable() && row.coefficient_for(tag.marker) < 0.0 {
return tag.marker;
}
if !tag.other.is_invalid()
&& tag.other.is_pivotable()
&& row.coefficient_for(tag.other) < 0.0
{
return tag.other;
}
Sym::invalid()
}
fn all_dummies(row: &Row) -> bool {
row.cells.keys().all(|s| s.is_dummy())
}
fn add_with_artificial(&mut self, row: &Row) -> Result<bool, SolverError> {
let art = new_sym(SymKind::Slack);
let mut art_obj = row.clone();
self.rows.insert(art, row.clone());
self.optimize_row(&mut art_obj)?;
let success = near_zero(art_obj.constant);
if let Some(mut art_row) = self.rows.remove(&art) {
if !art_row.cells.is_empty() {
let entering = Self::any_pivotable_sym(&art_row);
if !entering.is_invalid() {
art_row.solve_for_symbols(art, entering);
self.substitute_all(entering, &art_row);
self.rows.insert(entering, art_row);
}
}
}
for row in self.rows.values_mut() {
row.remove(art);
}
self.objective.remove(art);
Ok(success)
}
fn any_pivotable_sym(row: &Row) -> Sym {
row.cells
.keys()
.find(|&&s| s.is_pivotable())
.cloned()
.unwrap_or_else(Sym::invalid)
}
fn substitute_all(&mut self, sym: Sym, row: &Row) {
let keys: Vec<Sym> = self.rows.keys().cloned().collect();
for key in keys {
let r = self.rows.get_mut(&key).expect("row present");
r.substitute(sym, row);
if !key.is_external() && r.constant < 0.0 {
self.infeasible_rows.push(key);
}
}
self.objective.substitute(sym, row);
}
fn optimize(&mut self) -> Result<(), SolverError> {
loop {
let entering = Self::get_entering_sym(&self.objective);
if entering.is_invalid() {
return Ok(());
}
let (leaving, mut row) = self
.get_leaving_row(entering)
.ok_or_else(|| SolverError::InternalError("objective unbounded".into()))?;
row.solve_for_symbols(leaving, entering);
self.substitute_all(entering, &row);
self.rows.insert(entering, row);
}
}
fn optimize_row(&mut self, obj: &mut Row) -> Result<(), SolverError> {
loop {
let entering = Self::get_entering_sym(obj);
if entering.is_invalid() {
return Ok(());
}
let (leaving, mut row) = self
.get_leaving_row(entering)
.ok_or_else(|| SolverError::InternalError("art objective unbounded".into()))?;
row.solve_for_symbols(leaving, entering);
self.substitute_all(entering, &row);
obj.substitute(entering, &row);
self.rows.insert(entering, row);
}
}
fn dual_optimize(&mut self) -> Result<(), SolverError> {
while let Some(leaving) = self.infeasible_rows.pop() {
let constant = match self.rows.get(&leaving) {
Some(r) => r.constant,
None => continue,
};
if constant >= 0.0 {
continue;
}
let entering = {
let row = self.rows.get(&leaving).expect("infeasible row");
let mut best = Sym::invalid();
let mut ratio = f64::INFINITY;
for (&sym, &coeff) in &row.cells {
if coeff > 0.0 && !sym.is_dummy() {
let obj_c = self.objective.coefficient_for(sym);
let r = obj_c / coeff;
if r < ratio {
ratio = r;
best = sym;
}
}
}
best
};
if entering.is_invalid() {
return Err(SolverError::InternalError("dual optimize failed".into()));
}
let mut row = self
.rows
.remove(&leaving)
.ok_or_else(|| SolverError::InternalError("leaving row missing".into()))?;
row.solve_for_symbols(leaving, entering);
self.substitute_all(entering, &row);
self.rows.insert(entering, row);
}
Ok(())
}
fn get_entering_sym(obj: &Row) -> Sym {
for (&sym, &coeff) in &obj.cells {
if !sym.is_dummy() && coeff < 0.0 {
return sym;
}
}
Sym::invalid()
}
fn get_leaving_row(&mut self, entering: Sym) -> Option<(Sym, Row)> {
let mut ratio = f64::INFINITY;
let mut found = None;
for (&sym, row) in &self.rows {
if sym.is_external() {
continue;
}
let c = row.coefficient_for(entering);
if c < 0.0 {
let r = -row.constant / c;
if r < ratio {
ratio = r;
found = Some(sym);
}
}
}
found.map(|s| (s, self.rows.remove(&s).expect("row present")))
}
fn get_marker_leaving_row(&mut self, marker: Sym) -> Option<(Sym, Row)> {
let mut r1 = f64::INFINITY;
let mut r2 = f64::INFINITY;
let mut first: Option<Sym> = None;
let mut second: Option<Sym> = None;
let mut third: Option<Sym> = None;
for (&sym, row) in &self.rows {
let c = row.coefficient_for(marker);
if near_zero(c) {
continue;
}
if sym.is_external() {
third = Some(sym);
} else if c < 0.0 {
let r = -row.constant / c;
if r < r1 {
r1 = r;
first = Some(sym);
}
} else {
let r = row.constant / c;
if r < r2 {
r2 = r;
second = Some(sym);
}
}
}
first
.or(second)
.or(third)
.map(|s| (s, self.rows.remove(&s).expect("row present")))
}
fn remove_constraint_effects(&mut self, tag: &Tag, strength: f64) {
if tag.marker.is_error() {
self.remove_marker_effects(tag.marker, strength);
} else if tag.other.is_error() {
self.remove_marker_effects(tag.other, strength);
}
}
fn remove_marker_effects(&mut self, marker: Sym, strength: f64) {
if let Some(row) = self.rows.get(&marker).cloned() {
self.objective.insert_row(&row, -strength);
} else {
self.objective.insert_symbol(marker, -strength);
}
}
}
impl Default for Solver {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f64, b: f64) -> bool {
(a - b).abs() < 1e-4
}
#[test]
fn two_equality_constraints() {
let mut s = Solver::new();
let x = Variable::new();
let y = Variable::new();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-10.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.add_constraint(Constraint::new(
Expression::new(
vec![
Term {
variable: y.clone(),
coefficient: 1.0,
},
Term {
variable: x.clone(),
coefficient: -1.0,
},
],
-5.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.update_variables();
assert!(approx(s.value_of(&x), 10.0), "x={}", s.value_of(&x));
assert!(approx(s.value_of(&y), 15.0), "y={}", s.value_of(&y));
}
#[test]
fn suggest_value_and_update() {
let mut s = Solver::new();
let x = Variable::new();
s.add_constraint(Constraint::new(
Expression::from_variable(x.clone()),
RelOp::Equal,
Strength::WEAK,
))
.unwrap();
s.add_edit_variable(&x, Strength::STRONG).unwrap();
s.suggest_value(&x, 30.0).unwrap();
s.update_variables();
assert!(approx(s.value_of(&x), 30.0), "x={}", s.value_of(&x));
}
#[test]
fn over_constrained_required() {
let mut s = Solver::new();
let x = Variable::new();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-10.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
let result = s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-20.0,
),
RelOp::Equal,
Strength::REQUIRED,
));
assert_eq!(result, Err(SolverError::UnsatisfiableConstraint));
}
#[test]
fn proportional_constraints() {
let mut s = Solver::new();
let a = Variable::new();
let b = Variable::new();
s.add_constraint(Constraint::new(
Expression::new(
vec![
Term {
variable: a.clone(),
coefficient: 1.0,
},
Term {
variable: b.clone(),
coefficient: -2.0,
},
],
0.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.add_constraint(Constraint::new(
Expression::new(
vec![
Term {
variable: a.clone(),
coefficient: 1.0,
},
Term {
variable: b.clone(),
coefficient: 1.0,
},
],
-300.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.update_variables();
assert!(approx(s.value_of(&a), 200.0), "a={}", s.value_of(&a));
assert!(approx(s.value_of(&b), 100.0), "b={}", s.value_of(&b));
}
#[test]
fn anchoring_with_bounds() {
let mut s = Solver::new();
let x = Variable::new();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-50.0,
),
RelOp::GreaterThanOrEq,
Strength::REQUIRED,
))
.unwrap();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-200.0,
),
RelOp::LessThanOrEq,
Strength::REQUIRED,
))
.unwrap();
s.add_edit_variable(&x, Strength::STRONG).unwrap();
s.suggest_value(&x, 75.0).unwrap();
s.update_variables();
let v = s.value_of(&x);
assert!(v >= 50.0 - 1e-4, "x={v} < 50");
assert!(v <= 200.0 + 1e-4, "x={v} > 200");
assert!(approx(v, 75.0), "x={v} ≠ 75");
}
#[test]
fn remove_constraint_and_resolv() {
let mut s = Solver::new();
let x = Variable::new();
let c1 = Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-10.0,
),
RelOp::Equal,
Strength::REQUIRED,
);
let c2 = Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-20.0,
),
RelOp::Equal,
Strength::REQUIRED,
);
s.add_constraint(c1.clone()).unwrap();
s.update_variables();
assert!(
approx(s.value_of(&x), 10.0),
"before remove: x={}",
s.value_of(&x)
);
s.remove_constraint(&c1).unwrap();
s.add_constraint(c2).unwrap();
s.update_variables();
assert!(
approx(s.value_of(&x), 20.0),
"after remove: x={}",
s.value_of(&x)
);
}
#[test]
fn strength_ordering() {
let mut s = Solver::new();
let x = Variable::new();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-100.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-200.0,
),
RelOp::Equal,
Strength::STRONG,
))
.unwrap();
s.update_variables();
assert!(approx(s.value_of(&x), 100.0), "x={}", s.value_of(&x));
}
#[test]
fn reset_and_readd() {
let mut s = Solver::new();
let x = Variable::new();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-99.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.update_variables();
assert!(approx(s.value_of(&x), 99.0));
s.reset();
s.add_constraint(Constraint::new(
Expression::new(
vec![Term {
variable: x.clone(),
coefficient: 1.0,
}],
-5.0,
),
RelOp::Equal,
Strength::REQUIRED,
))
.unwrap();
s.update_variables();
assert!(approx(s.value_of(&x), 5.0), "x={}", s.value_of(&x));
}
use proptest::prelude::*;
proptest! {
#[test]
fn no_panic_random_constraints(
vals in prop::collection::vec(
(0.0f64..100.0, 0.0f64..100.0), 1..10
)
) {
let mut s = Solver::new();
let x = Variable::new();
for (lo, hi) in &vals {
let lo = lo.min(*hi);
let hi = hi.max(lo);
let _ = s.add_constraint(Constraint::new(
Expression::new(
vec![Term { variable: x.clone(), coefficient: 1.0 }],
-lo,
),
RelOp::GreaterThanOrEq,
Strength::WEAK,
));
let _ = s.add_constraint(Constraint::new(
Expression::new(
vec![Term { variable: x.clone(), coefficient: 1.0 }],
-hi,
),
RelOp::LessThanOrEq,
Strength::WEAK,
));
}
s.update_variables();
}
}
}