├── .gitignore ├── README.md ├── project.clj ├── src ├── prolin.clj └── prolin │ ├── chebyshev.clj │ ├── commons_math.clj │ ├── polynomial.clj │ └── protocols.clj └── test └── prolin ├── chebyshev_test.clj ├── commons_math_test.clj ├── polynomial_test.clj └── testutil.clj /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /lib 3 | /classes 4 | /checkouts 5 | pom.xml 6 | pom.xml.asc 7 | *.jar 8 | *.class 9 | .lein-deps-sum 10 | .lein-failures 11 | .lein-plugins 12 | .lein-repl-history 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Prolin 2 | 3 | Prolin is a linear programming library for Clojure. It provides 4 | idiomatic Clojure APIs to formulate and solve linear programming 5 | problems, and a small set of utilties for some common transformations 6 | of LP objective functions and constraints. 7 | 8 | - http://en.wikipedia.org/wiki/Linear_programming 9 | 10 | It uses the two-phase Simplex algorithm provided by Apache Commons 11 | Math as its internal LP solver. 12 | 13 | - http://en.wikipedia.org/wiki/Simplex_algorithm 14 | - http://commons.apache.org/proper/commons-math/userguide/optimization.html 15 | - http://google-opensource.blogspot.com/2009/06/introducing-apache-commons-math.html 16 | 17 | There are several value propositions to using Prolin over Commons Math directly: 18 | 19 | - Idiomatic Clojure API 20 | - Allows the use of arbitrary values (anything with equality 21 | semantics) to identify variables 22 | - Modular, protocol-based design to allow for easy extension to 23 | alternative solver implementations or alternative representations of 24 | constraints and polynomials. 25 | 26 | ## Usage 27 | 28 | The API is centered around a few key protocols. 29 | 30 | #### Polynomials 31 | 32 | An instance of `prolin.protocols.LinearPolynomial` represents a linear 33 | polynomial. LinearPolynomials are a key building block of linear 34 | programming. 35 | 36 | ```clojure 37 | (defprotocol LinearPolynomial 38 | "Representation of a linear polynomial (a polynomial of degree 39 | one). A linear polynomial consists of: 40 | 41 | - Any number of variables, each with a numerical coefficient 42 | - A constant numerical term 43 | 44 | The keys representing variables can be any type that supports good 45 | equality semantics." 46 | 47 | (variables [this] "Return a map of variable identifiers to coefficients.") 48 | (constant [this] "Returns the constant term of the polynomial")) 49 | ``` 50 | 51 | A `prolin.protocols/linear-polynomial` function is provided to construct a 52 | LinearPolynomial from a constant number and a variables map. 53 | 54 | An implementation of `LinearPolynomial` for `java.lang.String` is 55 | provided, allowing strings such as `"x + y - 4"` or `"3x + 4y - 2z"` 56 | to be used anywhere you want a polynomial. Note that the parser is not 57 | sophisticated and is provided mostly for experimentation and testing; 58 | it will only work for basic equations or inequalities (not, for 59 | example, equations with parenthesis, multiple constant terms, etc.) 60 | 61 | The `prolin.polynomial` namespace contains utility functions for: 62 | 63 | - Adding polynomials 64 | - Subtracting polynomials 65 | - Multiplying polynomials by a scalar 66 | - Constructing a 'zero' polynomial with the given variables 67 | - Instantiating a polynomial by plugging in numbers for each of its variables 68 | 69 | #### Constraints 70 | 71 | Linear constraints are linear equalities or inequalities that are used 72 | to restrict the 'feasible region' of a linear programming problem. 73 | 74 | Constraints are represented as the `prolin.protocols.Constraint` 75 | protocol, to allow callers to define implementations that are the best 76 | fit for a particular problem. 77 | 78 | ```clojure 79 | (defprotocol Constraint 80 | "Representation of a linear constraint as an (in)equality" 81 | (relation [this] "Return one of '>=, '<=, or '=") 82 | (polynomial [this] 83 | "Returns a LinearPolynomial representing the variables, 84 | coefficients and constant term of the (in)equality, when it is put 85 | in one of the forms: 86 | 87 | a[0]x[0] + ... + a[n]x[n] + c = 0 88 | a[0]x[0] + ... + a[n]x[n] + c <= 0 89 | a[0]x[0] + ... + a[n]x[n] + c >= 0 90 | 91 | Any linear (in)equality can be algebraically manipulated to this 92 | form without any loss of generality, and it is this form that is 93 | used to represent all linear constraints internally. See 94 | 'prolin.polynomial/subtract' for a function to help transform 95 | arbitrary (in)equalities to this format.")) 96 | ``` 97 | 98 | A `prolin.protocols/constraint` constructor is also provided, to construct a constraint 99 | directly from a polynomial and its relation to 0. 100 | 101 | Additionally, `Constraint` is extended to `java.lang.String` to allow 102 | Strings such as `"x = y"`, `"3x + y => 4"` to be used anywhere you 103 | want a Constraint. Again, note that the parsing of such strings is 104 | naive and intended only for experimentation and testing. 105 | 106 | 107 | #### Solving 108 | 109 | A solution algorithm is provided by an instance of the 110 | `prolin.protocols.Solver` protocol. 111 | 112 | Only one implementation of `Solver` is currently provided; one based 113 | on the Apache Commons Math `SimplexSolver`. You can obtain an instance 114 | of this solver by calling `prolin.commons-math/solver`. `solver` 115 | optionally takes an options map containing the following keys: 116 | 117 | ``` 118 | :epsilon - Amount of error to accept for algorithm convergence. (double value) 119 | :max-ulps - Amount of error to accept in floating point comparisons. (int value) 120 | :cutoff - Values smaller than the cutOff are treated as zero." (double value) 121 | ``` 122 | 123 | If you wish to implement `Solver` for an alternative linear 124 | programming solver or algorithm, see the protocol definition in 125 | `prolin.protocols`. 126 | 127 | Once you have an instance of `Solver`, you can invoke the 128 | `prolin/optimize` function, which takes a solver, an objective 129 | function (as a `LinearPolynomial`), a collection of `Constraints`, and 130 | a boolean (true to minimize the objective, false to maximize it.) 131 | 132 | The `prolin/maximize` and `prolin/minimize` functions have the same 133 | signature, but eliminating the final boolean flag. 134 | 135 | ### Examples 136 | 137 | ```clojure 138 | 139 | (require '[prolin :as p]) 140 | (require '[prolin.protocols :as pp]) 141 | (require '[prolin.commons-math :as cm]) 142 | 143 | ;; Maximize x 144 | (p/optimize (cm/solver) "x" #{"x <= 5", "x >= -2"} false) 145 | ;; => {"x" 5.0} 146 | 147 | ;; Same as above 148 | (p/maximize (cm/solver) "x" #{"x <= 5", "x >= -2"}) 149 | ;; => {"x" 5.0} 150 | 151 | ;; Now minimizing 152 | (p/minimize (cm/solver) "x" #{"x <= 5", "x >= -2"}) 153 | ;; => {"x" -2.0} 154 | 155 | ;; Using more than one variable 156 | (p/maximize (cm/solver) "x" #{"2x = y", "y <= 5" }) 157 | ;; => {"x" 5.0, "y" 2.5} 158 | 159 | ;; Same as above, but constructing objective & constraints directly, 160 | ;; instead of using the String implementations 161 | (p/maximize (cm/solver) 162 | (pp/linear-polynomial 0 {:x 1}) 163 | #{(pp/constraint '= (pp/linear-polynomial 0 {:x 2 :y -1})) 164 | (pp/constraint '<= (pp/linear-polynomial -5 {:y 1}))}) 165 | ;; => {:x 5.0, :y 2.5} 166 | 167 | ;; Throws an ex-info with a :reason of :no-solution if it can't be solved 168 | (p/maximize (cm/solver) "x" #{"x = 3", "x = 4" }) 169 | ;; => Exception! 170 | 171 | ;; Throws an ex-info with a :reason of :unbounded if the solution 172 | ;; is unconstrainted 173 | (p/maximize (cm/solver) "x" #{"x >= 1"}) 174 | ;; => Exception! 175 | 176 | ``` 177 | 178 | ## Development 179 | 180 | To run the `clojure.test` tests, run `lein test` from the project 181 | directory. 182 | 183 | To run the `clojure.test.generative` tests, run `lein with-profile 184 | test generative`. 185 | 186 | ## License 187 | 188 | Copyright © 2013 Luke VanderHart 189 | 190 | Distributed under the Eclipse Public License, the same as Clojure. 191 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject prolin "0.1.3-SNAPSHOT" 2 | :description "A linear programming library for Clojure" 3 | :url "http://github.com/levand/prolin" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.5.1"] 7 | [org.apache.commons/commons-math3 "3.2"]] 8 | :profiles {:test {:dependencies [[org.clojure/test.generative "0.5.0"]]}} 9 | :aliases {"generative" ["run" "-m" "clojure.test.generative.runner" "test"]} 10 | :jvm-opts ^:replace ["-Dclojure.test.generative.msec=30000"]) 11 | -------------------------------------------------------------------------------- /src/prolin.clj: -------------------------------------------------------------------------------- 1 | (ns prolin 2 | (:require [prolin.protocols :as p] 3 | [prolin.polynomial :as poly] 4 | [clojure.string :as s])) 5 | 6 | (defn optimize 7 | "Find the maximum or minimum value of an objective LinearPolynomial, subject to 8 | the given set of Constraints, using the specified solver implementation." 9 | [solver objective constraints minimize?] 10 | (p/optimize solver objective constraints minimize?)) 11 | 12 | (defn minimize 13 | "Find the minimum value of an objective LinearPolynomial, subject to 14 | the given set of Constraints, using the specified solver implementation." 15 | [solver objective constraints] 16 | (p/optimize solver objective constraints true)) 17 | 18 | (defn maximize 19 | "Find the maximum value of an objective LinearPolynomial, subject to 20 | the given set of Constraints, using the specified solver implementation." 21 | [solver objective constraints] 22 | (p/optimize solver objective constraints false)) 23 | 24 | 25 | ;; Extend LinearPolynomial and Constraint to java.lang.String 26 | ;; Uses very rudimetary regex-based parsing 27 | 28 | (defn- parse-variable 29 | "Given a string term, return the variable (as a string). Returns nil 30 | if not present." 31 | [term] 32 | (re-find #"[a-zA-Z]" term)) 33 | 34 | (defn- parse-coefficient 35 | "Given a string term, return the coefficient (as a double). Returns 36 | 1 if the term doesn't have an explicit coefficient." 37 | [term] 38 | (let [stripped (s/replace term #"\s" "") 39 | c (re-find #"[+\-]?[0-9]*\.?[0-9]+" stripped)] 40 | (cond 41 | c (Double/parseDouble c) 42 | (re-find #"-" term) -1.0 43 | :else 1.0))) 44 | 45 | (defn- parse-term 46 | "Parse a term and return a tuple of [variable coefficient]" 47 | [term] 48 | [(parse-variable term) (parse-coefficient term)]) 49 | 50 | (defn- parse-terms 51 | "Given a string, return individual terms (as strings)" 52 | [s] 53 | (re-seq #"[+\-]?\s*[0-9a-zA-z.]+" s)) 54 | 55 | (extend-type String 56 | p/LinearPolynomial 57 | (constant [s] 58 | (let [terms (map parse-term (parse-terms s)) 59 | constant-terms (filter (comp nil? first) terms)] 60 | (or (second (first constant-terms)) 0))) 61 | (variables [s] 62 | (into {} (filter (comp identity first) 63 | (map parse-term (parse-terms s))))) 64 | p/Constraint 65 | (relation [s] (symbol (re-find #"<=|>=|=" s))) 66 | (polynomial [s] 67 | (let [[lhs rhs] (s/split s #"<=|>=|=")] 68 | (poly/subtract lhs rhs)))) 69 | -------------------------------------------------------------------------------- /src/prolin/chebyshev.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.chebyshev 2 | (:require [prolin.protocols :as p] 3 | [prolin.polynomial :as poly])) 4 | 5 | (defn dot-product 6 | "Returns the dot product of two vectors" 7 | [v1 v2] 8 | (reduce + (map * v1 v2))) 9 | 10 | (defn- standardize 11 | "Puts a constraint in 'standard' form for easier manipulation by the orthagonal formula: 12 | 13 | a'x + c <= 0" 14 | 15 | [c] 16 | (if (= '>= (p/relation c)) 17 | (p/constraint '<= (poly/multiply (p/polynomial c) -1)) 18 | c)) 19 | 20 | (defn orthagonal 21 | "Return a constraint orthagonal to the provided constraint, with an 22 | additional term d inserted, which relates the distance from the 23 | original point to the new point. 24 | 25 | In other words, transforms: 26 | 27 | a'x + c <= 0 28 | 29 | to: 30 | 31 | a'y + ||a||r + c <= 0 32 | 33 | " 34 | [constraint d] 35 | (let [c (standardize constraint) 36 | p (p/polynomial c)] 37 | (p/constraint (p/relation c) 38 | (p/linear-polynomial 39 | (p/constant p) 40 | (let [vs (p/variables p) 41 | cs (vals vs)] 42 | (assoc vs d (Math/sqrt (dot-product cs cs)))))))) 43 | 44 | (defn chebyshev-center 45 | "Given a set of constraints, returns the set of constraints altered 46 | to refer to the coordinates and radius of the Chebyshev center of 47 | the feasible polyhedron indicated by the input set. Uses the provided 48 | r argument as the variable identifier for the radius." 49 | [constraints r] 50 | (map #(orthagonal % r) constraints)) 51 | 52 | -------------------------------------------------------------------------------- /src/prolin/commons_math.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.commons-math 2 | (:require [prolin.protocols :as p] 3 | [prolin.polynomial :as poly] 4 | [clojure.string :as str] 5 | [clojure.set :as set]) 6 | (:import [org.apache.commons.math3.optim.linear 7 | SimplexSolver 8 | LinearObjectiveFunction 9 | LinearConstraint 10 | LinearConstraintSet 11 | Relationship] 12 | [org.apache.commons.math3.optim.nonlinear.scalar GoalType] 13 | [org.apache.commons.math3.optim OptimizationData] 14 | [org.apache.commons.math3.optim.linear 15 | UnboundedSolutionException 16 | NoFeasibleSolutionException])) 17 | 18 | (def ^:dynamic *debug* false) 19 | 20 | (defmulti debug class) 21 | 22 | (defmethod debug prolin.protocols.LinearPolynomial 23 | [p] 24 | (str (p/variables p) " " (p/constant p))) 25 | 26 | (defmethod debug prolin.protocols.Constraint 27 | [c] 28 | (str (debug (p/polynomial c)) " " (p/relation c) " 0")) 29 | 30 | (defmethod debug :default 31 | [o] 32 | (str o)) 33 | 34 | (defmethod debug org.apache.commons.math3.linear.RealVector 35 | [v] 36 | (str (seq (.toArray v)))) 37 | 38 | (defmethod debug LinearObjectiveFunction 39 | [objective] 40 | (str (.getConstantTerm objective) ", " (debug (.getCoefficients objective)))) 41 | 42 | (defmethod debug LinearConstraint 43 | [c] 44 | (str (debug (.getCoefficients c)) " " (debug (.getRelationship c)) " " (.getValue c))) 45 | 46 | (defmethod debug LinearConstraintSet 47 | [cs] 48 | (str (str/join "\n" (map debug (.getConstraints cs))))) 49 | 50 | (defn- print-cm-debug-info 51 | [[objective constraints]] 52 | (println "======") 53 | (println "raw objective:" (debug objective)) 54 | (doseq [constraint (.getConstraints constraints)] 55 | (println "raw constraint:" (debug constraint)))) 56 | 57 | (defn- print-debug-info 58 | [objective constraints] 59 | (println "======") 60 | (println "objective:" (debug objective)) 61 | (doseq [constraint constraints] 62 | (println "constraint:" (debug constraint)))) 63 | 64 | (defn- build-objective 65 | "Build a LinearObjectiveFunction from a normalized 66 | p/LinearPolynomial, with coefficients ordered by the supplied 67 | ordered key sequence." 68 | [poly key-sequence] 69 | (LinearObjectiveFunction. (double-array (map (p/variables poly) key-sequence)) 70 | (double (p/constant poly)))) 71 | 72 | (def relationships {'= Relationship/EQ 73 | '<= Relationship/LEQ 74 | '>= Relationship/GEQ}) 75 | 76 | (defn- build-constraints 77 | "Build a LinearConstraintSet from the provided p/Constraints, with 78 | coefficients ordered by the supplied ordered key sequence." 79 | [constraints key-sequence] 80 | (LinearConstraintSet. 81 | (map (fn [constraint] 82 | (LinearConstraint. (double-array (map (p/variables (p/polynomial constraint)) 83 | key-sequence)) 84 | (relationships (p/relation constraint)) 85 | (double (* -1 (p/constant (p/polynomial constraint)))))) 86 | constraints))) 87 | 88 | (def defaults 89 | "Default options for constructing a SimplexSolver." 90 | {:epsilon 1.0e-6 91 | :max-ulps 10 92 | :cutoff 1.0e-12}) 93 | 94 | (defn solver 95 | "Return an implementation of Solver using the 2-stage Simplex 96 | algorithm provided by Apache Commons Math. 97 | 98 | Optionally takes an options map containing the following keys: 99 | 100 | :epsilon - Amount of error to accept for algorithm convergence. 101 | :max-ulps - Amount of error to accept in floating point comparisons. 102 | :cutoff - Values smaller than the cutOff are treated as zero." 103 | ([] (solver {})) 104 | ([options] 105 | (reify p/Solver 106 | (optimize [_ objective constraints minimize?] 107 | (try 108 | (let [opts (merge defaults options) 109 | solver (SimplexSolver. (:epsilon opts) (:max-ulps opts) (:cutoff opts)) 110 | zero (poly/zero (reduce set/union 111 | (keys (p/variables objective)) 112 | (map (comp set keys p/variables p/polynomial) 113 | constraints))) 114 | coefficient-ordering (keys (p/variables zero)) 115 | normalized-objective (poly/add zero objective) 116 | normalized-constraints (map (fn [constraint] 117 | (p/constraint (p/relation constraint) 118 | (poly/add zero (p/polynomial constraint)))) 119 | constraints) 120 | optimization-data [(build-objective normalized-objective coefficient-ordering) 121 | (build-constraints normalized-constraints coefficient-ordering) 122 | (if minimize? GoalType/MINIMIZE GoalType/MAXIMIZE)] 123 | _ (when *debug* (print-debug-info normalized-objective normalized-constraints)) 124 | _ (when *debug* (print-cm-debug-info optimization-data)) 125 | solution (.optimize solver (into-array OptimizationData optimization-data))] 126 | (zipmap (keys (p/variables zero)) 127 | (.getPoint solution))) 128 | (catch UnboundedSolutionException e 129 | (throw (ex-info "Unbounded solution" {:reason :unbounded 130 | :objective objective 131 | :constraints constraints 132 | :minimize? minimize?} e))) 133 | (catch NoFeasibleSolutionException e 134 | (throw (ex-info "No solution" {:reason :no-solution 135 | :objective objective 136 | :constraints constraints 137 | :minimize? minimize?} e)))))))) 138 | 139 | 140 | -------------------------------------------------------------------------------- /src/prolin/polynomial.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.polynomial 2 | (:require [prolin.protocols :as p])) 3 | 4 | (defn multiply 5 | "Multiply a LinearPolynomial by a constant" 6 | [polynomial n] 7 | (let [c (* n (p/constant polynomial)) 8 | old-vs (p/variables polynomial) 9 | vs (zipmap (keys old-vs) (map #(* n %) (vals old-vs)))] 10 | (reify p/LinearPolynomial 11 | (variables [_] vs) 12 | (constant [_] c)))) 13 | 14 | (defn add 15 | "Add a LinearPolynomial to another" 16 | [a b] 17 | (let [c (+ (p/constant a) (p/constant b)) 18 | vs (merge-with (fnil + 0 0) (p/variables a) (p/variables b))] 19 | (reify p/LinearPolynomial 20 | (variables [_] vs) 21 | (constant [_] c)))) 22 | 23 | (defn subtract 24 | "Subtract a polynomial from another" 25 | [a b] 26 | (add a (multiply b -1))) 27 | 28 | (defn zero 29 | "Construct a LinearPolynomial that contains the set of provided 30 | variables, each with a coefficient of zero." 31 | [variables] 32 | (let [vs (into {} (map (fn [v] [v 0]) variables))] 33 | (reify p/LinearPolynomial 34 | (variables [_] vs) 35 | (constant [_] 0)))) 36 | 37 | (defn instantiate 38 | "Given a LinearPolynomial and a map of variables to values, return a 39 | single number. All variables in the polynomial must be present in 40 | the value map." 41 | [polynomial values] 42 | (reduce + (p/constant polynomial) 43 | (map (fn [[v c]] 44 | (* c (values v 0))) 45 | (p/variables polynomial)))) 46 | 47 | 48 | -------------------------------------------------------------------------------- /src/prolin/protocols.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.protocols) 2 | 3 | (defprotocol LinearPolynomial 4 | "Representation of a linear polynomial (a polynomial of degree 5 | one). A linear polynomial consists of: 6 | 7 | - Any number of variables, each with a numerical coefficient 8 | - A constant numerical term 9 | 10 | The keys representing variables can be any type that supports good equality semantics." 11 | 12 | (variables [this] "Return a map of variable identifiers to coefficients.") 13 | (constant [this] "Returns the constant term of the polynomial")) 14 | 15 | (extend-protocol LinearPolynomial 16 | clojure.lang.APersistentMap 17 | (variables [this] (:v this)) 18 | (constant [this] (:c this))) 19 | 20 | (defn linear-polynomial 21 | "Construct a linear polynomial, given a constant and variables map" 22 | [constant variables] 23 | {:c constant 24 | :v variables}) 25 | 26 | (defprotocol Constraint 27 | "Representation of a linear constraint as an (in)equality" 28 | (relation [this] "Return one of '>=, '<=, or '=") 29 | (polynomial [this] 30 | "Returns a LinearPolynomial representing the variables, 31 | coefficients and constant term of the (in)equality, when it is put 32 | in one of the forms: 33 | 34 | a[0]x[0] + ... + a[n]x[n] + c = 0 35 | a[0]x[0] + ... + a[n]x[n] + c <= 0 36 | a[0]x[0] + ... + a[n]x[n] + c >= 0 37 | 38 | Any linear (in)equality can be algebraically manipulated to this 39 | form without any loss of generality, and it is this form that is 40 | used to represent all linear constraints internally. See 41 | 'prolin.polynomial/subtract' for a function to help transform 42 | arbitrary (in)equalities to this format.")) 43 | 44 | (extend-protocol Constraint 45 | clojure.lang.APersistentMap 46 | (relation [this] (:r this)) 47 | (polynomial [this] (:p this))) 48 | 49 | (defn constraint 50 | "Construct a constraint, given a relation and a polynomial." 51 | [relation polynomial] 52 | {:r relation :p polynomial}) 53 | 54 | (defprotocol Solver 55 | "An implementation of a linear programming solver" 56 | (optimize [this objective constraints minimize?] 57 | "Maximize or minimize the given objective polynomial, subject to 58 | the provided set of LinearConstraints. Pass true as the third 59 | argument to minimize instead of maximize. Return a variables 60 | mapping representing the assignemtn of each variable present in 61 | the objective and constraints. 62 | 63 | If there is no solution matching the constraints, throws an 64 | ex-info with a :reason key of :no-solution. 65 | 66 | If the solution is unbounded by the provided constraints, throws 67 | an ex-info with a :reason key of :unbounded.")) 68 | 69 | -------------------------------------------------------------------------------- /test/prolin/chebyshev_test.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.chebyshev-test 2 | (:require [clojure.test :refer :all] 3 | [prolin :as prolin] 4 | [prolin.chebyshev :refer :all] 5 | [prolin.commons-math :as cm] 6 | [prolin.protocols :as p]) 7 | (:import [org.apache.commons.math3.util Precision])) 8 | 9 | (defn f= 10 | "Fuzzy floating point equals" 11 | [x y] 12 | (Precision/equals (double x) (double y) 1e-6)) 13 | 14 | (deftest center-of-line 15 | (is (f= 0 (get (prolin/maximize (cm/solver) 16 | "r" (chebyshev-center ["x <= 10" 17 | "x >= -10"] "r")) 18 | "x")))) 19 | 20 | (deftest center-of-square 21 | (is (= {"x" 0.5 "y" 0.5 "r" 0.5} 22 | (prolin/maximize (cm/solver) 23 | "r" (chebyshev-center ["x <= 1" 24 | "x >= 0" 25 | "y <= 1" 26 | "y >= 0"] "r"))))) 27 | 28 | (deftest center-of-triangle 29 | (let [{x "x" y "y"} 30 | (prolin/maximize (cm/solver) 31 | "r" (chebyshev-center ["y >= 0" 32 | "y - 4 <= x" 33 | "y <= 4 - x"] "r"))] 34 | (is (f= x 0)) 35 | (is (f= y 1.656854249423)))) 36 | -------------------------------------------------------------------------------- /test/prolin/commons_math_test.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.commons-math-test 2 | (:require [clojure.test :refer :all] 3 | [prolin :as prolin] 4 | [prolin.commons-math :as cm] 5 | [prolin.protocols :as p])) 6 | 7 | (deftest one-variable 8 | (let [constraints #{"x <= 5", "x >= -2"}] 9 | (is (= {"x" 5.0} (prolin/maximize (cm/solver) "x" constraints))) 10 | (is (= {"x" -2.0} (prolin/minimize (cm/solver) "x" constraints))) 11 | (is (= {"x" -2.0} (prolin/maximize (cm/solver) "-x" constraints))))) 12 | 13 | 14 | (deftest point-on-a-line 15 | (let [constraints #{"2x = y", "y <= 5"}] 16 | (is (= {"y" 5.0 "x" 2.5} (prolin/maximize (cm/solver) "y" constraints))) 17 | (is (= {"x" 2.5 "y" 5.0} (prolin/maximize (cm/solver) "x" constraints))) 18 | (is (thrown? clojure.lang.ExceptionInfo 19 | (prolin/minimize (cm/solver) "x" constraints))))) 20 | 21 | (deftest an-impossible-conundrum 22 | (let [constraints #{"x = -2", "x >= 0"}] 23 | (is (thrown? clojure.lang.ExceptionInfo 24 | (prolin/maximize (cm/solver) "x" constraints))))) 25 | -------------------------------------------------------------------------------- /test/prolin/polynomial_test.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.polynomial-test 2 | (:require [prolin.polynomial :as poly] 3 | [prolin.protocols :as p] 4 | [prolin.testutil :as util] 5 | [clojure.set :as set] 6 | [clojure.data.generators :as gen] 7 | [clojure.test.generative :refer [defspec]])) 8 | 9 | (defspec polynomial-instantiation 10 | (fn [constant variables coefficients values] 11 | (let [sum (reduce + constant (map * coefficients values)) 12 | poly (p/linear-polynomial constant (zipmap variables coefficients)) 13 | values (zipmap variables values) 14 | poly-sum (poly/instantiate poly values)] 15 | {:sum sum :poly-sum poly-sum})) 16 | [^{:tag (util/gen-number)} constant 17 | ^{:tag (util/gen-set gen/anything 10)} variables 18 | ^{:tag (gen/vec util/gen-number 10)} coefficients 19 | ^{:tag (gen/vec util/gen-number 10)} values] 20 | (assert (util/f= (:sum %) (:poly-sum %)))) 21 | 22 | (defspec polynomial-addition 23 | poly/add 24 | [^{:tag (util/gen-linear-polynomial 25 | util/gen-var 26 | util/gen-number)} a 27 | ^{:tag (util/gen-linear-polynomial 28 | util/gen-var 29 | util/gen-number)} b] 30 | (let [vars (set/union (keys (p/variables a)) 31 | (keys (p/variables b))) 32 | values (zipmap vars (repeatedly util/gen-number)) 33 | val (poly/instantiate % values) 34 | check (+ (poly/instantiate a values) (poly/instantiate b values))] 35 | (util/debug-ex "The check should have been equal to the calculated value" 36 | [val val 37 | check check] 38 | (assert (util/f= val check))))) 39 | 40 | (defspec polynomial-multiplication 41 | poly/multiply 42 | [^{:tag (util/gen-linear-polynomial 43 | util/gen-var 44 | util/gen-number)} poly 45 | ^{:tag util/gen-number} n] 46 | (let [values (zipmap (keys (p/variables poly)) 47 | (repeatedly util/gen-number)) 48 | val (poly/instantiate % values) 49 | check (* n (poly/instantiate poly values))] 50 | (util/debug-ex "The check should have been equal to the calculated value" 51 | [val val 52 | check check] 53 | (assert (util/f= val check))))) 54 | 55 | (defspec polynomial-subtraction 56 | poly/subtract 57 | [^{:tag (util/gen-linear-polynomial 58 | util/gen-var 59 | util/gen-number)} a 60 | ^{:tag (util/gen-linear-polynomial 61 | util/gen-var 62 | util/gen-number)} b] 63 | (let [vars (set/union (keys (p/variables a)) 64 | (keys (p/variables b))) 65 | values (zipmap vars (repeatedly util/gen-number)) 66 | val (poly/instantiate % values) 67 | diff (- (poly/instantiate a values) (poly/instantiate b values))] 68 | (util/debug-ex "The differences should have been equal" 69 | [val val 70 | diff diff 71 | a a 72 | b b 73 | values values] 74 | (assert (util/f= val diff))))) 75 | -------------------------------------------------------------------------------- /test/prolin/testutil.clj: -------------------------------------------------------------------------------- 1 | (ns prolin.testutil 2 | (:require [prolin.protocols :as p] 3 | [clojure.data.generators :as gen]) 4 | (:import [org.apache.commons.math3.util Precision])) 5 | 6 | (defmacro debug-ex 7 | "Executes body, re-throwings any exception as an ex-info with the 8 | given message, and the bindings evaluated and added to the ex-data map." 9 | [msg bindings & body] 10 | `(try 11 | (do ~@body) 12 | (catch Throwable t# 13 | (throw (ex-info ~msg 14 | (hash-map ~@(mapcat (fn [[v e]] [(list 'quote v) e]) 15 | (partition 2 bindings))) 16 | t#))))) 17 | 18 | (defn f= 19 | "Fuzzy floating point equals" 20 | [x y] 21 | (Precision/equals (double x) (double y) 1e-6)) 22 | 23 | (defn gen-number 24 | "Generate a random double in a realistic range for coefficients" 25 | [] 26 | (* (gen/double) (gen/uniform -1000 1000))) 27 | 28 | (defn gen-set 29 | "Generates a random set with a fixed size populated with values from 30 | f. The built in gen/set is incapable of doing this." 31 | [f size] 32 | (loop [s #{}] 33 | (let [v (f)] 34 | (if (< (count s) size) 35 | (recur (conj s v)) 36 | s)))) 37 | 38 | (defn gen-linear-polynomial 39 | "Generate a random polynomial with variables provided by varf and values provided by valf" 40 | [varf valf] 41 | (let [c (valf) 42 | varset (gen/set varf 2) 43 | varmap (into {} (map (fn [var] [var (valf)]) varset))] 44 | (p/linear-polynomial c varmap))) 45 | 46 | (defn gen-var 47 | "Generate a random variable name" 48 | [] 49 | (str (gen/uniform 1 5))) 50 | 51 | --------------------------------------------------------------------------------