diff --git a/packages/cassowary/lib/cassowary.dart b/packages/cassowary/lib/cassowary.dart new file mode 100644 index 0000000000..913ec48cf2 --- /dev/null +++ b/packages/cassowary/lib/cassowary.dart @@ -0,0 +1,22 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library cassowary; + +import 'dart:math'; + +part 'constraint.dart'; +part 'expression.dart'; +part 'term.dart'; +part 'variable.dart'; +part 'equation_member.dart'; +part 'constant_member.dart'; +part 'solver.dart'; +part 'symbol.dart'; +part 'row.dart'; +part 'utils.dart'; +part 'result.dart'; +part 'parser_exception.dart'; +part 'param.dart'; +part 'priority.dart'; diff --git a/packages/cassowary/lib/constant_member.dart b/packages/cassowary/lib/constant_member.dart new file mode 100644 index 0000000000..fed8d20740 --- /dev/null +++ b/packages/cassowary/lib/constant_member.dart @@ -0,0 +1,19 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class ConstantMember extends _EquationMember { + final double value; + + bool get isConstant => true; + + ConstantMember(this.value); + + Expression asExpression() => new Expression([], this.value); +} + +ConstantMember cm(double value) { + return new ConstantMember(value); +} diff --git a/packages/cassowary/lib/constraint.dart b/packages/cassowary/lib/constraint.dart new file mode 100644 index 0000000000..83a5946cc2 --- /dev/null +++ b/packages/cassowary/lib/constraint.dart @@ -0,0 +1,42 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +enum Relation { equalTo, lessThanOrEqualTo, greaterThanOrEqualTo, } + +class Constraint { + final Relation relation; + final Expression expression; + double priority = Priority.required; + + Constraint(this.expression, this.relation); + + Constraint operator |(double p) => this..priority = p; + + String toString() { + StringBuffer buffer = new StringBuffer(); + buffer.write(expression.toString()); + + switch (relation) { + case Relation.equalTo: + buffer.write(" == 0 "); + break; + case Relation.greaterThanOrEqualTo: + buffer.write(" >= 0 "); + break; + case Relation.lessThanOrEqualTo: + buffer.write(" <= 0 "); + break; + } + + buffer.write(" | priority = ${priority}"); + + if (priority == Priority.required) { + buffer.write(" (required)"); + } + + return buffer.toString(); + } +} diff --git a/packages/cassowary/lib/equation_member.dart b/packages/cassowary/lib/equation_member.dart new file mode 100644 index 0000000000..247710d624 --- /dev/null +++ b/packages/cassowary/lib/equation_member.dart @@ -0,0 +1,30 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +abstract class _EquationMember { + Expression asExpression(); + + bool get isConstant; + + double get value; + + Constraint operator >=(_EquationMember m) => asExpression() >= m; + + Constraint operator <=(_EquationMember m) => asExpression() <= m; + + operator ==(_EquationMember m) => asExpression() == m; + + Expression operator +(_EquationMember m) => asExpression() + m; + + Expression operator -(_EquationMember m) => asExpression() - m; + + Expression operator *(_EquationMember m) => asExpression() * m; + + Expression operator /(_EquationMember m) => asExpression() / m; + + int get hashCode => + throw "An equation member is not comparable and cannot be added to collections"; +} diff --git a/packages/cassowary/lib/expression.dart b/packages/cassowary/lib/expression.dart new file mode 100644 index 0000000000..0c7f1e9ce2 --- /dev/null +++ b/packages/cassowary/lib/expression.dart @@ -0,0 +1,172 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Expression extends _EquationMember { + final List terms; + + final double constant; + + bool get isConstant => terms.length == 0; + + double get value => terms.fold(constant, (value, term) => value + term.value); + + Expression(this.terms, this.constant); + Expression.fromExpression(Expression expr) + : this.terms = new List.from(expr.terms), + this.constant = expr.constant; + + Expression asExpression() => this; + + Constraint _createConstraint( + _EquationMember /* rhs */ value, Relation relation) { + if (value is ConstantMember) { + return new Constraint( + new Expression(new List.from(terms), constant - value.value), + relation); + } + + if (value is Param) { + var newTerms = new List.from(terms) + ..add(new Term(value.variable, -1.0)); + return new Constraint(new Expression(newTerms, constant), relation); + } + + if (value is Term) { + var newTerms = new List.from(terms) + ..add(new Term(value.variable, -value.coefficient)); + return new Constraint(new Expression(newTerms, constant), relation); + } + + if (value is Expression) { + var newTerms = value.terms.fold(new List.from(terms), + (list, t) => list..add(new Term(t.variable, -t.coefficient))); + return new Constraint( + new Expression(newTerms, constant - value.constant), relation); + } + + assert(false); + return null; + } + + Constraint operator >=(_EquationMember value) => + _createConstraint(value, Relation.greaterThanOrEqualTo); + + Constraint operator <=(_EquationMember value) => + _createConstraint(value, Relation.lessThanOrEqualTo); + + operator ==(_EquationMember value) => + _createConstraint(value, Relation.equalTo); + + Expression operator +(_EquationMember m) { + if (m is ConstantMember) { + return new Expression(new List.from(terms), constant + m.value); + } + + if (m is Param) { + return new Expression( + new List.from(terms)..add(new Term(m.variable, 1.0)), constant); + } + + if (m is Term) { + return new Expression(new List.from(terms)..add(m), constant); + } + + if (m is Expression) { + return new Expression( + new List.from(terms)..addAll(m.terms), constant + m.constant); + } + + assert(false); + return null; + } + + Expression operator -(_EquationMember m) { + if (m is ConstantMember) { + return new Expression(new List.from(terms), constant - m.value); + } + + if (m is Param) { + return new Expression( + new List.from(terms)..add(new Term(m.variable, -1.0)), constant); + } + + if (m is Term) { + return new Expression(new List.from(terms) + ..add(new Term(m.variable, -m.coefficient)), constant); + } + + if (m is Expression) { + var copiedTerms = new List.from(terms); + m.terms.forEach( + (t) => copiedTerms.add(new Term(t.variable, -t.coefficient))); + return new Expression(copiedTerms, constant - m.constant); + } + + assert(false); + return null; + } + + _EquationMember _applyMultiplicand(double m) { + var newTerms = terms.fold(new List(), (list, term) => list + ..add(new Term(term.variable, term.coefficient * m))); + return new Expression(newTerms, constant * m); + } + + _Pair _findMulitplierAndMultiplicand(_EquationMember m) { + // At least on of the the two members must be constant for the resulting + // expression to be linear + + if (!this.isConstant && !m.isConstant) { + return null; + } + + if (this.isConstant) { + return new _Pair(m.asExpression(), this.value); + } + + if (m.isConstant) { + return new _Pair(this.asExpression(), m.value); + } + + assert(false); + return null; + } + + _EquationMember operator *(_EquationMember m) { + _Pair args = _findMulitplierAndMultiplicand(m); + + if (args == null) { + throw new ParserException( + "Could not find constant multiplicand or multiplier", [this, m]); + return null; + } + + return args.first._applyMultiplicand(args.second); + } + + _EquationMember operator /(_EquationMember m) { + if (!m.isConstant) { + throw new ParserException( + "The divisor was not a constant expression", [this, m]); + return null; + } + + return this._applyMultiplicand(1.0 / m.value); + } + + String toString() { + StringBuffer buffer = new StringBuffer(); + + terms.forEach((t) => buffer.write("${t}")); + + if (constant != 0.0) { + buffer.write(constant.sign > 0.0 ? "+" : "-"); + buffer.write(constant.abs()); + } + + return buffer.toString(); + } +} diff --git a/packages/cassowary/lib/param.dart b/packages/cassowary/lib/param.dart new file mode 100644 index 0000000000..6efe14dffa --- /dev/null +++ b/packages/cassowary/lib/param.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Param extends _EquationMember { + final Variable variable; + dynamic context; + + Param([double value = 0.0]) : variable = new Variable(value) { + variable._owner = this; + } + + Param.withContext(ctx, [double value = 0.0]) + : variable = new Variable(value), + context = ctx { + variable._owner = this; + } + + bool get isConstant => false; + + double get value => variable.value; + + String get name => variable.name; + set name(String name) => variable.name = name; + + Expression asExpression() => new Expression([new Term(variable, 1.0)], 0.0); +} diff --git a/packages/cassowary/lib/parser_exception.dart b/packages/cassowary/lib/parser_exception.dart new file mode 100644 index 0000000000..979533caa4 --- /dev/null +++ b/packages/cassowary/lib/parser_exception.dart @@ -0,0 +1,16 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class ParserException implements Exception { + final String message; + List<_EquationMember> members; + ParserException(this.message, this.members); + + String toString() { + if (message == null) return "Error while parsing constraint or expression"; + return "Error: '$message' while trying to parse constraint or expression"; + } +} diff --git a/packages/cassowary/lib/priority.dart b/packages/cassowary/lib/priority.dart new file mode 100644 index 0000000000..9e432e1df3 --- /dev/null +++ b/packages/cassowary/lib/priority.dart @@ -0,0 +1,24 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Priority { + static final double required = create(1e3, 1e3, 1e3); + static final double strong = create(1.0, 0.0, 0.0); + static final double medium = create(0.0, 1.0, 0.0); + static final double weak = create(0.0, 0.0, 1.0); + + static double create(double a, double b, double c) { + double result = 0.0; + result += max(0.0, min(1e3, a)) * 1e6; + result += max(0.0, min(1e3, b)) * 1e3; + result += max(0.0, min(1e3, c)); + return result; + } + + static double clamp(double value) { + return max(0.0, min(required, value)); + } +} diff --git a/packages/cassowary/lib/result.dart b/packages/cassowary/lib/result.dart new file mode 100644 index 0000000000..36c822bccd --- /dev/null +++ b/packages/cassowary/lib/result.dart @@ -0,0 +1,29 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Result { + final String message; + final bool error; + + const Result(this.message, { bool isError: true }) : error = isError; + + static final Result success = const Result("Success", isError: false); + static final Result unimplemented = const Result("Unimplemented"); + static final Result duplicateConstraint = + const Result("Duplicate Constraint"); + static final Result unsatisfiableConstraint = + const Result("Unsatisfiable Constraint"); + static final Result unknownConstraint = + const Result("Unknown Constraint"); + static final Result duplicateEditVariable = + const Result("Duplicate Edit Variable"); + static final Result badRequiredStrength = + const Result("Bad Required Strength"); + static final Result unknownEditVariable = + const Result("Unknown Edit Variable"); + static final Result internalSolverError = + const Result("Internal Solver Error"); +} diff --git a/packages/cassowary/lib/row.dart b/packages/cassowary/lib/row.dart new file mode 100644 index 0000000000..5c49c0c78e --- /dev/null +++ b/packages/cassowary/lib/row.dart @@ -0,0 +1,78 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class _Row { + final Map<_Symbol, double> cells; + double constant = 0.0; + + _Row(this.constant) : this.cells = new Map<_Symbol, double>(); + _Row.fromRow(_Row row) + : this.cells = new Map<_Symbol, double>.from(row.cells), + this.constant = row.constant; + + double add(double value) => constant += value; + + void insertSymbol(_Symbol symbol, [double coefficient = 1.0]) { + double val = _elvis(cells[symbol], 0.0); + + if (_nearZero(val + coefficient)) { + cells.remove(symbol); + } else { + cells[symbol] = val + coefficient; + } + } + + void insertRow(_Row other, [double coefficient = 1.0]) { + constant += other.constant * coefficient; + other.cells.forEach((s, v) => insertSymbol(s, v * coefficient)); + } + + void removeSymbol(_Symbol symbol) { + cells.remove(symbol); + } + + void reverseSign() { + constant = -constant; + cells.forEach((s, v) => cells[s] = -v); + } + + void solveForSymbol(_Symbol symbol) { + assert(cells.containsKey(symbol)); + double coefficient = -1.0 / cells[symbol]; + cells.remove(symbol); + constant *= coefficient; + cells.forEach((s, v) => cells[s] = v * coefficient); + } + + void solveForSymbols(_Symbol lhs, _Symbol rhs) { + insertSymbol(lhs, -1.0); + solveForSymbol(rhs); + } + + double coefficientForSymbol(_Symbol symbol) => _elvis(cells[symbol], 0.0); + + void substitute(_Symbol symbol, _Row row) { + double coefficient = cells[symbol]; + + if (coefficient == null) { + return; + } + + cells.remove(symbol); + insertRow(row, coefficient); + } + + String toString() { + StringBuffer buffer = new StringBuffer(); + + buffer.write(constant); + + cells.forEach((symbol, value) => + buffer.write(" + " + value.toString() + " * " + symbol.toString())); + + return buffer.toString(); + } +} diff --git a/packages/cassowary/lib/solver.dart b/packages/cassowary/lib/solver.dart new file mode 100644 index 0000000000..249560def1 --- /dev/null +++ b/packages/cassowary/lib/solver.dart @@ -0,0 +1,652 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Solver { + final Map _constraints = new Map(); + final Map<_Symbol, _Row> _rows = new Map<_Symbol, _Row>(); + final Map _vars = new Map(); + final Map _edits = new Map(); + final List<_Symbol> _infeasibleRows = new List<_Symbol>(); + final _Row _objective = new _Row(0.0); + _Row _artificial = new _Row(0.0); + int tick = 1; + + /// Attempts to add the constraints in the list to the solver. If it cannot + /// add any for some reason, a cleanup is attempted so that either all + /// constraints will be added or none. + Result addConstraints(List constraints) { + _SolverBulkUpdate applier = (Constraint c) => addConstraint(c); + _SolverBulkUpdate undoer = (Constraint c) => removeConstraint(c); + + return _bulkEdit(constraints, applier, undoer); + } + + Result addConstraint(Constraint constraint) { + if (_constraints.containsKey(constraint)) { + return Result.duplicateConstraint; + } + + _Tag tag = new _Tag(new _Symbol(_SymbolType.invalid, 0), + new _Symbol(_SymbolType.invalid, 0)); + + _Row row = _createRow(constraint, tag); + + _Symbol subject = _chooseSubjectForRow(row, tag); + + if (subject.type == _SymbolType.invalid && _allDummiesInRow(row)) { + if (!_nearZero(row.constant)) { + return Result.unsatisfiableConstraint; + } else { + subject = tag.marker; + } + } + + if (subject.type == _SymbolType.invalid) { + if (!_addWithArtificialVariableOnRow(row)) { + return Result.unsatisfiableConstraint; + } + } else { + row.solveForSymbol(subject); + _substitute(subject, row); + _rows[subject] = row; + } + + _constraints[constraint] = tag; + + return _optimizeObjectiveRow(_objective); + } + + Result removeConstraints(List constraints) { + _SolverBulkUpdate applier = (Constraint c) => removeConstraint(c); + _SolverBulkUpdate undoer = (Constraint c) => addConstraint(c); + + return _bulkEdit(constraints, applier, undoer); + } + + Result removeConstraint(Constraint constraint) { + _Tag tag = _constraints[constraint]; + if (tag == null) { + return Result.unknownConstraint; + } + + tag = new _Tag.fromTag(tag); + _constraints.remove(constraint); + + _removeConstraintEffects(constraint, tag); + + _Row row = _rows[tag.marker]; + if (row != null) { + _rows.remove(tag.marker); + } else { + _Pair<_Symbol, _Row> rowPair = _leavingRowPairForMarkerSymbol(tag.marker); + + if (rowPair == null) { + return Result.internalSolverError; + } + + _Symbol leaving = rowPair.first; + row = rowPair.second; + var removed = _rows.remove(rowPair.first); + assert(removed != null); + row.solveForSymbols(leaving, tag.marker); + _substitute(tag.marker, row); + } + + return _optimizeObjectiveRow(_objective); + } + + bool hasConstraint(Constraint constraint) { + return _constraints.containsKey(constraint); + } + + Result addEditVariables(List variables, double priority) { + _SolverBulkUpdate applier = (Variable v) => addEditVariable(v, priority); + _SolverBulkUpdate undoer = (Variable v) => removeEditVariable(v); + + return _bulkEdit(variables, applier, undoer); + } + + Result addEditVariable(Variable variable, double priority) { + if (_edits.containsKey(variable)) { + return Result.duplicateEditVariable; + } + + if (!_isValidNonRequiredPriority(priority)) { + return Result.badRequiredStrength; + } + + Constraint constraint = new Constraint( + new Expression([new Term(variable, 1.0)], 0.0), Relation.equalTo); + constraint.priority = priority; + + if (addConstraint(constraint) != Result.success) { + return Result.internalSolverError; + } + + _EditInfo info = new _EditInfo(); + info.tag = _constraints[constraint]; + info.constraint = constraint; + info.constant = 0.0; + + _edits[variable] = info; + + return Result.success; + } + + Result removeEditVariables(List variables) { + _SolverBulkUpdate applier = (Variable v) => removeEditVariable(v); + _SolverBulkUpdate undoer = (Variable v) => + addEditVariable(v, _edits[v].constraint.priority); + + return _bulkEdit(variables, applier, undoer); + } + + Result removeEditVariable(Variable variable) { + _EditInfo info = _edits[variable]; + if (info == null) { + return Result.unknownEditVariable; + } + + if (removeConstraint(info.constraint) != Result.success) { + return Result.internalSolverError; + } + + _edits.remove(variable); + return Result.success; + } + + bool hasEditVariable(Variable variable) { + return _edits.containsKey(variable); + } + + Result suggestValueForVariable(Variable variable, double value) { + if (!_edits.containsKey(variable)) { + return Result.unknownEditVariable; + } + + _suggestValueForEditInfoWithoutDualOptimization(_edits[variable], value); + + return _dualOptimize(); + } + + Set flushUpdates() { + Set updates = new Set(); + + for (Variable variable in _vars.keys) { + _Symbol symbol = _vars[variable]; + _Row row = _rows[symbol]; + + double updatedValue = row == null ? 0.0 : row.constant; + + if (variable._applyUpdate(updatedValue) && variable._owner != null) { + dynamic context = variable._owner.context; + + if (context != null) { + updates.add(context); + } + } + } + + return updates; + } + + Result _bulkEdit(Iterable items, + _SolverBulkUpdate applier, + _SolverBulkUpdate undoer) { + List applied = new List(); + bool needsCleanup = false; + + Result result = Result.success; + + for (dynamic item in items) { + result = applier(item); + if (result == Result.success) { + applied.add(item); + } else { + needsCleanup = true; + break; + } + } + + if (needsCleanup) { + for (dynamic item in applied.reversed) { + undoer(item); + } + } + + return result; + } + + _Symbol _symbolForVariable(Variable variable) { + _Symbol symbol = _vars[variable]; + + if (symbol != null) { + return symbol; + } + + symbol = new _Symbol(_SymbolType.external, tick++); + _vars[variable] = symbol; + + return symbol; + } + + _Row _createRow(Constraint constraint, _Tag tag) { + Expression expr = new Expression.fromExpression(constraint.expression); + _Row row = new _Row(expr.constant); + + expr.terms.forEach((term) { + if (!_nearZero(term.coefficient)) { + _Symbol symbol = _symbolForVariable(term.variable); + + _Row foundRow = _rows[symbol]; + + if (foundRow != null) { + row.insertRow(foundRow, term.coefficient); + } else { + row.insertSymbol(symbol, term.coefficient); + } + } + }); + + switch (constraint.relation) { + case Relation.lessThanOrEqualTo: + case Relation.greaterThanOrEqualTo: + { + double coefficient = + constraint.relation == Relation.lessThanOrEqualTo ? 1.0 : -1.0; + + _Symbol slack = new _Symbol(_SymbolType.slack, tick++); + tag.marker = slack; + row.insertSymbol(slack, coefficient); + + if (constraint.priority < Priority.required) { + _Symbol error = new _Symbol(_SymbolType.error, tick++); + tag.other = error; + row.insertSymbol(error, -coefficient); + _objective.insertSymbol(error, constraint.priority); + } + } + break; + case Relation.equalTo: + if (constraint.priority < Priority.required) { + _Symbol errPlus = new _Symbol(_SymbolType.error, tick++); + _Symbol errMinus = new _Symbol(_SymbolType.error, tick++); + tag.marker = errPlus; + tag.other = errMinus; + row.insertSymbol(errPlus, -1.0); + row.insertSymbol(errMinus, 1.0); + _objective.insertSymbol(errPlus, constraint.priority); + _objective.insertSymbol(errMinus, constraint.priority); + } else { + _Symbol dummy = new _Symbol(_SymbolType.dummy, tick++); + tag.marker = dummy; + row.insertSymbol(dummy); + } + break; + } + + if (row.constant < 0.0) { + row.reverseSign(); + } + + return row; + } + + _Symbol _chooseSubjectForRow(_Row row, _Tag tag) { + for (_Symbol symbol in row.cells.keys) { + if (symbol.type == _SymbolType.external) { + return symbol; + } + } + + if (tag.marker.type == _SymbolType.slack || + tag.marker.type == _SymbolType.error) { + if (row.coefficientForSymbol(tag.marker) < 0.0) { + return tag.marker; + } + } + + if (tag.other.type == _SymbolType.slack || + tag.other.type == _SymbolType.error) { + if (row.coefficientForSymbol(tag.other) < 0.0) { + return tag.other; + } + } + + return new _Symbol(_SymbolType.invalid, 0); + } + + bool _allDummiesInRow(_Row row) { + for (_Symbol symbol in row.cells.keys) { + if (symbol.type != _SymbolType.dummy) { + return false; + } + } + return true; + } + + bool _addWithArtificialVariableOnRow(_Row row) { + _Symbol artificial = new _Symbol(_SymbolType.slack, tick++); + _rows[artificial] = new _Row.fromRow(row); + _artificial = new _Row.fromRow(row); + + Result result = _optimizeObjectiveRow(_artificial); + + if (result.error) { + // FIXME(csg): Propagate this up! + return false; + } + + bool success = _nearZero(_artificial.constant); + _artificial = new _Row(0.0); + + _Row foundRow = _rows[artificial]; + if (foundRow != null) { + _rows.remove(artificial); + if (foundRow.cells.isEmpty) { + return success; + } + + _Symbol entering = _anyPivotableSymbol(foundRow); + if (entering.type == _SymbolType.invalid) { + return false; + } + + foundRow.solveForSymbols(artificial, entering); + _substitute(entering, foundRow); + _rows[entering] = foundRow; + } + + for (_Row row in _rows.values) { + row.removeSymbol(artificial); + } + _objective.removeSymbol(artificial); + return success; + } + + Result _optimizeObjectiveRow(_Row objective) { + while (true) { + _Symbol entering = _enteringSymbolForObjectiveRow(objective); + if (entering.type == _SymbolType.invalid) { + return Result.success; + } + + _Pair<_Symbol, _Row> leavingPair = _leavingRowForEnteringSymbol(entering); + + if (leavingPair == null) { + return Result.internalSolverError; + } + + _Symbol leaving = leavingPair.first; + _Row row = leavingPair.second; + _rows.remove(leavingPair.first); + row.solveForSymbols(leaving, entering); + _substitute(entering, row); + _rows[entering] = row; + } + } + + _Symbol _enteringSymbolForObjectiveRow(_Row objective) { + Map<_Symbol, double> cells = objective.cells; + + for (_Symbol symbol in cells.keys) { + if (symbol.type != _SymbolType.dummy && cells[symbol] < 0.0) { + return symbol; + } + } + + return new _Symbol(_SymbolType.invalid, 0); + } + + _Pair<_Symbol, _Row> _leavingRowForEnteringSymbol(_Symbol entering) { + double ratio = double.MAX_FINITE; + _Pair<_Symbol, _Row> result = new _Pair(null, null); + + _rows.forEach((symbol, row) { + if (symbol.type != _SymbolType.external) { + double temp = row.coefficientForSymbol(entering); + + if (temp < 0.0) { + double temp_ratio = -row.constant / temp; + + if (temp_ratio < ratio) { + ratio = temp_ratio; + result.first = symbol; + result.second = row; + } + } + } + }); + + if (result.first == null || result.second == null) { + return null; + } + + return result; + } + + void _substitute(_Symbol symbol, _Row row) { + _rows.forEach((first, second) { + second.substitute(symbol, row); + if (first.type != _SymbolType.external && second.constant < 0.0) { + _infeasibleRows.add(first); + } + }); + + _objective.substitute(symbol, row); + if (_artificial != null) { + _artificial.substitute(symbol, row); + } + } + + _Symbol _anyPivotableSymbol(_Row row) { + for (_Symbol symbol in row.cells.keys) { + if (symbol.type == _SymbolType.slack || + symbol.type == _SymbolType.error) { + return symbol; + } + } + return new _Symbol(_SymbolType.invalid, 0); + } + + void _removeConstraintEffects(Constraint cn, _Tag tag) { + if (tag.marker.type == _SymbolType.error) { + _removeMarkerEffects(tag.marker, cn.priority); + } + if (tag.other.type == _SymbolType.error) { + _removeMarkerEffects(tag.other, cn.priority); + } + } + + void _removeMarkerEffects(_Symbol marker, double strength) { + _Row row = _rows[marker]; + if (row != null) { + _objective.insertRow(row, -strength); + } else { + _objective.insertSymbol(marker, -strength); + } + } + + _Pair<_Symbol, _Row> _leavingRowPairForMarkerSymbol(_Symbol marker) { + double r1 = double.MAX_FINITE; + double r2 = double.MAX_FINITE; + + _Pair<_Symbol, _Row> first, second, third; + + _rows.forEach((symbol, row) { + double c = row.coefficientForSymbol(marker); + + if (c == 0.0) { + return; + } + + if (symbol.type == _SymbolType.external) { + third = new _Pair(symbol, row); + } else if (c < 0.0) { + double r = -row.constant / c; + if (r < r1) { + r1 = r; + first = new _Pair(symbol, row); + } + } else { + double r = row.constant / c; + if (r < r2) { + r2 = r; + second = new _Pair(symbol, row); + } + } + }); + + if (first != null) { + return first; + } + if (second != null) { + return second; + } + return third; + } + + void _suggestValueForEditInfoWithoutDualOptimization( + _EditInfo info, double value) { + double delta = value - info.constant; + info.constant = value; + + { + _Symbol symbol = info.tag.marker; + _Row row = _rows[info.tag.marker]; + + if (row != null) { + if (row.add(-delta) < 0.0) { + _infeasibleRows.add(symbol); + } + return; + } + + symbol = info.tag.other; + row = _rows[info.tag.other]; + + if (row != null) { + if (row.add(delta) < 0.0) { + _infeasibleRows.add(symbol); + } + return; + } + } + + for (_Symbol symbol in _rows.keys) { + _Row row = _rows[symbol]; + double coeff = row.coefficientForSymbol(info.tag.marker); + if (coeff != 0.0 && + row.add(delta * coeff) < 0.0 && + symbol.type != _SymbolType.external) { + _infeasibleRows.add(symbol); + } + } + } + + Result _dualOptimize() { + while (_infeasibleRows.length != 0) { + _Symbol leaving = _infeasibleRows.removeLast(); + _Row row = _rows[leaving]; + + if (row != null && row.constant < 0.0) { + _Symbol entering = _dualEnteringSymbolForRow(row); + + if (entering.type == _SymbolType.invalid) { + return Result.internalSolverError; + } + + _rows.remove(leaving); + + row.solveForSymbols(leaving, entering); + _substitute(entering, row); + _rows[entering] = row; + } + } + return Result.success; + } + + _Symbol _dualEnteringSymbolForRow(_Row row) { + _Symbol entering; + + double ratio = double.MAX_FINITE; + + Map<_Symbol, double> rowCells = row.cells; + + for (_Symbol symbol in rowCells.keys) { + double value = rowCells[symbol]; + + if (value > 0.0 && symbol.type != _SymbolType.dummy) { + double coeff = _objective.coefficientForSymbol(symbol); + double r = coeff / value; + if (r < ratio) { + ratio = r; + entering = symbol; + } + } + } + + return _elvis(entering, new _Symbol(_SymbolType.invalid, 0)); + } + + String toString() { + StringBuffer buffer = new StringBuffer(); + String separator = "\n~~~~~~~~~"; + + // Objective + buffer.writeln(separator + " Objective"); + buffer.writeln(_objective.toString()); + + // Tableau + buffer.writeln(separator + " Tableau"); + _rows.forEach((symbol, row) { + buffer.write(symbol.toString()); + buffer.write(" | "); + buffer.writeln(row.toString()); + }); + + // Infeasible + buffer.writeln(separator + " Infeasible"); + _infeasibleRows.forEach((symbol) => buffer.writeln(symbol.toString())); + + // Variables + buffer.writeln(separator + " Variables"); + _vars.forEach((variable, symbol) => + buffer.writeln("${variable.toString()} = ${symbol.toString()}")); + + // Edit Variables + buffer.writeln(separator + " Edit Variables"); + _edits.forEach((variable, editinfo) => buffer.writeln(variable)); + + // Constraints + buffer.writeln(separator + " Constraints"); + _constraints.forEach((constraint, _) => buffer.writeln(constraint)); + + return buffer.toString(); + } +} + +class _Tag { + _Symbol marker; + _Symbol other; + + _Tag(this.marker, this.other); + _Tag.fromTag(_Tag tag) + : this.marker = tag.marker, + this.other = tag.other; +} + +class _EditInfo { + _Tag tag; + Constraint constraint; + double constant; +} + +bool _isValidNonRequiredPriority(double priority) { + return (priority >= 0.0 && priority < Priority.required); +} + +typedef Result _SolverBulkUpdate(dynamic item); diff --git a/packages/cassowary/lib/symbol.dart b/packages/cassowary/lib/symbol.dart new file mode 100644 index 0000000000..cd7bcf491c --- /dev/null +++ b/packages/cassowary/lib/symbol.dart @@ -0,0 +1,36 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +enum _SymbolType { invalid, external, slack, error, dummy, } + +class _Symbol { + final _SymbolType type; + final int tick; + + _Symbol(this.type, this.tick); + + String toString() { + String typeString = "unknown"; + switch (type) { + case _SymbolType.invalid: + typeString = "i"; + break; + case _SymbolType.external: + typeString = "v"; + break; + case _SymbolType.slack: + typeString = "s"; + break; + case _SymbolType.error: + typeString = "e"; + break; + case _SymbolType.dummy: + typeString = "d"; + break; + } + return "${typeString}${tick}"; + } +} diff --git a/packages/cassowary/lib/term.dart b/packages/cassowary/lib/term.dart new file mode 100644 index 0000000000..5849f65e16 --- /dev/null +++ b/packages/cassowary/lib/term.dart @@ -0,0 +1,34 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Term extends _EquationMember { + final Variable variable; + final double coefficient; + + bool get isConstant => false; + + double get value => coefficient * variable.value; + + Term(this.variable, this.coefficient); + + Expression asExpression() => + new Expression([new Term(this.variable, this.coefficient)], 0.0); + + String toString() { + StringBuffer buffer = new StringBuffer(); + + buffer.write(coefficient.sign > 0.0 ? "+" : "-"); + + if (coefficient.abs() != 1.0) { + buffer.write(coefficient.abs()); + buffer.write("*"); + } + + buffer.write(variable); + + return buffer.toString(); + } +} diff --git a/packages/cassowary/lib/utils.dart b/packages/cassowary/lib/utils.dart new file mode 100644 index 0000000000..699a6ae902 --- /dev/null +++ b/packages/cassowary/lib/utils.dart @@ -0,0 +1,21 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +bool _nearZero(double value) { + const double epsilon = 1.0e-8; + return value < 0.0 ? -value < epsilon : value < epsilon; +} + +// Workaround for the lack of a null coalescing operator. Uses a ternary +// instead. Sadly, due the lack of generic types on functions, we have to use +// dynamic instead. +_elvis(a, b) => a != null ? a : b; + +class _Pair { + X first; + Y second; + _Pair(this.first, this.second); +} diff --git a/packages/cassowary/lib/variable.dart b/packages/cassowary/lib/variable.dart new file mode 100644 index 0000000000..93850e716e --- /dev/null +++ b/packages/cassowary/lib/variable.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of cassowary; + +class Variable { + double value; + String name; + + Param _owner; + + final int _tick; + static int _total = 0; + + Variable(this.value) : _tick = _total++; + + bool _applyUpdate(double updated) { + bool res = updated != value; + value = updated; + return res; + } + + String get debugName => _elvis(name, "variable${_tick}"); + + String toString() => debugName; +} diff --git a/packages/cassowary/test/cassowary_test.dart b/packages/cassowary/test/cassowary_test.dart new file mode 100644 index 0000000000..300f5e85d2 --- /dev/null +++ b/packages/cassowary/test/cassowary_test.dart @@ -0,0 +1,627 @@ +// Copyright (c) 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +library cassowary.test; + +import 'package:test/test.dart'; + +import 'package:cassowary/cassowary.dart'; + +void main() { + test('variable', () { + var v = new Param(22.0); + expect(v.value, 22); + }); + + test('variable1', () { + var v = new Param(22.0); + expect((v + cm(22.0)).value, 44.0); + expect((v - cm(20.0)).value, 2.0); + }); + + test('term', () { + var t = new Term(new Variable(22.0), 2.0); + expect(t.value, 44); + }); + + test('expression', () { + var terms = [ + new Term(new Variable(22.0), 2.0), + new Term(new Variable(1.0), 1.0), + ]; + var e = new Expression(terms, 40.0); + expect(e.value, 85.0); + }); + + test('expression1', () { + var v1 = new Param(10.0); + var v2 = new Param(10.0); + var v3 = new Param(22.0); + + expect(v1 is Param, true); + expect(v1 + cm(20.0) is Expression, true); + expect(v1 + v2 is Expression, true); + + expect((v1 + v2).value, 20.0); + expect((v1 - v2).value, 0.0); + + expect((v1 + v2 + v3) is Expression, true); + expect((v1 + v2 + v3).value, 42.0); + }); + + test('expression2', () { + var e = new Param(10.0) + cm(5.0); + expect(e.value, 15.0); + expect(e is Expression, true); + + // Constant + expect((e + cm(2.0)) is Expression, true); + expect((e + cm(2.0)).value, 17.0); + expect((e - cm(2.0)) is Expression, true); + expect((e - cm(2.0)).value, 13.0); + + expect(e.value, 15.0); + + // Param + var v = new Param(2.0); + expect((e + v) is Expression, true); + expect((e + v).value, 17.0); + expect((e - v) is Expression, true); + expect((e - v).value, 13.0); + + expect(e.value, 15.0); + + // Term + var t = new Term(v.variable, 2.0); + expect((e + t) is Expression, true); + expect((e + t).value, 19.0); + expect((e - t) is Expression, true); + expect((e - t).value, 11.0); + + expect(e.value, 15.0); + + // Expression + var e2 = new Param(7.0) + new Param(3.0); + expect((e + e2) is Expression, true); + expect((e + e2).value, 25.0); + expect((e - e2) is Expression, true); + expect((e - e2).value, 5.0); + + expect(e.value, 15.0); + }); + + test('term2', () { + var t = new Term(new Variable(12.0), 1.0); + + // Constant + var c = cm(2.0); + expect((t + c) is Expression, true); + expect((t + c).value, 14.0); + expect((t - c) is Expression, true); + expect((t - c).value, 10.0); + + // Variable + var v = new Param(2.0); + expect((t + v) is Expression, true); + expect((t + v).value, 14.0); + expect((t - v) is Expression, true); + expect((t - v).value, 10.0); + + // Term + var t2 = new Term(new Variable(1.0), 2.0); + expect((t + t2) is Expression, true); + expect((t + t2).value, 14.0); + expect((t - t2) is Expression, true); + expect((t - t2).value, 10.0); + + // Expression + var exp = new Param(1.0) + cm(1.0); + expect((t + exp) is Expression, true); + expect((t + exp).value, 14.0); + expect((t - exp) is Expression, true); + expect((t - exp).value, 10.0); + }); + + test('variable3', () { + var v = new Param(3.0); + + // Constant + var c = cm(2.0); + expect((v + c) is Expression, true); + expect((v + c).value, 5.0); + expect((v - c) is Expression, true); + expect((v - c).value, 1.0); + + // Variable + var v2 = new Param(2.0); + expect((v + v2) is Expression, true); + expect((v + v2).value, 5.0); + expect((v - v2) is Expression, true); + expect((v - v2).value, 1.0); + + // Term + var t2 = new Term(new Variable(1.0), 2.0); + expect((v + t2) is Expression, true); + expect((v + t2).value, 5.0); + expect((v - t2) is Expression, true); + expect((v - t2).value, 1.0); + + // Expression + var exp = new Param(1.0) + cm(1.0); + expect(exp.terms.length, 1); + + expect((v + exp) is Expression, true); + expect((v + exp).value, 5.0); + expect((v - exp) is Expression, true); + expect((v - exp).value, 1.0); + }); + + test('constantmember', () { + var c = cm(3.0); + + // Constant + var c2 = cm(2.0); + expect((c + c2) is Expression, true); + expect((c + c2).value, 5.0); + expect((c - c2) is Expression, true); + expect((c - c2).value, 1.0); + + // Variable + var v2 = new Param(2.0); + expect((c + v2) is Expression, true); + expect((c + v2).value, 5.0); + expect((c - v2) is Expression, true); + expect((c - v2).value, 1.0); + + // Term + var t2 = new Term(new Variable(1.0), 2.0); + expect((c + t2) is Expression, true); + expect((c + t2).value, 5.0); + expect((c - t2) is Expression, true); + expect((c - t2).value, 1.0); + + // Expression + var exp = new Param(1.0) + cm(1.0); + + expect((c + exp) is Expression, true); + expect((c + exp).value, 5.0); + expect((c - exp) is Expression, true); + expect((c - exp).value, 1.0); + }); + + test('constraint2', () { + var left = new Param(10.0); + var right = new Param(100.0); + + var c = right - left >= cm(25.0); + expect(c is Constraint, true); + }); + + test('simple_multiplication', () { + // Constant + var c = cm(20.0); + expect((c * cm(2.0)).value, 40.0); + + // Variable + var v = new Param(20.0); + expect((v * cm(2.0)).value, 40.0); + + // Term + var t = new Term(v.variable, 1.0); + expect((t * cm(2.0)).value, 40.0); + + // Expression + var e = new Expression([t], 0.0); + expect((e * cm(2.0)).value, 40.0); + }); + + test('simple_division', () { + // Constant + var c = cm(20.0); + expect((c / cm(2.0)).value, 10.0); + + // Variable + var v = new Param(20.0); + expect((v / cm(2.0)).value, 10.0); + + // Term + var t = new Term(v.variable, 1.0); + expect((t / cm(2.0)).value, 10.0); + + // Expression + var e = new Expression([t], 0.0); + expect((e / cm(2.0)).value, 10.0); + }); + + test('full_constraints_setup', () { + var left = new Param(2.0); + var right = new Param(10.0); + + var c1 = right - left >= cm(20.0); + expect(c1 is Constraint, true); + expect(c1.expression.constant, -20.0); + expect(c1.relation, Relation.greaterThanOrEqualTo); + + var c2 = (right - left == cm(30.0)) as Constraint; + expect(c2 is Constraint, true); + expect(c2.expression.constant, -30.0); + expect(c2.relation, Relation.equalTo); + + var c3 = right - left <= cm(30.0); + expect(c3 is Constraint, true); + expect(c3.expression.constant, -30.0); + expect(c3.relation, Relation.lessThanOrEqualTo); + }); + + test('constraint_strength_update', () { + var left = new Param(2.0); + var right = new Param(10.0); + + var c = (right - left >= cm(200.0)) | 750.0; + expect(c is Constraint, true); + expect(c.expression.terms.length, 2); + expect(c.expression.constant, -200.0); + expect(c.priority, 750.0); + }); + + test('solver', () { + var s = new Solver(); + + var left = new Param(2.0); + var right = new Param(100.0); + + var c1 = right - left >= cm(200.0); + + expect((right >= left) is Constraint, true); + + expect(s.addConstraint(c1), Result.success); + }); + + test('constraint_complex', () { + var e = new Param(200.0) - new Param(100.0); + + // Constant + var c1 = e >= cm(50.0); + expect(c1 is Constraint, true); + expect(c1.expression.terms.length, 2); + expect(c1.expression.constant, -50.0); + + // Variable + var c2 = e >= new Param(2.0); + expect(c2 is Constraint, true); + expect(c2.expression.terms.length, 3); + expect(c2.expression.constant, 0.0); + + // Term + var c3 = e >= new Term(new Variable(2.0), 1.0); + expect(c3 is Constraint, true); + expect(c3.expression.terms.length, 3); + expect(c3.expression.constant, 0.0); + + // Expression + var c4 = e >= new Expression([new Term(new Variable(2.0), 1.0)], 20.0); + expect(c4 is Constraint, true); + expect(c4.expression.terms.length, 3); + expect(c4.expression.constant, -20.0); + }); + + test('constraint_complex_non_exprs', () { + // Constant + var c1 = cm(100.0) >= cm(50.0); + expect(c1 is Constraint, true); + expect(c1.expression.terms.length, 0); + expect(c1.expression.constant, 50.0); + + // Variable + var c2 = new Param(100.0) >= new Param(2.0); + expect(c2 is Constraint, true); + expect(c2.expression.terms.length, 2); + expect(c2.expression.constant, 0.0); + + // Term + var t = new Term(new Variable(100.0), 1.0); + var c3 = t >= new Term(new Variable(2.0), 1.0); + expect(c3 is Constraint, true); + expect(c3.expression.terms.length, 2); + expect(c3.expression.constant, 0.0); + + // Expression + var e = new Expression([t], 0.0); + var c4 = e >= new Expression([new Term(new Variable(2.0), 1.0)], 20.0); + expect(c4 is Constraint, true); + expect(c4.expression.terms.length, 2); + expect(c4.expression.constant, -20.0); + }); + + test('constraint_update_in_solver', () { + var s = new Solver(); + + var left = new Param(2.0); + var right = new Param(100.0); + + var c1 = right - left >= cm(200.0); + var c2 = right >= right; + + expect(s.addConstraint(c1), Result.success); + expect(s.addConstraint(c1), Result.duplicateConstraint); + expect(s.removeConstraint(c2), Result.unknownConstraint); + expect(s.removeConstraint(c1), Result.success); + expect(s.removeConstraint(c1), Result.unknownConstraint); + }); + + test('test_multiplication_division_override', () { + var c = cm(10.0); + var v = new Param(c.value); + var t = new Term(v.variable, 1.0); + var e = new Expression([t], 0.0); + + // Constant + expect((c * cm(10.0)).value, 100); + + // Variable + expect((v * cm(10.0)).value, 100); + + // Term + expect((t * cm(10.0)).value, 100); + + // Expression + expect((e * cm(10.0)).value, 100); + + // Constant + expect((c / cm(10.0)).value, 1); + + // Variable + expect((v / cm(10.0)).value, 1); + + // Term + expect((t / cm(10.0)).value, 1); + + // Expression + expect((e / cm(10.0)).value, 1); + }); + + test('test_multiplication_division_exceptions', () { + var c = cm(10.0); + var v = new Param(c.value); + var t = new Term(v.variable, 1.0); + var e = new Expression([t], 0.0); + + expect((c * c).value, 100); + expect(() => v * v, throwsA(new isInstanceOf())); + expect(() => v / v, throwsA(new isInstanceOf())); + expect(() => v * t, throwsA(new isInstanceOf())); + expect(() => v / t, throwsA(new isInstanceOf())); + expect(() => v * e, throwsA(new isInstanceOf())); + expect(() => v / e, throwsA(new isInstanceOf())); + expect(() => v * c, returnsNormally); + expect(() => v / c, returnsNormally); + }); + + test('edit_updates', () { + Solver s = new Solver(); + + var left = new Param(0.0); + var right = new Param(100.0); + var mid = new Param(0.0); + + Constraint c = left + right >= cm(2.0) * mid; + expect(s.addConstraint(c), Result.success); + + expect(s.addEditVariable(mid.variable, 999.0), Result.success); + expect( + s.addEditVariable(mid.variable, 999.0), Result.duplicateEditVariable); + expect(s.removeEditVariable(mid.variable), Result.success); + expect(s.removeEditVariable(mid.variable), Result.unknownEditVariable); + }); + + test('bug1', () { + var left = new Param(0.0); + var right = new Param(100.0); + var mid = new Param(0.0); + + expect(((left + right) >= (cm(2.0) * mid)) is Constraint, true); + }); + + test('single_item', () { + var left = new Param(-20.0); + Solver s = new Solver(); + s.addConstraint(left >= cm(0.0)); + s.flushUpdates(); + expect(left.value, 0.0); + }); + + test('midpoints', () { + var left = new Param(0.0)..name = "left"; + var right = new Param(0.0)..name = "right"; + var mid = new Param(0.0)..name = "mid"; + + Solver s = new Solver(); + + expect(s.addConstraint((right + left == mid * cm(2.0)) as Constraint), + Result.success); + expect(s.addConstraint(right - left >= cm(100.0)), Result.success); + expect(s.addConstraint(left >= cm(0.0)), Result.success); + + s.flushUpdates(); + + expect(left.value, 0.0); + expect(mid.value, 50.0); + expect(right.value, 100.0); + }); + + test('addition_of_multiple', () { + var left = new Param(0.0); + var right = new Param(0.0); + var mid = new Param(0.0); + + Solver s = new Solver(); + + var c = (left >= cm(0.0)); + + expect(s.addConstraints([ + (left + right == cm(2.0) * mid) as Constraint, + (right - left >= cm(100.0)), + c + ]), Result.success); + + expect(s.addConstraints([(right >= cm(-20.0)), c]), + Result.duplicateConstraint); + }); + + test('edit_constraints', () { + var left = new Param(0.0)..name = "left"; + var right = new Param(0.0)..name = "right"; + var mid = new Param(0.0)..name = "mid"; + + Solver s = new Solver(); + + expect(s.addConstraint((right + left == mid * cm(2.0)) as Constraint), + Result.success); + expect(s.addConstraint(right - left >= cm(100.0)), Result.success); + expect(s.addConstraint(left >= cm(0.0)), Result.success); + + expect(s.addEditVariable(mid.variable, Priority.strong), Result.success); + expect(s.suggestValueForVariable(mid.variable, 300.0), Result.success); + + s.flushUpdates(); + + expect(left.value, 0.0); + expect(mid.value, 300.0); + expect(right.value, 600.0); + }); + + test('test_description', () { + var left = new Param(0.0); + var right = new Param(100.0); + var c1 = right >= left; + var c2 = right <= left; + var c3 = (right == left) as Constraint; + + Solver s = new Solver(); + expect(s.addConstraint(c1), Result.success); + expect(s.addConstraint(c2), Result.success); + expect(s.addConstraint(c3), Result.success); + + expect(s.toString() != null, true); + }); + + test('solution_with_optimize', () { + Param p1 = new Param(); + Param p2 = new Param(); + Param p3 = new Param(); + + Param container = new Param(); + + Solver solver = new Solver(); + + solver.addEditVariable(container.variable, Priority.strong); + solver.suggestValueForVariable(container.variable, 100.0); + + solver.addConstraint((p1 >= cm(30.0)) | Priority.strong); + solver.addConstraint(((p1 == p3) as Constraint) | Priority.medium); + solver.addConstraint((p2 == cm(2.0) * p1) as Constraint); + solver.addConstraint((container == (p1 + p2 + p3)) as Constraint); + + solver.flushUpdates(); + + expect(container.value, 100.0); + + expect(p1.value, 30.0); + expect(p2.value, 60.0); + expect(p3.value, 10.0); + }); + + test('test_updates_collection', () { + Param left = new Param.withContext("left"); + Param mid = new Param.withContext("mid"); + Param right = new Param.withContext("right"); + + Solver s = new Solver(); + + expect(s.addEditVariable(mid.variable, Priority.strong), Result.success); + + expect(s.addConstraint((mid * cm(2.0) == left + right) as Constraint), + Result.success); + expect(s.addConstraint(left >= cm(0.0)), Result.success); + + expect(s.suggestValueForVariable(mid.variable, 50.0), Result.success); + + var updates = s.flushUpdates(); + + expect(updates.length, 2); + + expect(left.value, 0.0); + expect(mid.value, 50.0); + expect(right.value, 100.0); + }); + + test('test_updates_collection_is_set', () { + Param left = new Param.withContext("a"); + Param mid = new Param.withContext("a"); + Param right = new Param.withContext("a"); + + Solver s = new Solver(); + + expect(s.addEditVariable(mid.variable, Priority.strong), Result.success); + + expect(s.addConstraint((mid * cm(2.0) == left + right) as Constraint), + Result.success); + expect(s.addConstraint(left >= cm(10.0)), Result.success); + + expect(s.suggestValueForVariable(mid.variable, 50.0), Result.success); + + var updates = s.flushUpdates(); + + expect(updates.length, 1); + + expect(left.value, 10.0); + expect(mid.value, 50.0); + expect(right.value, 90.0); + }); + + test('param_context_non_final', () { + var p = new Param.withContext("a"); + p.context = "b"; + expect(p.context, "b"); + }); + + test('check_type_of_eq_result', () { + Param left = new Param(); + Param right = new Param(); + + expect((left == right).runtimeType, Constraint); + }); + + test('bulk_add_edit_variables', () { + Solver s = new Solver(); + + var left = new Param(0.0); + var right = new Param(100.0); + var mid = new Param(0.0); + + expect(s.addEditVariables( + [left.variable, right.variable, mid.variable], 999.0), Result.success); + }); + + test('bulk_remove_constraints_and_variables', () { + Solver s = new Solver(); + + var left = new Param(0.0); + var right = new Param(100.0); + var mid = new Param(0.0); + + expect(s.addEditVariables( + [left.variable, right.variable, mid.variable], 999.0), Result.success); + + var c1 = left <= mid; + var c2 = mid <= right; + + expect(s.addConstraints([c1, c2]), Result.success); + + expect(s.removeConstraints([c1, c2]), Result.success); + + expect(s.removeEditVariables( + [left.variable, right.variable, mid.variable]), Result.success); + }); +}