├── .spi.yml ├── .gitignore ├── Package.swift ├── Sources └── Cassowary │ ├── Variable.swift │ ├── Strength.swift │ ├── Term.swift │ ├── Symbol.swift │ ├── Expression.swift │ ├── Errors.swift │ ├── Constraint.swift │ ├── Row.swift │ ├── Symbolics.swift │ └── Solver.swift ├── .github └── workflows │ ├── build.yml │ └── coverage.yml ├── LICENSE ├── README.md └── Tests └── CassowaryTests └── CassowaryTests.swift /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - documenation_targets: [Cassowary] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # git ls-files --others --exclude-from=.git/info/exclude 2 | # Lines that start with '#' are comments. 3 | 4 | .*.sw? 5 | .build/ 6 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.3 2 | 3 | import PackageDescription 4 | 5 | let cassowary = Package( 6 | name: "cassowary", 7 | products: [ 8 | .library(name: "cassowary", targets: ["Cassowary"]), 9 | ], 10 | dependencies: [ 11 | .package(url: "https://github.com/apple/swift-collections.git", 12 | .upToNextMinor(from: "1.0.0")), 13 | ], 14 | targets: [ 15 | .target(name: "Cassowary", dependencies: [ 16 | .product(name: "OrderedCollections", package: "swift-collections") 17 | ]), 18 | .testTarget(name: "CassowaryTests", dependencies: ["Cassowary"]), 19 | ] 20 | ) 21 | -------------------------------------------------------------------------------- /Sources/Cassowary/Variable.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | public final class Variable { 5 | public var name: String 6 | public var value: Double 7 | 8 | public init(_ name: String, _ value: Double = 0.0) { 9 | self.name = name 10 | self.value = value 11 | } 12 | } 13 | 14 | // MARK: - Hashable 15 | 16 | extension Variable: Hashable { 17 | public func hash(into hasher: inout Hasher) { 18 | hasher.combine(name) 19 | } 20 | } 21 | 22 | // MARK: - Equatable 23 | 24 | extension Variable: Equatable { 25 | public static func == (lhs: Variable, rhs: Variable) -> Bool { 26 | return lhs.name == rhs.name && lhs.value == rhs.value 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/Cassowary/Strength.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | public typealias Strength = Double 5 | 6 | public extension Strength { 7 | init(_ a: Double, _ b: Double, _ c: Double, _ w: Double = 1.0) { 8 | self = max(0.0, min(1000.0, a * w)) * 1000000.0 9 | + max(0.0, min(1000.0, b * w)) * 1000.0 10 | + max(0.0, min(1000.0, c * w)) * 1.0 11 | } 12 | } 13 | 14 | public extension Strength { 15 | static let required: Strength = Strength(1000.0, 1000.0, 1000.0) 16 | static let strong: Strength = Strength(1.0, 0.0, 0.0) 17 | static let medium: Strength = Strength(0.0, 1.0, 0.0) 18 | static let weak: Strength = Strength(0.0, 0.0, 1.0) 19 | } 20 | 21 | internal extension Strength { 22 | static func clip(_ value: Strength) -> Strength { 23 | return max(0.0, min(Strength.required, value)) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /Sources/Cassowary/Term.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | public final class Term { 5 | public let variable: Variable 6 | public let coefficient: Double 7 | 8 | public var value: Double { 9 | return self.coefficient * self.variable.value 10 | } 11 | 12 | public init(_ variable: Variable, _ coefficient: Double = 1.0) { 13 | self.variable = variable 14 | self.coefficient = coefficient 15 | } 16 | } 17 | 18 | // MARK: - Hashable 19 | 20 | extension Term: Hashable { 21 | public func hash(into hasher: inout Hasher) { 22 | hasher.combine(variable) 23 | hasher.combine(coefficient) 24 | } 25 | } 26 | 27 | // MARK: - Equatable 28 | 29 | extension Term: Equatable { 30 | public static func == (lhs: Term, rhs: Term) -> Bool { 31 | return lhs.variable == rhs.variable && lhs.coefficient == rhs.coefficient 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | windows: 11 | runs-on: windows-latest 12 | 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | include: 17 | - branch: swift-5.4.3-release 18 | tag: 5.4.3-RELEASE 19 | options: '-Xmanifest -use-ld=link -Xswiftc -use-ld=link' 20 | 21 | - branch: swift-5.5.3-release 22 | tag: 5.5.3-RELEASE 23 | options: '-Xmanifest -use-ld=link -Xswiftc -use-ld=link' 24 | 25 | - branch: development 26 | tag: DEVELOPMENT-SNAPSHOT-2023-08-12-a 27 | options: '' 28 | 29 | steps: 30 | - uses: compnerd/gha-setup-swift@main 31 | with: 32 | tag: ${{ matrix.tag }} 33 | branch: ${{ matrix.branch }} 34 | 35 | - uses: actions/checkout@v3 36 | 37 | - name: Build 38 | run: swift build ${{ matrix.options }} 39 | 40 | - name: Test 41 | run: swift test ${{ matrix.options }} 42 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | coverage: 14 | runs-on: windows-latest 15 | 16 | strategy: 17 | matrix: 18 | include: 19 | - branch: development 20 | tag: DEVELOPMENT-SNAPSHOT-2023-08-12-a 21 | options: '' 22 | 23 | steps: 24 | - uses: compnerd/gha-setup-swift@main 25 | with: 26 | tag: ${{ matrix.tag }} 27 | branch: ${{ matrix.branch }} 28 | 29 | - uses: actions/checkout@v3 30 | 31 | - name: Build 32 | run: swift build ${{ matrix.options }} 33 | 34 | - name: Test 35 | run: swift test --enable-code-coverage ${{ matrix.options }} 36 | 37 | - name: Process Coverage 38 | run: | 39 | llvm-cov export -format lcov -ignore-filename-regex ".build|Tests" -instr-profile .build\x86_64-unknown-windows-msvc\debug\codecov\default.profdata .build\x86_64-unknown-windows-msvc\debug\CassowaryPackageTests.xctest > coverage.lcov 40 | 41 | - uses: codecov/codecov-action@v3 42 | with: 43 | files: coverage.lcov 44 | -------------------------------------------------------------------------------- /Sources/Cassowary/Symbol.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | public extension Symbol { 5 | enum `Type` { 6 | case invalid 7 | case external 8 | case slack 9 | case error 10 | case dummy 11 | } 12 | } 13 | 14 | public struct Symbol { 15 | public typealias ID = UInt64 16 | 17 | public let id: ID 18 | public let type: `Type` 19 | 20 | public init(_ type: `Type`, _ id: ID) { 21 | self.id = id 22 | self.type = type 23 | } 24 | } 25 | 26 | extension Symbol { 27 | static let invalid: Symbol = Symbol(.invalid, 0) 28 | } 29 | 30 | extension Symbol: Comparable { 31 | public static func == (lhs: Symbol, rhs: Symbol) -> Bool { 32 | return lhs.id == rhs.id 33 | } 34 | 35 | public static func < (lhs: Symbol, rhs: Symbol) -> Bool { 36 | return lhs.id < rhs.id 37 | } 38 | } 39 | 40 | // MARK: - Hashable 41 | 42 | extension Symbol: Hashable { 43 | public func hash(into hasher: inout Hasher) { 44 | hasher.combine(id) 45 | hasher.combine(type) 46 | } 47 | } 48 | 49 | // MARK: - CustomStringConvertible 50 | 51 | extension Symbol: CustomStringConvertible { 52 | public var description: String { 53 | switch type { 54 | case .invalid: return "i\(id)" 55 | case .external: return "v\(id)" 56 | case .slack: return "s\(id)" 57 | case .error: return "e\(id)" 58 | case .dummy: return "d\(id)" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright © 2019 Saleem Abdulrasool . 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Sources/Cassowary/Expression.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | @_implementationOnly 5 | import OrderedCollections 6 | 7 | /// An expression is a linear combination of terms and a constant. 8 | public struct Expression { 9 | /// The terms of the expression. 10 | public let terms: [Term] 11 | 12 | /// The constant of the expression. 13 | public let constant: Double 14 | 15 | public var value: Double { 16 | return terms.reduce(constant, { $0 + $1.value }) 17 | } 18 | 19 | public init(_ constant: Double = 0.0) { 20 | self.terms = [] 21 | self.constant = constant 22 | } 23 | 24 | public init(_ term: Term, _ constant: Double = 0.0) { 25 | self.init([term], constant) 26 | } 27 | 28 | public init(_ terms: [Term], _ constant: Double = 0.0) { 29 | self.terms = terms 30 | self.constant = constant 31 | } 32 | } 33 | 34 | // MARK: - Hashable 35 | 36 | extension Expression: Hashable { 37 | public func hash(into hasher: inout Hasher) { 38 | hasher.combine(terms) 39 | hasher.combine(constant) 40 | } 41 | } 42 | 43 | // MARK: - Equatable 44 | 45 | extension Expression: Equatable { 46 | public static func == (lhs: Expression, rhs: Expression) -> Bool { 47 | return lhs.terms == rhs.terms && lhs.constant == rhs.constant 48 | } 49 | } 50 | 51 | internal func reduce(_ expression: Expression) -> Expression { 52 | var vars: OrderedDictionary = [:] 53 | for term in expression.terms { 54 | vars[term.variable] = vars[term.variable, default: 0.0] + term.coefficient 55 | } 56 | return Expression(vars.map { Term($0, $1) }, expression.constant) 57 | } 58 | -------------------------------------------------------------------------------- /Sources/Cassowary/Errors.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | /// The constraint cannot be satisfied. 5 | public struct UnsatisfiableConstraint: Error { 6 | private let constraint: Constraint 7 | public init(_ constraint: Constraint) { 8 | self.constraint = constraint 9 | } 10 | } 11 | 12 | // MARK: - CustomStringConvertible 13 | 14 | extension UnsatisfiableConstraint: CustomStringConvertible { 15 | public var description: String { 16 | return "unable to satisfy constraint: \(constraint)" 17 | } 18 | } 19 | 20 | /// The constraint is not currently in the system. 21 | public struct UnknownConstraint: Error { 22 | private let constraint: Constraint 23 | public init(_ constraint: Constraint) { 24 | self.constraint = constraint 25 | } 26 | } 27 | 28 | // MARK: - CustomStringConvertible 29 | 30 | extension UnknownConstraint: CustomStringConvertible { 31 | public var description: String { 32 | return "unknown constraint: \(constraint)" 33 | } 34 | } 35 | 36 | /// The constraint is already in the system. 37 | public struct DuplicateConstraint: Error { 38 | private let constraint: Constraint 39 | public init(_ constraint: Constraint) { 40 | self.constraint = constraint 41 | } 42 | } 43 | 44 | // MARK: - CustomStringConvertible 45 | 46 | extension DuplicateConstraint: CustomStringConvertible { 47 | public var description: String { 48 | return "duplicate constraint: \(constraint)" 49 | } 50 | } 51 | 52 | /// The edit variable is not in the system. 53 | public struct UnknownEditVariable: Error { 54 | public init(_ variable: Variable) { 55 | } 56 | } 57 | 58 | /// The edit variable is already in the system. 59 | public struct DuplicateEditVariable: Error { 60 | public init(_ variable: Variable) { 61 | } 62 | } 63 | 64 | /// The required strength is invalid. 65 | public struct BadRequiredStrength: Error { 66 | } 67 | 68 | /// An internal solver error occurred. 69 | internal struct InternalSolverError: Error { 70 | public init(_ message: String) { 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Sources/Cassowary/Constraint.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | public extension Constraint { 5 | enum Relationship { 6 | case le 7 | case ge 8 | case eq 9 | } 10 | } 11 | 12 | /// Describes a constraint over variables in the system. 13 | public final class Constraint { 14 | /// The expression that is constrained. 15 | public let expression: Expression 16 | 17 | /// The relationship between the expression and the constant. 18 | public let operation: Constraint.Relationship 19 | 20 | /// The strength of the constraint. 21 | public let strength: Strength 22 | 23 | public init(_ expression: Expression, _ operation: Constraint.Relationship, 24 | _ strength: Strength = .required) { 25 | self.expression = reduce(expression) 26 | self.operation = operation 27 | self.strength = Strength.clip(strength) 28 | } 29 | 30 | public init(_ constraint: Constraint, _ strength: Strength) { 31 | self.expression = constraint.expression 32 | self.operation = constraint.operation 33 | self.strength = Strength.clip(strength) 34 | } 35 | } 36 | 37 | // MARK: - Hashable 38 | 39 | extension Constraint: Hashable { 40 | public func hash(into hasher: inout Hasher) { 41 | hasher.combine(expression) 42 | hasher.combine(operation) 43 | hasher.combine(strength) 44 | } 45 | } 46 | 47 | // MARK: - Equatable 48 | 49 | extension Constraint: Equatable { 50 | public static func == (lhs: Constraint, rhs: Constraint) -> Bool { 51 | return lhs.expression == rhs.expression && 52 | lhs.operation == rhs.operation && 53 | lhs.strength == rhs.strength 54 | } 55 | } 56 | 57 | // MARK: - CustomStringConvertible 58 | 59 | extension Constraint: CustomStringConvertible { 60 | public var description: String { 61 | var value: String = expression.terms.reduce("") { 62 | $0 + "\($1.coefficient) * \($1.variable.name) + " 63 | } 64 | value += String(describing: expression.constant) 65 | switch operation { 66 | case .le: value += " <= 0 " 67 | case .ge: value += " >= 0 " 68 | case .eq: value += " == 0 " 69 | } 70 | value += " | strength = \(strength)" 71 | return value 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cassowary 2 | 3 | This is a Swift implementation of the cassowary[1](#f1) 4 | simplex solver inspired by the C++ implementation, Kiwi[2](#f2). 5 | 6 | ## Constraints 7 | 8 | Cassowary supports linear equations and non-strict inequalities. Additionally, 9 | a strength may be associated with each constraint in the system, constrolling 10 | its importance to the overall solution to the system. 11 | 12 | ### Defining Variables and Constraints 13 | 14 | Variables are the values which the solver is trying to resolve. These 15 | correspond to the `Variable` type in the implementation. The variables can be 16 | used to create the expressions which form constraints of the system. These must 17 | be added to an instance of the solver. 18 | 19 | ```swift 20 | import cassowary 21 | 22 | let simplex: Solver = Solver() 23 | 24 | let x_l: Variable = Variable("x_l") 25 | let x_r: Variable = Variable("x_r") 26 | let x_m: Variable = Variable("x_m") 27 | 28 | simplex.add(constraint: 2.0 * x_m == x_l + x_r) 29 | simplex.add(constraint: x_l + 10.0 <= x_r) 30 | simplex.add(constraint: x_l >= -10.0) 31 | simplex.add(constraint: x_r <= 100.0) 32 | ``` 33 | 34 | This creates a system with three variables (xl, xr, 35 | xm) representings points on a line segment. xm is 36 | constrained to the midpoint between xl and xr, 37 | xl is constrained to be at least 10 to the left of xr, and 38 | all variables must lie in the range [-10, 100]. All constraints must be 39 | satisfied and are considered as `required` by the cassowary algorithm. 40 | 41 | **NOTE** The same constraint in the same form cannot be added to the solver 42 | multiply. Redundant constraints, as per cassowary, are supported. That is, the 43 | following set of constraints can be added to the solver: 44 | 45 | ``` 46 | x == 10 47 | x + y == 30 48 | y == 20 49 | ``` 50 | 51 | ### Managing Constraint Strength 52 | 53 | Cassowary supports constraints which are not required but are handled as 54 | best-effort. Such a constraint is modelled as having a _strength_ other than 55 | `required`. The constraints are considered in order of the value of their 56 | strengths. Three standard strengths are defined by default: 57 | 1. `strong` 58 | 1. `medium` 59 | 1. `weak` 60 | 61 | We can add a constraint to our previous example to place xm at 50 by 62 | adding a new `weak` constraint: 63 | 64 | ```swift 65 | simplex.add(constraint: x_m == 50.0, strength: .weak) 66 | ``` 67 | 68 | ### Edit Variables 69 | 70 | The system described thus far has been static. In order to find solutions for 71 | particular value of xm, Cassowary provides the concept of _edit 72 | variables_ which allows you to suggest values for the variable before evaluating 73 | the system. These variables can have any strength other than `required`. 74 | 75 | Continuing our example, we could make xm editable and suggest a value 76 | of `60` for it. 77 | 78 | ```swift 79 | simplex.add(variable: x_m, .strong) 80 | simplex.suggest(value: 60.0, for: x_m) 81 | ``` 82 | 83 | ### Solving and Updating Variables 84 | 85 | This implementation solves the system each time a constraint is added or 86 | removed, or when a new value is suggested for an edit variable. However, the 87 | variable values are not updated automatically and you must request the solver to 88 | update the values. 89 | 90 | ```swift 91 | simplex.suggest(value: 90, for: x_m) 92 | simplex.update() 93 | ``` 94 | 95 | # 96 | 1 https://constraints.cs.washington.edu/solvers/cassowary-tochi.pdf [↩](#a1)
97 | 2 https://github.com/nucleic/kiwi [↩](#a2) 98 | 99 | -------------------------------------------------------------------------------- /Sources/Cassowary/Row.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | @_implementationOnly 5 | import OrderedCollections 6 | 7 | internal final class Row { 8 | // Must be in insertion order 9 | public private(set) var cells: OrderedDictionary = [:] 10 | public private(set) var constant: Double 11 | 12 | public init() { 13 | self.constant = 0.0 14 | } 15 | 16 | public init(_ constant: Double) { 17 | self.constant = constant 18 | } 19 | 20 | public init(copy rhs: Row) { 21 | self.cells = rhs.cells 22 | self.constant = rhs.constant 23 | } 24 | 25 | /// Add a constant value to the row constant. 26 | /// 27 | /// The new value of the constant is returned. 28 | public func add(_ value: Double) -> Double { 29 | constant += value 30 | return self.constant 31 | } 32 | 33 | /// Insert a symbol into the row with a given coefficient. 34 | /// 35 | /// If the symbol already exists in the row, the coefficient will be added to 36 | /// the existing coefficient. If the resulting coefficient is zero, the 37 | /// symbol will be removed from the row. 38 | public func insert(symbol: Symbol, _ coefficient: Double = 1.0) { 39 | cells[symbol] = cells[symbol, default: 0.0] + coefficient 40 | if cells[symbol]! ~= 0.0 { 41 | cells.removeValue(forKey: symbol) 42 | } 43 | } 44 | 45 | /// Insert a row into this row with a given coefficient. 46 | /// 47 | /// The constant and the cells of the inserted row will be multiplied by the 48 | /// coefficient and be added to this row. Any cell with a resulting 49 | /// coefficient of zero will be removed from the row. 50 | public func insert(row: Row, _ coefficient: Double = 1.0) { 51 | constant = constant + (row.constant * coefficient) 52 | 53 | for (r, c) in row.cells { 54 | cells[r] = cells[r, default: 0.0] + (c * coefficient) 55 | if cells[r]! ~= 0.0 { 56 | cells.removeValue(forKey: r) 57 | } 58 | } 59 | } 60 | 61 | /// Remove the given symbol from the row. 62 | public func remove(_ symbol: Symbol) { 63 | cells.removeValue(forKey: symbol) 64 | } 65 | 66 | /// Invert the sign of the constant and all cells in the row. 67 | public func invert() { 68 | constant = -1 * constant 69 | cells = cells.mapValues { -1 * $0 } 70 | } 71 | 72 | /// Solve the row for the given symbol. 73 | /// 74 | /// This method assymes that the row is of the form ax + by + c = 0 and 75 | /// (assuming solve for x) will modify the row to represent the right hand 76 | /// side of the x = -b/a * y - c/a. The target symbol will be removed from the 77 | /// row, and the constant and other clls wiill be multipied by the negative 78 | /// inverse of the target coefficient. 79 | /// 80 | /// The given symbol *must* exist in the row. 81 | public func solve(for symbol: Symbol) { 82 | let coefficient: Double = -1.0 / cells[symbol]! 83 | cells.removeValue(forKey: symbol) 84 | constant *= coefficient 85 | cells = cells.mapValues { $0 * coefficient } 86 | } 87 | 88 | /// Solve the row for the given symbols. 89 | /// 90 | /// This method assumes that he row is of the form x = by + c and will solve 91 | /// the row such that y = x/b - c/b. The rhs symbol will be removed from the 92 | /// row, the lhs added, and the result divided by the negative inverse of the 93 | /// rhs coefficient. 94 | /// 95 | /// The lhs symbol *must not* exist in the row, and the rhs symbol *must* 96 | /// exist in the row. 97 | public func solve(for lhs: Symbol, and rhs: Symbol) { 98 | insert(symbol: lhs, -1.0) 99 | solve(for: rhs) 100 | } 101 | 102 | /// Get the coefficient for the given symbol 103 | /// 104 | /// If the symbol does not exist in the row, zero will be returned. 105 | public func coefficient(for symbol: Symbol) -> Double { 106 | return cells[symbol] ?? 0.0 107 | } 108 | 109 | /// Substitute a symbol with the data from another row. 110 | /// 111 | /// Given a row of the form ax + b and a substitution of the form x = 3y + c 112 | /// the row will be updated to reflect the expression 3ay + ac + b. 113 | /// 114 | /// If the symbol does not exist in the row, this is a no-op. 115 | public func substitute(symbol: Symbol, from row: Row) { 116 | if let coefficient = cells[symbol] { 117 | cells.removeValue(forKey: symbol) 118 | insert(row: row, coefficient) 119 | } 120 | } 121 | } 122 | 123 | // MARK: - CustomStringConvertible 124 | 125 | extension Row: CustomStringConvertible { 126 | public var description: String { 127 | return cells.reduce("\(constant)") { 128 | return $0 + " + \($1.value) * \(String(describing: $1.key))" 129 | } 130 | } 131 | } 132 | 133 | -------------------------------------------------------------------------------- /Tests/CassowaryTests/CassowaryTests.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | import Cassowary 5 | import XCTest 6 | 7 | final class CassowaryTests: XCTestCase { 8 | func testSimple1() throws { 9 | let solver: Solver = Solver() 10 | 11 | let x: Variable = Variable("x") 12 | 13 | try solver.add(constraint: x + 2.0 == 20.0) 14 | 15 | solver.update() 16 | 17 | XCTAssertEqual(x.value, 18.0) 18 | } 19 | 20 | func testSimple2() throws { 21 | let solver: Solver = Solver() 22 | 23 | let x: Variable = Variable("x") 24 | let y: Variable = Variable("y") 25 | 26 | try solver.add(constraint: x == 20.0) 27 | try solver.add(constraint: x + 2.0 == y + 10.0) 28 | 29 | solver.update() 30 | 31 | XCTAssertEqual(x.value, 20.0) 32 | XCTAssertEqual(y.value, 12.0) 33 | } 34 | 35 | func testSimple3() throws { 36 | let solver: Solver = Solver() 37 | 38 | let x: Variable = Variable("x") 39 | let y: Variable = Variable("y") 40 | 41 | try solver.add(constraint: x <= y) 42 | try solver.add(constraint: y == x + 3.0) 43 | try solver.add(constraint: x == 10.0, strength: .weak) 44 | try solver.add(constraint: y == 10.0, strength: .weak) 45 | 46 | solver.update() 47 | 48 | if x.value == 10.0 { 49 | XCTAssertEqual(x.value, 10.0) 50 | XCTAssertEqual(y.value, 13.0) 51 | } else { 52 | XCTAssertEqual(x.value, 7.0) 53 | XCTAssertEqual(y.value, 10.0) 54 | } 55 | } 56 | 57 | func testComplex1() throws { 58 | let solver: Solver = Solver() 59 | 60 | let x: Variable = Variable("x") 61 | 62 | try solver.add(constraint: x <= 100.0, strength: .weak) 63 | 64 | solver.update() 65 | 66 | XCTAssertEqual(x.value, 100.0) 67 | 68 | let c10: Constraint = x <= 10.0 69 | let c20: Constraint = x <= 20.0 70 | 71 | try solver.add(constraint: c10) 72 | try solver.add(constraint: c20) 73 | 74 | solver.update() 75 | 76 | XCTAssertEqual(x.value, 10.0) 77 | 78 | try solver.remove(constraint: c10) 79 | 80 | solver.update() 81 | 82 | XCTAssertEqual(x.value, 20.0) 83 | 84 | try solver.remove(constraint: c20) 85 | 86 | solver.update() 87 | 88 | XCTAssertEqual(x.value, 100.0) 89 | } 90 | 91 | func testComplex2() throws { 92 | let solver: Solver = Solver() 93 | 94 | let x: Variable = Variable("x") 95 | let y: Variable = Variable("y") 96 | 97 | try solver.add(constraint: x == 100, strength: .weak) 98 | try solver.add(constraint: y == 120, strength: .strong) 99 | 100 | let c10: Constraint = x <= 10.0 101 | let c20: Constraint = x <= 20.0 102 | 103 | try solver.add(constraint: c10) 104 | try solver.add(constraint: c20) 105 | 106 | solver.update() 107 | 108 | XCTAssertEqual(x.value, 10.0) 109 | XCTAssertEqual(y.value, 120.0) 110 | 111 | try solver.remove(constraint: c10) 112 | 113 | solver.update() 114 | 115 | XCTAssertEqual(x.value, 20.0) 116 | XCTAssertEqual(y.value, 120.0) 117 | 118 | let cxy: Constraint = x * 2.0 == y 119 | 120 | try solver.add(constraint: cxy) 121 | 122 | solver.update() 123 | 124 | XCTAssertEqual(x.value, 20.0) 125 | XCTAssertEqual(y.value, 40.0) 126 | 127 | try solver.remove(constraint: c20) 128 | 129 | solver.update() 130 | 131 | XCTAssertEqual(x.value, 60.0) 132 | XCTAssertEqual(y.value, 120.0) 133 | 134 | try solver.remove(constraint: cxy) 135 | 136 | solver.update() 137 | 138 | XCTAssertEqual(x.value, 100.0) 139 | XCTAssertEqual(y.value, 120.0) 140 | } 141 | 142 | func testUnderConstrainedSystem() throws { 143 | let solver: Solver = Solver() 144 | let v: Variable = Variable("v") 145 | let c: Constraint = 2.0 * v + 1.0 >= 0.0 146 | 147 | try solver.add(variable: v, strength: .weak) 148 | try solver.add(constraint: c) 149 | try solver.suggest(value: 10, for: v) 150 | 151 | solver.update() 152 | 153 | XCTAssertEqual(c.expression.value, 21) 154 | XCTAssertEqual(c.expression.terms[0].value, 20) 155 | XCTAssertEqual(c.expression.terms[0].variable.value, 10) 156 | } 157 | 158 | func testWithStrength() throws { 159 | let solver: Solver = Solver() 160 | let v: Variable = Variable("v") 161 | let w: Variable = Variable("w") 162 | 163 | try solver.add(constraint: v + w == 0.0) 164 | try solver.add(constraint: v == 10.0) 165 | try solver.add(constraint: w >= 0.0, strength: .weak) 166 | 167 | solver.update() 168 | 169 | XCTAssertEqual(v.value, 10) 170 | XCTAssertEqual(w.value, -10) 171 | } 172 | 173 | func testWithStrength2() throws { 174 | let solver: Solver = Solver() 175 | 176 | let v: Variable = Variable("v") 177 | let w: Variable = Variable("w") 178 | 179 | try solver.add(constraint: v + w == 0.0) 180 | try solver.add(constraint: v >= 10.0, strength: .medium) 181 | try solver.add(constraint: w == 2.0, strength: .strong) 182 | 183 | solver.update() 184 | 185 | XCTAssertEqual(v.value, -2) 186 | XCTAssertEqual(w.value, 2) 187 | } 188 | 189 | func testHandlingInfeasibleConstraints() throws { 190 | let solver: Solver = Solver() 191 | 192 | let x_m: Variable = Variable("xm") 193 | let x_l: Variable = Variable("xl") 194 | let x_r: Variable = Variable("xr") 195 | 196 | try solver.add(variable: x_m, strength: .strong) 197 | try solver.add(variable: x_l, strength: .weak) 198 | try solver.add(variable: x_r, strength: .weak) 199 | 200 | try solver.add(constraint: 2.0 * x_m == x_l + x_r) 201 | try solver.add(constraint: x_l + 20.0 <= x_r) 202 | try solver.add(constraint: x_l >= -10.0) 203 | try solver.add(constraint: x_r <= 100.0) 204 | 205 | try solver.suggest(value: 40, for: x_m) 206 | try solver.suggest(value: 50, for: x_r) 207 | try solver.suggest(value: 30, for: x_l) 208 | 209 | // First update causing a normal update. 210 | try solver.suggest(value: 60, for: x_m) 211 | 212 | // Create an infeasible condition, triggering a dual optimization. 213 | try solver.suggest(value: 90, for: x_m) 214 | 215 | solver.update() 216 | 217 | XCTAssertEqual(x_l.value + x_r.value, 2 * x_m.value) 218 | XCTAssertEqual(x_l.value, 80) 219 | XCTAssertEqual(x_r.value, 100) 220 | } 221 | 222 | // Terms must be evaluated in insertion order 223 | func testMultiTermExpressionOrder() throws { 224 | let solver: Solver = Solver() 225 | 226 | let c: Variable = Variable("c") 227 | let a: Variable = Variable("a") 228 | let b: Variable = Variable("b") 229 | 230 | try solver.add(variable: c, strength: .strong) 231 | try solver.add(constraint: a >= 0, .strong) 232 | try solver.add(constraint: b >= a, .strong) 233 | try solver.add(constraint: b - a == c, .required) 234 | try solver.suggest(value: 100, for: c) 235 | 236 | solver.update() 237 | 238 | XCTAssertEqual(a.value, 0) 239 | XCTAssertEqual(b.value, 100) 240 | XCTAssertEqual(c.value, 100) 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /Sources/Cassowary/Symbolics.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | // MARK: - epsilon check 5 | 6 | infix operator ~= 7 | 8 | public func ~= (_ lhs: Double, _ rhs: Double) -> Bool { 9 | let epsilon: Double = 1.0e-8 10 | return abs(lhs - rhs) <= epsilon 11 | } 12 | 13 | // MARK: - expression operations 14 | 15 | public prefix func - (_ expression: Expression) -> Expression { 16 | return expression * -1.0 17 | } 18 | 19 | public func + (_ lhs: Expression, _ rhs: Expression) -> Expression { 20 | return Expression(lhs.terms + rhs.terms, lhs.constant + rhs.constant) 21 | } 22 | 23 | public func + (_ lhs: Expression, _ rhs: Term) -> Expression { 24 | return Expression(lhs.terms + [rhs], lhs.constant) 25 | } 26 | 27 | public func + (_ lhs: Expression, _ rhs: Variable) -> Expression { 28 | return lhs + Term(rhs) 29 | } 30 | 31 | public func + (_ lhs: Expression, _ rhs: Double) -> Expression { 32 | return Expression(lhs.terms, lhs.constant + rhs) 33 | } 34 | 35 | public func - (_ lhs: Expression, _ rhs: Expression) -> Expression { 36 | return lhs + -rhs 37 | } 38 | 39 | public func - (_ lhs: Expression, _ rhs: Term) -> Expression { 40 | return lhs + -rhs 41 | } 42 | 43 | public func - (_ lhs: Expression, _ rhs: Variable) -> Expression { 44 | return lhs + -rhs 45 | } 46 | 47 | public func - (_ lhs: Expression, _ rhs: Double) -> Expression { 48 | return lhs + -rhs 49 | } 50 | 51 | public func * (_ expression: Expression, _ coefficient: Double) -> Expression { 52 | return Expression(expression.terms.map { $0 * coefficient }, 53 | expression.constant * coefficient) 54 | } 55 | 56 | public func / (_ expression: Expression, _ denominator: Double) -> Expression { 57 | return expression * (1.0 / denominator) 58 | } 59 | 60 | // MARK: - term operations 61 | 62 | public prefix func - (_ term: Term) -> Term { 63 | return term * -1.0 64 | } 65 | 66 | public func + (_ lhs: Term, _ rhs: Expression) -> Expression { 67 | return rhs + lhs 68 | } 69 | 70 | public func + (_ lhs: Term, _ rhs: Term) -> Expression { 71 | return Expression([lhs, rhs]) 72 | } 73 | 74 | public func + (_ lhs: Term, _ rhs: Variable) -> Expression { 75 | return lhs + Term(rhs) 76 | } 77 | 78 | public func + (_ lhs: Term, _ rhs: Double) -> Expression { 79 | return Expression(lhs, rhs) 80 | } 81 | 82 | public func - (_ lhs: Term, _ rhs: Expression) -> Expression { 83 | return -rhs + lhs 84 | } 85 | 86 | public func - (_ lhs: Term, _ rhs: Term) -> Expression { 87 | return lhs + -rhs 88 | } 89 | 90 | public func - (_ lhs: Term, _ rhs: Variable) -> Expression { 91 | return lhs + -rhs 92 | } 93 | 94 | public func - (_ lhs: Term, _ rhs: Double) -> Expression { 95 | return lhs + -rhs 96 | } 97 | 98 | public func * (_ term: Term, _ coefficient: Double) -> Term { 99 | return Term(term.variable, term.coefficient * coefficient) 100 | } 101 | 102 | public func / (_ term: Term, _ denominator: Double) -> Term { 103 | return term * (1.0 / denominator) 104 | } 105 | 106 | // MARK: - variable operations 107 | 108 | public prefix func - (_ variable: Variable) -> Term { 109 | return variable * -1.0 110 | } 111 | 112 | public func + (_ lhs: Variable, _ rhs: Expression) -> Expression { 113 | return rhs + lhs 114 | } 115 | 116 | public func + (_ lhs: Variable, _ rhs: Term) -> Expression { 117 | return rhs + lhs 118 | } 119 | 120 | public func + (_ lhs: Variable, _ rhs: Variable) -> Expression { 121 | return Term(lhs) + rhs 122 | } 123 | 124 | public func + (_ lhs: Variable, _ rhs: Double) -> Expression { 125 | return Term(lhs) + rhs 126 | } 127 | 128 | public func - (_ lhs: Variable, _ rhs: Expression) -> Expression { 129 | return lhs + -rhs 130 | } 131 | 132 | public func - (_ lhs: Variable, _ rhs: Term) -> Expression { 133 | return lhs + -rhs 134 | } 135 | 136 | public func - (_ lhs: Variable, _ rhs: Variable) -> Expression { 137 | return lhs + -rhs 138 | } 139 | 140 | public func - (_ lhs: Variable, _ rhs: Double) -> Expression { 141 | return lhs + -rhs 142 | } 143 | 144 | public func * (_ variable: Variable, _ coefficient: Double) -> Term { 145 | return Term(variable, coefficient) 146 | } 147 | 148 | public func / (_ variable: Variable, _ denominator: Double) -> Term { 149 | return variable * (1.0 / denominator) 150 | } 151 | 152 | // MARK: - double operations 153 | 154 | public func + (_ lhs: Double, _ rhs: Expression) -> Expression { 155 | return rhs + lhs 156 | } 157 | 158 | public func + (_ lhs: Double, _ rhs: Term) -> Expression { 159 | return rhs + lhs 160 | } 161 | 162 | public func + (_ lhs: Double, _ rhs: Variable) -> Expression { 163 | return rhs + lhs 164 | } 165 | 166 | public func - (_ lhs: Double, _ rhs: Expression) -> Expression { 167 | return -rhs + lhs 168 | } 169 | 170 | public func - (_ lhs: Double, _ rhs: Term) -> Expression { 171 | return -rhs + lhs 172 | } 173 | 174 | public func - (_ lhs: Double, _ rhs: Variable) -> Expression { 175 | return -rhs + lhs 176 | } 177 | 178 | public func * (_ coefficient: Double, _ expression: Expression) -> Expression { 179 | return expression * coefficient 180 | } 181 | 182 | public func * (_ coefficient: Double, _ term: Term) -> Term { 183 | return term * coefficient 184 | } 185 | 186 | public func * (_ coefficient: Double, _ variable: Variable) -> Term { 187 | return variable * coefficient 188 | } 189 | 190 | // MARK: - expression constraints 191 | 192 | public func == (_ lhs: Expression, _ rhs: Expression) -> Constraint { 193 | return Constraint(lhs - rhs, .eq) 194 | } 195 | 196 | public func == (_ lhs: Expression, _ rhs: Term) -> Constraint { 197 | return lhs == Expression(rhs) 198 | } 199 | 200 | public func == (_ lhs: Expression, _ rhs: Variable) -> Constraint { 201 | return lhs == Term(rhs) 202 | } 203 | 204 | public func == (_ lhs: Expression, _ rhs: Double) -> Constraint { 205 | return lhs == Expression(rhs) 206 | } 207 | 208 | public func <= (_ lhs: Expression, _ rhs: Expression) -> Constraint { 209 | return Constraint(lhs - rhs, .le) 210 | } 211 | 212 | public func <= (_ lhs: Expression, _ rhs: Term) -> Constraint { 213 | return lhs <= Expression(rhs) 214 | } 215 | 216 | public func <= (_ lhs: Expression, _ rhs: Variable) -> Constraint { 217 | return lhs <= Term(rhs) 218 | } 219 | 220 | public func <= (_ lhs: Expression, _ rhs: Double) -> Constraint { 221 | return lhs <= Expression(rhs) 222 | } 223 | 224 | public func >= (_ lhs: Expression, _ rhs: Expression) -> Constraint { 225 | return Constraint(lhs - rhs, .ge) 226 | } 227 | 228 | public func >= (_ lhs: Expression, _ rhs: Term) -> Constraint { 229 | return lhs >= Expression(rhs) 230 | } 231 | 232 | public func >= (_ lhs: Expression, _ rhs: Variable) -> Constraint { 233 | return lhs >= Term(rhs) 234 | } 235 | 236 | public func >= (_ lhs: Expression, _ rhs: Double) -> Constraint { 237 | return lhs >= Expression(rhs) 238 | } 239 | 240 | // MARK: - term constraints 241 | 242 | public func == (_ lhs: Term, _ rhs: Expression) -> Constraint { 243 | return rhs == lhs 244 | } 245 | 246 | public func == (_ lhs: Term, _ rhs: Term) -> Constraint { 247 | return Expression(lhs) == rhs 248 | } 249 | 250 | public func == (_ lhs: Term, _ rhs: Variable) -> Constraint { 251 | return Expression(lhs) == rhs 252 | } 253 | 254 | public func == (_ lhs: Term, _ rhs: Double) -> Constraint { 255 | return Expression(lhs) == rhs 256 | } 257 | 258 | public func <= (_ lhs: Term, _ rhs: Expression) -> Constraint { 259 | return rhs >= lhs 260 | } 261 | 262 | public func <= (_ lhs: Term, _ rhs: Term) -> Constraint { 263 | return Expression(lhs) <= rhs 264 | } 265 | 266 | public func <= (_ lhs: Term, _ rhs: Variable) -> Constraint { 267 | return Expression(lhs) <= rhs 268 | } 269 | 270 | public func <= (_ lhs: Term, _ rhs: Double) -> Constraint { 271 | return Expression(lhs) <= rhs 272 | } 273 | 274 | public func >= (_ lhs: Term, _ rhs: Expression) -> Constraint { 275 | return rhs <= lhs 276 | } 277 | 278 | public func >= (_ lhs: Term, _ rhs: Term) -> Constraint { 279 | return Expression(lhs) >= rhs 280 | } 281 | 282 | public func >= (_ lhs: Term, _ rhs: Variable) -> Constraint { 283 | return Expression(lhs) >= rhs 284 | } 285 | 286 | public func >= (_ lhs: Term, _ rhs: Double) -> Constraint { 287 | return Expression(lhs) >= rhs 288 | } 289 | 290 | // MARK: - Variable constraints 291 | 292 | public func == (_ lhs: Variable, _ rhs: Expression) -> Constraint { 293 | return rhs == lhs 294 | } 295 | 296 | public func == (_ lhs: Variable, _ rhs: Term) -> Constraint { 297 | return rhs == lhs 298 | } 299 | 300 | public func == (_ lhs: Variable, _ rhs: Variable) -> Constraint { 301 | return Term(lhs) == rhs 302 | } 303 | 304 | public func == (_ lhs: Variable, _ rhs: Double) -> Constraint { 305 | return Term(lhs) == rhs 306 | } 307 | 308 | public func <= (_ lhs: Variable, _ rhs: Expression) -> Constraint { 309 | return rhs >= lhs 310 | } 311 | 312 | public func <= (_ lhs: Variable, _ rhs: Term) -> Constraint { 313 | return rhs >= lhs 314 | } 315 | 316 | public func <= (_ lhs: Variable, _ rhs: Variable) -> Constraint { 317 | return Term(lhs) <= rhs 318 | } 319 | 320 | public func <= (_ lhs: Variable, _ rhs: Double) -> Constraint { 321 | return Term(lhs) <= rhs 322 | } 323 | 324 | public func >= (_ lhs: Variable, _ rhs: Expression) -> Constraint { 325 | return rhs <= lhs 326 | } 327 | 328 | public func >= (_ lhs: Variable, _ rhs: Term) -> Constraint { 329 | return rhs <= lhs 330 | } 331 | 332 | public func >= (_ lhs: Variable, _ rhs: Variable) -> Constraint { 333 | return Term(lhs) >= rhs 334 | } 335 | 336 | public func >= (_ lhs: Variable, _ rhs: Double) -> Constraint { 337 | return Term(lhs) >= rhs 338 | } 339 | 340 | // MARK: - double constraints 341 | 342 | public func == (_ lhs: Double, _ rhs: Expression) -> Constraint { 343 | return rhs == lhs 344 | } 345 | 346 | public func == (_ lhs: Double, _ rhs: Term) -> Constraint { 347 | return rhs == lhs 348 | } 349 | 350 | public func == (_ lhs: Double, _ rhs: Variable) -> Constraint { 351 | return rhs == lhs 352 | } 353 | 354 | public func <= (_ lhs: Double, _ rhs: Expression) -> Constraint { 355 | return rhs >= lhs 356 | } 357 | 358 | public func <= (_ lhs: Double, _ rhs: Term) -> Constraint { 359 | return rhs >= lhs 360 | } 361 | 362 | public func <= (_ lhs: Double, _ rhs: Variable) -> Constraint { 363 | return rhs >= lhs 364 | } 365 | 366 | public func >= (_ lhs: Double, _ rhs: Expression) -> Constraint { 367 | return rhs <= lhs 368 | } 369 | 370 | public func >= (_ lhs: Double, _ rhs: Term) -> Constraint { 371 | return rhs <= lhs 372 | } 373 | 374 | public func >= (_ lhs: Double, _ rhs: Variable) -> Constraint { 375 | return rhs <= lhs 376 | } 377 | 378 | // MARK: - constraint strength modifiers 379 | 380 | @available(*, deprecated, message: "explicitly pass strength when adding the constraint") 381 | public func | (_ lhs: Constraint, _ rhs: Strength) -> Constraint { 382 | return Constraint(lhs, rhs) 383 | } 384 | 385 | @available(*, deprecated, message: "explicitly pass strength when adding the constraint") 386 | public func | (_ lhs: Strength, _ rhs: Constraint) -> Constraint { 387 | return Constraint(rhs, lhs) 388 | } 389 | 390 | -------------------------------------------------------------------------------- /Sources/Cassowary/Solver.swift: -------------------------------------------------------------------------------- 1 | // Copyright © 2019 Saleem Abdulrasool . 2 | // SPDX-License-Identifier: BSD-3-Clause 3 | 4 | private typealias Tag = (marker: Symbol, other: Symbol) 5 | 6 | private final class EditInfo { 7 | public var tag: Tag 8 | public var constraint: Constraint 9 | public var constant: Double 10 | 11 | public init(tag: Tag, constraint: Constraint, constant: Double) { 12 | self.tag = tag 13 | self.constraint = constraint 14 | self.constant = constant 15 | } 16 | } 17 | 18 | public final class Solver { 19 | private var rows: [Symbol:Row] = [:] 20 | private var constraints: [Constraint:Tag] = [:] 21 | private var variables: [Variable:Symbol] = [:] 22 | private var edits: [Variable:EditInfo] = [:] 23 | private var infeasible: [Symbol] = [] 24 | private var objective: Row = Row() 25 | private var artificial: Row? 26 | private var id: Symbol.ID = 1 27 | 28 | public init() {} 29 | 30 | private func SymID() -> Symbol.ID { 31 | let value: Symbol.ID = id 32 | id += 1 33 | return value 34 | } 35 | 36 | // MARK - private functions 37 | 38 | // Choose the subject for solving the row. 39 | // 40 | // This method will choose the best target subject to solve the row. An 41 | // invalid symbol will be returned if there is no valid target. 42 | // 43 | // The symbols are chosen according to the following precedence: 44 | // 1) the first symbol representing an external variable. 45 | // 2) a ngative slack or error tag variable. 46 | // 47 | // If a subject cannot be found, an invalid symbol will be returned. 48 | private func subject(for row: Row, with tag: Tag) -> Symbol { 49 | for (symbol, _) in row.cells { 50 | if symbol.type == .external { 51 | return symbol 52 | } 53 | } 54 | 55 | if tag.marker.type == .slack || tag.marker.type == .error { 56 | if row.coefficient(for: tag.marker) < 0.0 { 57 | return tag.marker 58 | } 59 | } 60 | 61 | if tag.other.type == .slack || tag.other.type == .error { 62 | if row.coefficient(for: tag.other) < 0.0 { 63 | return tag.other 64 | } 65 | } 66 | 67 | return .invalid 68 | } 69 | 70 | // Add the row to the tableau using an artificial variable. 71 | // 72 | // This will return `false` if the constraint cannot be satisfied. 73 | private func add(row: Row) -> Bool { 74 | // Create and add the artificial variable to the tableau 75 | let variable: Symbol = Symbol(.slack, SymID()) 76 | rows[variable] = Row(copy: row) 77 | artificial = Row(copy: row) 78 | 79 | // Optimize the artificial objective. This is successful only if the 80 | // artificial objective is optimized to zero. 81 | try! optimize(objective: artificial!) 82 | let success: Bool = artificial!.constant ~= 0.0 83 | artificial = nil 84 | 85 | // If the artificial variable is not basic, pivot the row so that it becomes 86 | // basic If the row is constant, exit early. 87 | if let r: Row = rows[variable] { 88 | rows.removeValue(forKey: variable) 89 | if r.cells.count == 0 { return success } 90 | 91 | let entering: Symbol = pivot(for: r) 92 | // The constraint is unsatisfyable (will this ever happen?) 93 | if entering.type == .invalid { return false } 94 | 95 | r.solve(for: variable, and: entering) 96 | substitute(symbol: entering, for: r) 97 | rows[entering] = r 98 | } 99 | 100 | // Remove the artificial variable from the tableau. 101 | for (_, row) in rows { 102 | row.remove(variable) 103 | } 104 | objective.remove(variable) 105 | 106 | return success 107 | } 108 | 109 | // Get the symbol for the given variable. 110 | private func symbol(for variable: Variable) -> Symbol { 111 | guard let symbol: Symbol = variables[variable] else { 112 | let symbol: Symbol = Symbol(.external, SymID()) 113 | variables[variable] = symbol 114 | return symbol 115 | } 116 | return symbol 117 | } 118 | 119 | // Create a new Row for the given constraint. 120 | // 121 | // The terms in the constaint will be converted to cells in the row. Any 122 | // term in the constraint with a coefficient of zero is ignored. If the 123 | // symbol for a given cell variable is basic, the ell variable will be 124 | // substituted with the basic row. 125 | // 126 | // The necessary slack and error variables will be added to the row. If the 127 | // constant for the row is negative, the sign for the row will be inverted so 128 | // the constant becomes positive. 129 | // 130 | // The tag will be updated with the marker and error symbols to use for 131 | // tracking the movement of the constraint in the tableau. 132 | private func row(for constraint: Constraint) -> (Row, Tag) { 133 | let expression: Expression = constraint.expression 134 | let row: Row = Row(expression.constant) 135 | var tag: Tag = Tag(marker: .invalid, other: .invalid) 136 | 137 | // Substitute the current basic variables into the row. 138 | for term in expression.terms { 139 | if term.coefficient ~= 0.0 { continue } 140 | 141 | let symbol: Symbol = self.symbol(for: term.variable) 142 | if let r = rows[symbol] { 143 | row.insert(row: r, term.coefficient) 144 | } else { 145 | row.insert(symbol: symbol, term.coefficient) 146 | } 147 | } 148 | 149 | // Add the necessary slack, error, and dummy variables. 150 | switch constraint.operation { 151 | case .le, .ge: 152 | let coefficient: Double = constraint.operation == .le ? 1.0 : -1.0 153 | 154 | tag.marker = Symbol(.slack, SymID()) 155 | row.insert(symbol: tag.marker, coefficient) 156 | 157 | if constraint.strength < .required { 158 | tag.other = Symbol(.error, SymID()) 159 | row.insert(symbol: tag.other, -coefficient) 160 | objective.insert(symbol: tag.other, constraint.strength) 161 | } 162 | 163 | case .eq: 164 | if constraint.strength < .required { 165 | tag.marker = Symbol(.error, SymID()) 166 | tag.other = Symbol(.error, SymID()) 167 | 168 | row.insert(symbol: tag.marker, -1.0) // v = eplus - eminus 169 | row.insert(symbol: tag.other, 1.0) // v = eplus + eminus = 0 170 | 171 | objective.insert(symbol: tag.marker, constraint.strength) 172 | objective.insert(symbol: tag.other, constraint.strength) 173 | } else { 174 | tag.marker = Symbol(.dummy, SymID()) 175 | row.insert(symbol: tag.marker) 176 | } 177 | } 178 | 179 | // Ensure that the row has a positive constant. 180 | if row.constant < 0.0 { 181 | row.invert() 182 | } 183 | 184 | return (row, tag) 185 | } 186 | 187 | 188 | // Substitute the parameteric symbol with the given row. 189 | // 190 | // This method will subsitute all instances of the parametric symbol in the 191 | // tableau and the objective function with the given row. 192 | private func substitute(symbol: Symbol, for row: Row) { 193 | for (s, r) in rows { 194 | r.substitute(symbol: symbol, from: row) 195 | if s.type == .external { continue } 196 | if r.constant < 0.0 { 197 | infeasible.append(s) 198 | } 199 | } 200 | 201 | objective.substitute(symbol: symbol, from: row) 202 | if let artificial = artificial { 203 | artificial.substitute(symbol: symbol, from: row) 204 | } 205 | } 206 | 207 | // Optimize the system for the given objective function. 208 | // 209 | // This method performs iterations of Phase 2 of the simplex method until the 210 | // objective function reaches a minimum. 211 | private func optimize(objective: Row) throws { 212 | while true { 213 | let entering: Symbol = variable(for: objective) 214 | if entering.type == .invalid { return } 215 | 216 | guard let (exiting, row) = self.row(entering: entering) else { 217 | throw InternalSolverError("the objective is unbounded") 218 | } 219 | 220 | // Pivot the entering symbol into the basis. 221 | rows.removeValue(forKey: exiting) 222 | row.solve(for: exiting, and: entering) 223 | substitute(symbol: entering, for: row) 224 | rows[entering] = row 225 | } 226 | } 227 | 228 | // Optimize the system using the dual of the simplex method. 229 | // 230 | // The current state of the system should be such that the objective function 231 | // is optimal, but not feasible. THis method will perform an iteration of the 232 | // dual simplex method to make the solution both optional and feasible. 233 | private func optimize() throws { 234 | while infeasible.count > 0 { 235 | let exiting: Symbol = infeasible.popLast()! 236 | guard let row: Row = rows[exiting] else { 237 | continue 238 | } 239 | 240 | if row.constant ~= 0.0 || row.constant < 0.0 { 241 | continue 242 | } 243 | 244 | let entering: Symbol = variable_(for: row) 245 | if entering.type == .invalid { 246 | throw InternalSolverError("dual optimize failed") 247 | } 248 | 249 | // Pivot the entering symbol into the basis. 250 | rows.removeValue(forKey: exiting) 251 | row.solve(for: exiting, and: entering) 252 | substitute(symbol: entering, for: row) 253 | rows[entering] = row 254 | } 255 | } 256 | 257 | // Compute the entering variable for a pivot operation. 258 | // 259 | // This method will return the first symbol in the objective function which is 260 | // non-dummy and has a coefficient less than zero. If no symbol meets the 261 | // criteria, it means that the objective function is at a minimum and an 262 | // invalid symbol is returned. 263 | private func variable(for objective: Row) -> Symbol { 264 | for (symbol, coefficient) in objective.cells { 265 | if symbol.type == .dummy { continue } 266 | if coefficient < 0.0 { 267 | return symbol 268 | } 269 | } 270 | return .invalid 271 | } 272 | 273 | // Compute the entering symbol for the dual of the optimize operation. 274 | // 275 | // This method will return the symol in the row which has a positive 276 | // coefficient and yields the minimum ratio for its respective symbol in the 277 | // objective function. The provided row *must* be infeasible. If no symbol 278 | // is found which mets the criteria, an invlaid symbol is returned. 279 | private func variable_(for objective: Row) -> Symbol { 280 | var ratio: Double = .greatestFiniteMagnitude 281 | var entering: Symbol = .invalid 282 | 283 | for (symbol, coefficient) in objective.cells { 284 | if symbol.type == .dummy { continue } 285 | if coefficient > 0.0 { 286 | let r: Double = self.objective.coefficient(for: symbol) / coefficient 287 | if r < ratio { 288 | ratio = r 289 | entering = symbol 290 | } 291 | } 292 | } 293 | 294 | return entering 295 | } 296 | 297 | // Get the first slack or error symbol in the row. 298 | // 299 | // If no such symbol is present, an invalid symbol will be returned. 300 | private func pivot(for row: Row) -> Symbol { 301 | for (symbol, _) in row.cells { 302 | if symbol.type == .slack || symbol.type == .error { 303 | return symbol 304 | } 305 | } 306 | return .invalid 307 | } 308 | 309 | // Compute the row which holds the exit symbol for a pivot. 310 | // 311 | // This method will return the entry in the row map which holds the exit 312 | // symbol. If no appropriate exit symbol is found, `nil` will be returned. 313 | // This indicates that the object function is unbounded. 314 | private func row(entering: Symbol) -> (exiting: Symbol, row: Row)? { 315 | var entry: (exiting: Symbol, row: Row)? = nil 316 | 317 | var ratio: Double = .greatestFiniteMagnitude 318 | for (symbol, row) in rows { 319 | if symbol.type == .external { continue } 320 | 321 | let coefficient: Double = row.coefficient(for: entering) 322 | if coefficient < 0.0 { 323 | let r: Double = -row.constant / coefficient 324 | if r < ratio { 325 | ratio = r 326 | entry = (exiting: symbol, row: row) 327 | } 328 | } 329 | } 330 | 331 | return entry 332 | } 333 | 334 | // Compute the leaving row for a marker variable. 335 | // 336 | // This method will return the entry in the row map which holds the given 337 | // marker variable. The row will be chosen according to the following 338 | // procedure: 339 | // 340 | // 1) The row with a restricted basic variable and a negative coefficient for 341 | // the maker with the smallest ratio of -constant / coefficient. 342 | // 2) The row with a restricted basic variable and the smallest ratio of 343 | // constant / coefficient. 344 | // 3) The last unrestricted row which contains the marker. 345 | // 346 | // If the marker does not exist in any row, `nil` will be returned. This 347 | // indicates an internal solver error since the marker *should* exist 348 | // somewhere in the tableau. 349 | private func row(leaving: Symbol) -> (marker: Symbol, row: Row)? { 350 | var first: (marker: Symbol, row: Row)? = nil 351 | var second: (marker: Symbol, row: Row)? = nil 352 | var third: (marker: Symbol, row: Row)? = nil 353 | 354 | var r1: Double = .greatestFiniteMagnitude 355 | var r2: Double = .greatestFiniteMagnitude 356 | for (symbol, row) in rows { 357 | let coefficient: Double = row.coefficient(for: leaving) 358 | if coefficient == 0.0 { continue } 359 | 360 | if symbol.type == .external { 361 | third = (symbol, row) 362 | } else if coefficient < 0.0 { 363 | let ratio: Double = -row.constant / coefficient 364 | if ratio < r1 { 365 | r1 = ratio 366 | first = (symbol, row) 367 | } 368 | } else { 369 | let ratio: Double = row.constant / coefficient 370 | if ratio < r2 { 371 | r2 = ratio 372 | second = (symbol, row) 373 | } 374 | } 375 | } 376 | 377 | return first ?? second ?? third 378 | } 379 | 380 | // Remove the effects of a constraint on the objective function. 381 | private func nullify(constraint: Constraint, _ tag: Tag) { 382 | if tag.marker.type == .error { 383 | nullify(symbol: tag.marker, constraint.strength) 384 | } 385 | 386 | if tag.other.type == .error { 387 | nullify(symbol: tag.other, constraint.strength) 388 | } 389 | } 390 | 391 | // Remove the efects of an error marker on the objective function. 392 | private func nullify(symbol: Symbol, _ strength: Double) { 393 | if let row = rows[symbol] { 394 | objective.insert(row: row, -strength) 395 | } else { 396 | objective.insert(symbol: symbol, -strength) 397 | } 398 | } 399 | 400 | // MARK - constraint 401 | 402 | /// Add a constraint to the solver. 403 | public func add(constraint: Constraint) throws { 404 | guard constraints[constraint] == nil else { 405 | throw DuplicateConstraint(constraint) 406 | } 407 | 408 | // Creating a row causes sybols to be reserved for the variables in the 409 | // constraint. 410 | let (row, tag) = self.row(for: constraint) 411 | var subject: Symbol = self.subject(for: row, with: tag) 412 | 413 | // If we did not find a valid entering symbol, one last option is available 414 | // if the entire row is composed of dummy variables. If the constnat of the 415 | // row is zero, then this represents redundant constraints and the new dummy 416 | // marker can enter the basis. If the constant is non-zero, then it 417 | // represents and unsatisfiable constraint. 418 | if subject.type == .invalid && 419 | row.cells.reduce(true, { $0 && $1.key.type == .dummy }) { 420 | if row.constant ~= 0.0 { 421 | throw UnsatisfiableConstraint(constraint) 422 | } 423 | subject = tag.marker 424 | } 425 | 426 | // If an entering symbol still isn't found, then the row must be added using 427 | // an artificial variable. If that fails, then the row represents an 428 | // unsatisfiable constraint. 429 | if subject.type == .invalid { 430 | if !add(row: row) { 431 | throw UnsatisfiableConstraint(constraint) 432 | } 433 | } else { 434 | row.solve(for: subject) 435 | substitute(symbol: subject, for: row) 436 | rows[subject] = row 437 | } 438 | 439 | constraints[constraint] = tag 440 | 441 | // Optimizing after each constraint is added performs less aggregate work 442 | // due to a smaller average system size. It also ensures the solver remains 443 | // in a consistent state. 444 | try optimize(objective: objective) 445 | } 446 | 447 | @available(*, deprecated, message: "renamed add(constraint:strength:)") 448 | public func add(constraint: Constraint, _ strength: Strength) throws { 449 | try add(constraint: constraint, strength: strength) 450 | } 451 | 452 | public func add(constraint: Constraint, strength: Strength) throws { 453 | try add(constraint: Constraint(constraint, strength)) 454 | } 455 | 456 | /// Remove a constraint from the solver 457 | public func remove(constraint: Constraint) throws { 458 | guard let tag: Tag = constraints[constraint] else { 459 | throw UnknownConstraint(constraint) 460 | } 461 | 462 | constraints.removeValue(forKey: constraint) 463 | 464 | // Remove the error effects from the objective fnction *before* pivoting, or 465 | // substitutions into the objective will lead to incorrect solver results. 466 | nullify(constraint: constraint, tag) 467 | 468 | // If the marker is basic, simply drop the row. Otherwise, pivot the marker 469 | // into the basis and then drop the row. 470 | if rows.removeValue(forKey: tag.marker) == nil { 471 | guard let (leaving, row) = row(leaving: tag.marker) else { 472 | throw InternalSolverError("failed to find leaving low") 473 | } 474 | 475 | rows.removeValue(forKey: leaving) 476 | row.solve(for: leaving, and: tag.marker) 477 | substitute(symbol: tag.marker, for: row) 478 | } 479 | 480 | // Optimizing after each constraint is removed ensures that the solver 481 | // remains consistent. It makes the solver API easier to use at asmall 482 | // tradeoff for speed. 483 | try optimize(objective: objective) 484 | } 485 | 486 | /// Tests whether a constraint has been added to the solver. 487 | public func has(constraint: Constraint) -> Bool { 488 | return constraints[constraint] != nil 489 | } 490 | 491 | // MARK - Edit Variable 492 | 493 | /// Add an edit variable to the solver. 494 | //? 495 | /// This method should be called before the `suggest(value:for:)` method is 496 | /// used to supply a suggested value for the given edit variable. 497 | public func add(variable: Variable, strength s: Strength) throws { 498 | guard edits[variable] == nil else { 499 | throw DuplicateEditVariable(variable) 500 | } 501 | 502 | let strength: Strength = Strength.clip(s) 503 | if strength == .required { 504 | throw BadRequiredStrength() 505 | } 506 | 507 | let constraint: Constraint = 508 | Constraint(Expression(Term(variable)), .eq, strength) 509 | try add(constraint: constraint) 510 | edits[variable] = EditInfo(tag: constraints[constraint]!, 511 | constraint: constraint, constant: 0.0) 512 | } 513 | 514 | /// Remove an edit variable from the solver. 515 | public func remove(variable: Variable) throws { 516 | guard let edit: EditInfo = edits[variable] else { 517 | throw UnknownEditVariable(variable) 518 | } 519 | try remove(constraint: edit.constraint) 520 | edits.removeValue(forKey: variable) 521 | } 522 | 523 | /// Tests whether an edit variable has been added to the solver. 524 | public func has(variable: Variable) -> Bool { 525 | return edits[variable] != nil 526 | } 527 | 528 | /// Suggest a value for the given edit variable. 529 | /// 530 | /// This method should be used after an edit variable has been addd to the 531 | /// solver in order to suggest the value for that variable. 532 | public func suggest(value: Double, for variable: Variable) throws { 533 | guard let edit: EditInfo = edits[variable] else { 534 | throw UnknownEditVariable(variable) 535 | } 536 | 537 | defer { try! optimize() } 538 | 539 | let delta: Double = value - edit.constant 540 | edit.constant = value 541 | 542 | // Check first if the positive error variable is basic. 543 | if let row: Row = rows[edit.tag.marker] { 544 | if row.add(-delta) < 0.0 { 545 | infeasible.append(edit.tag.marker) 546 | } 547 | return 548 | } 549 | 550 | // Next, check if the negative error variable is basic. 551 | if let row: Row = rows[edit.tag.other] { 552 | if row.add(delta) < 0.0 { 553 | infeasible.append(edit.tag.other) 554 | } 555 | return 556 | } 557 | 558 | // Otherwise, update each row where the error variable exists 559 | for (symbol, row) in rows { 560 | let coefficient: Double = row.coefficient(for: edit.tag.marker) 561 | if coefficient != 0.0 && row.add(delta * coefficient) < 0.0 && symbol.type != .external { 562 | infeasible.append(symbol) 563 | } 564 | } 565 | } 566 | 567 | /// Update the values of the external solver variables. 568 | public func update() { 569 | for (variable, symbol) in variables { 570 | if let row = rows[symbol] { 571 | variable.value = row.constant 572 | } else { 573 | variable.value = 0 574 | } 575 | } 576 | } 577 | } 578 | 579 | // MARK: - CustomStringConvertible 580 | 581 | extension Solver: CustomStringConvertible { 582 | public var description: String { 583 | return """ 584 | Objective 585 | --------- 586 | \(String(describing: objective)) 587 | 588 | Tableau 589 | ------- 590 | \(String(describing: rows)) 591 | 592 | Infeasible 593 | ---------- 594 | \(String(describing: infeasible)) 595 | 596 | Variables 597 | --------- 598 | \(String(describing: variables)) 599 | 600 | Edit Variables 601 | -------------- 602 | \(String(describing: edits)) 603 | 604 | Constraints 605 | ----------- 606 | """ + 607 | constraints.reduce("\n") { 608 | return $0 + String(describing: $1.key) + "\n" 609 | } 610 | } 611 | } 612 | --------------------------------------------------------------------------------