view org/lpsolve.org @ 4:a227fe337e83

fixed headers
author Robert McIntyre <rlm@mit.edu>
date Thu, 20 Oct 2011 01:12:46 -0700
parents a0384c20e075
children eedd6897197d
line wrap: on
line source
1 #+title: Discovering Effective Pokemon Types Using Linear Optimization
2 #+author: Robert McIntyre & Dylan Holmes
3 #+EMAIL: rlm@mit.edu
4 #+SETUPFILE: ../../aurellem/org/setup.org
5 #+INCLUDE: ../../aurellem/org/level-0.org
9 * Introduction
10 In the [[./types.org][previous post]], we used the best-first search algorithm to
11 locate the most effective Pok\eacute{}mon type
12 combinations. Afterwards, we realized that we could transform this
13 search problem into a /linear optimization problem/. This conversion
14 offered several advantages: first, search algorithms are comparatively
15 slow, whereas linear optimization algorithms are extremely fast;
16 second, it is difficult to determine whether a search problem has any
17 solution, whereas it is straightforward to determine whether a linear
18 optimization problem has any solution; finally, because systems of
19 linear equations are so common, many programming languages have linear
20 equation solvers written for them.
22 In this article, we will
23 - Solve a simple linear optimization problem in C :: We demonstrate
24 how to use the linear programming C library, =lp_solve=, to
25 solve a simple linear
26 optimization problem.
27 - Incorporate a C library into Clojure :: We will show how we gave
28 Clojure access to the linear programming C library, =lp_solve=.
29 - Find effective Pokemon types using linear programming :: Building
30 on our earlier code, (...)
31 - Present our results :: (!)
33 #which can be represented and solved as a system of linear equations.
35 * COMMENT
36 This post continues the [[./types.org][previous one]] about pok\eacute{}mon types.
37 Pok\eacute{}mon is a game in which adorable creatures battle each
38 other using fantastic attacks. It was made into a several gameboy
39 games that all share the same battle system. Every pok\eacute{}mon in the
40 gameboy game has one or two /types/, such as Ground, Fire, Water,
41 etc. Every pok\eacute{}mon attack has exactly one type. Certain defending
42 types are weak or strong to other attacking types. For example, Water
43 attacks are strong against Fire pok\eacute{}mon, while Electric attacks are
44 weak against Ground Pok\eacute{}mon. In the games, attacks can be either
45 twice as effective as normal (Water vs. Fire), neutrally effective
46 (Normal vs. Normal), half as effective (Fire vs. Water), or not
47 effective at all (Electric vs. Ground). Thus the range of defense
48 values for a particular type is the set 0, 1/2, 1, 2. These are
49 referred to in the game as being immune, resistant, neutral, and
50 weak, respectively. I call them the /susceptance/ of one type to another.
52 If a pokemon has two types, then the strengths and weakness of each
53 type are /multiplied/ together. Thus Electric (2x weak to Ground)
54 combined with Flying (immune to Ground (0x)) is immune to
55 Ground. Fire (2x weak to Water) combined with Water (1/2x resistant
56 to Water) is neutral to Water. If both types are resistant to another
57 type, then the combination is doubly-resistant (1/4x) to that type. If
58 both types are weak to a certain type then the combination is
59 double-weak (4x) to that type.
61 ** Immortal Types
63 In the game, pok\eacute{}mon can have either one type, or two types. If this
64 restriction is lifted, is there any combination of types that is
65 resistant to all types? I call such a combination an /Immortal Type/,
66 since if that type's pattern was repeated over and over again towards
67 infinity, the resulting type would be immune to all attack types.
69 * Linear Programming
71 ** Terminology
72 Linear programming is the process of finding an optimal solution to a
73 linear equation of several variables which are constrained by some linear
74 inequalities.
76 In linear programming terminology, the function to be extremized is
77 the /objective function/.
79 ** COMMENT
80 First, we'll give a small example of a linear optimization problem,
81 and show how it can be solved with Clojure and =lp_solve=. Then,
82 we'll show how finding immortal pok\eacute{}mon types can be converted
83 into a linear problem suitable for solving in the same way.
85 ** The Farmer's Problem
87 Let's solve the Farmer's Problem, a typical linear programming problem
88 borrowed from http://lpsolve.sourceforge.net/5.5/formulate.htm.
91 #+BEGIN_QUOTE
92 *The Farmer's Problem:* Suppose a farmer has 75 acres on which to
93 plant two crops: wheat and barley. To produce these crops, it costs
94 the farmer (for seed, fertilizer, etc.) $120 per acre for the wheat
95 and $210 per acre for the barley. The farmer has $15000 available for
96 expenses. But after the harvest, the farmer must store the crops while
97 awaiting favorable market conditions. The farmer has storage space
98 for 4000 bushels. Each acre yields an average of 110 bushels of wheat
99 or 30 bushels of barley. If the net profit per bushel of wheat (after
100 all expenses have been subtracted) is $1.30 and for barley is $2.00,
101 how should the farmer plant the 75 acres to maximize profit?
102 #+END_QUOTE
104 The Farmer's Problem is to maximize profit subject to constraints on
105 available farmland, funds for expenses, and storage space.
107 | | Wheat | Barley | Maximum total |
108 |----------+----------------------+---------------------+--------------|
109 | / | < | > | <> |
110 | Farmland | \(w\) acres | \(b\) acres | 75 acres |
111 | Expense | $120 per acre | $210 per acre | $15000 |
112 | Storage | 110 bushels per acre | 30 bushels per acre | 4000 bushels |
113 |----------+----------------------+---------------------+--------------|
114 | Profit | $1.30 per bushel | $2.00 per bushel | |
116 *** COMMENT
117 can be represented as a linear optimization
118 problem. In this form, it is a problem with two variables\mdash{}the number of
119 acres of wheat, \(w\), and the number of acres of barley, \(b\). The
120 aim is to maximize profit, which
122 subject to three constraints: the farmer can't spend more money
123 than he has, the farmer can't use more acres than he owns, and the harvest has
124 to fit in his storage space.
126 We can express these constraints succinctly using matrix
127 notation. Denoting the number of acres of barley and wheat by \(b\) and \(w\),
128 we want to maximize the expression \(143 w + 60 b\) subject to
130 \(
131 \begin{cases}
132 120 w + 210 b & \leq & 1500\\
133 110 w + 30 b & \leq & 4000\\
134 1 w + 1 w & \leq & 75
135 \end{cases}
136 \)
138 #\(\begin{bmatrix}120&210\\110&30\\1 &
139 #1\end{bmatrix}\;\begin{bmatrix}w\\b\end{bmatrix}
140 #\leq \begin{bmatrix}\$15000\\4000\text{ bushels}\\75\text{ acres}\end{bmatrix}\)
143 ** Solution using LP Solve
144 #(LP solve is available at http://www.example.com.)
145 In a new file, =farmer.lp=, we list the variables and constraints
146 of our problem using LP Solve syntax.
148 #+begin_src lpsolve :tangle ../lp/farmer.lp
149 /* Maximize Total Profit */
150 max: +143 wheat +60 barley;
153 /* -------- Constraints --------*/
155 /* the farmer can't spend more money than he has */
156 +120 wheat +210 barley <= 15000;
158 /* the harvest has to fit in his storage space */
159 +110 wheat +30 barley <= 4000;
161 /* he can't use more acres than he owns */
162 +wheat +barley <= 75;
163 #+end_src
166 #This is a set of linear equations ideal for solving using a program like
167 #=lp_solve=. In Linear Algebra terms we are maximizing the linear function
169 #\(\text{profit} = 143\text{ wheat} + 60\text{ barley}\), subject to the constraints
171 #Ax <= b,
173 #where A is [120 210 110 30 1 1], x is [wheat barley] and b is [15000
174 #4000 75].
176 Running the =lp_solve= program on =farmer.lp= yields the following output.
178 #+begin_src sh :exports both :results scalar
179 ~/roBin/lpsolve/lp_solve ~/aurellem/src/pokemon/farmer.lp
180 #+end_src
182 #+results:
183 :
184 : Value of objective function: 6315.62500000
185 :
186 : Actual values of the variables:
187 : wheat 21.875
188 : barley 53.125
190 This shows that the farmer can maximize his profit by planting 21.875
191 of the available acres with wheat and the remaining 53.125 acres with
192 barley; by doing so, he will make $6315.62(5) in profit.
195 #The farmer can make a profit of $6315.62 by planting 21.875 acres of
196 #his land with wheat and the other 53.125 acres of his land with barley.
198 * Incorporating =lp_solve= into Clojure
200 There is a Java API available which enables Java programs to use Lp
201 Solve. Although Clojure can use this Java API directly, the
202 interaction between Java, C, and Clojure is clumsy:
205 The Java API for LP Solve makes it possible to use Lp Solve algorithms
206 within Java. Although Clojure can use this Java API directly,
209 ** The Farmer's Problem in Clojure
210 We are going to solve the same problem involving wheat and barley,
211 that we did above, but this time using clojure and the lpsolve API.
213 #Because we ultimately intend to use Lp Solve to find optimal Pokemon type combinations.
214 # we want to solve linear optimization problems within Clojure, the language
216 ** Setup
217 =lp_solve= is a crufty =C= program which already comes with a JNI
218 interface written by Juergen Ebert. It's API is not
219 particularly friendly from a functional/immutable perspective, but
220 with a little work, we can build something that works great with
221 clojure.
223 #+srcname: intro
224 #+begin_src clojure :results silent
225 (ns pokemon.lpsolve
226 (:use rlm.ns-rlm))
227 (rlm.ns-rlm/ns-clone rlm.light-base)
228 (use 'clojure.set)
229 (import 'lpsolve.LpSolve)
230 (use '[clojure [pprint :only [pprint]]])
231 #+end_src
233 The LpSolve Java interface is available from the same site as
234 =lp_solve= itself, http://lpsolve.sourceforge.net/
235 Using it is the same as many other =C=
236 programs. There are excellent instructions to get set
237 up. The short version is that you must call Java with
238 =-Djava.library.path=/path/to/lpsolve/libraries= and also add the
239 libraries to your export =LD_LIBRARY_PATH= if you are using Linux. For
240 example, in my =.bashrc= file, I have the line
241 =LD_LIBRARY_PATH=$HOME/roBin/lpsolve:$LD_LIBRARY_PATH=.
242 If everything is set-up correctly,
244 #+srcname: body
245 #+begin_src clojure :results verbatim :exports both
246 (import 'lpsolve.LpSolve)
247 #+end_src
249 #+results: body
250 : lpsolve.LpSolve
252 should run with no problems.
254 ** Making a DSL to talk with LpSolve
255 *** Problems
256 Since we are using a =C= wrapper, we have to deal with manual memory
257 management for the =C= structures which are wrapped by the =LpSolve=
258 object. Memory leaks in =LpSolve= instances can crash the JVM, so it's
259 very important to get it right. Also, the Java wrapper follows the
260 =C= tradition closely and defines many =static final int= constants
261 for the different states of the =LpSolve= instance instead of using Java
262 enums. The calling convention for adding rows and columns to
263 the constraint matrix is rather complicated and must be done column by
264 column or row by row, which can be error prone. Finally, I'd like to
265 gather all the important output information from the =LpSolve= instance
266 into a final, immutable structure.
268 In summary, the issues I'd like to address are:
270 - reliable memory management
271 - functional interface to =LpSolve=
272 - intelligible, immutable output
274 To deal with these issues I'll create four functions for interfacing
275 with =LpSolve=
277 #+srcname: declares
278 #+begin_src clojure :results silent
279 (in-ns 'pokemon.lpsolve)
281 ;; deal with automatic memory management for LpSolve instance.
282 (declare linear-program)
284 ;; functional interface to LpSolve
285 (declare lp-solve)
287 ;; immutable output from lp-solve
288 (declare solve get-results)
289 #+end_src
291 *** Memory Management
293 Every instance of =LpSolve= must be manually garbage collected via a
294 call to =deleteLP=. I use a non-hygienic macro similar to =with-open=
295 to ensure that =deleteLP= is always called.
297 #+srcname: memory-management
298 #+begin_src clojure :results silent
299 (in-ns 'pokemon.lpsolve)
300 (defmacro linear-program
301 "solve a linear programming problem using LpSolve syntax.
302 within the macro, the variable =lps= is bound to the LpSolve instance."
303 [& statements]
304 (list 'let '[lps (LpSolve/makeLp 0 0)]
305 (concat '(try)
306 statements
307 ;; always free the =C= data structures.
308 '((finally (.deleteLp lps))))))
309 #+end_src
312 The macro captures the variable =lps= within its body, providing for a
313 convenient way to access the object using any of the methods of the
314 =LpSolve= API without having to worry about when to call
315 =deleteLP=.
317 *** Sensible Results
318 The =linear-program= macro deletes the actual =lps= object once it is
319 done working, so it's important to collect the important results and
320 add return them in an immutable structure at the end.
322 #+srcname: get-results
323 #+begin_src clojure :results silent
324 (in-ns 'pokemon.lpsolve)
326 (defrecord LpSolution
327 [objective-value
328 optimal-values
329 variable-names
330 solution
331 status
332 model])
334 (defn model
335 "Returns a textual representation of the problem suitable for
336 direct input to the =lp_solve= program (lps format)"
337 [#^LpSolve lps]
338 (let [target (java.io.File/createTempFile "lps" ".lp")]
339 (.writeLp lps (.getPath target))
340 (slurp target)))
342 (defn results
343 "given an LpSolve object, solves the object and returns a map of the
344 essential values which compose the solution."
345 [#^LpSolve lps]
346 (locking lps
347 (let [status (solve lps)
348 number-of-variables (.getNcolumns lps)
349 optimal-values (double-array number-of-variables)
350 optimal-values (do (.getVariables lps optimal-values)
351 (seq optimal-values))
352 variable-names
353 (doall ;; the doall is necessary since the lps object might
354 ;; soon be deleted
355 (map
356 #(.getColName lps (inc %))
357 (range number-of-variables)))
358 model (model lps)]
359 (LpSolution.
360 (.getObjective lps)
361 optimal-values
362 variable-names
363 (zipmap variable-names optimal-values)
364 status
365 model))))
367 #+end_src
369 Here I've created an object called =LpSolution= which stores the
370 important results from a session with =lp_solve=. Of note is the
371 =model= function which returns the problem in a form that can be
372 solved by other linear programming packages.
374 *** Solution Status of an LpSolve Object
376 #+srcname: solve
377 #+begin_src clojure :results silent
378 (in-ns 'pokemon.lpsolve)
380 (defn static-integer?
381 "does the field represent a static integer constant?"
382 [#^java.lang.reflect.Field field]
383 (and (java.lang.reflect.Modifier/isStatic (.getModifiers field))
384 (integer? (.get field nil))))
386 (defn integer-constants [class]
387 (filter static-integer? (.getFields class)))
389 (defn-memo constant-map
390 "Takes a class and creates a map of the static constant integer
391 fields with their names. This helps with C wrappers where they have
392 just defined a bunch of integer constants instead of enums"
393 [class]
394 (let [integer-fields (integer-constants class)]
395 (into (sorted-map)
396 (zipmap (map #(.get % nil) integer-fields)
397 (map #(.getName %) integer-fields)))))
399 (defn solve
400 "Solve an instance of LpSolve and return a string representing the
401 status of the computation. Will only solve a particular LpSolve
402 instance once."
403 [#^LpSolve lps]
404 ((constant-map LpSolve)
405 (.solve lps)))
407 #+end_src
409 The =.solve= method of an =LpSolve= object only returns an integer code
410 to specify the status of the computation. The =solve= method here
411 uses reflection to look up the actual name of the status code and
412 returns a more helpful status message that is also resistant to
413 changes in the meanings of the code numbers.
415 *** The Farmer Example in Clojure, Pass 1
417 Now we can implement a nicer version of the examples from the
418 [[http://lpsolve.sourceforge.net/][=lp\_solve= website]]. The following is a more or less
419 line-by-line translation of the Java code from that example.
421 #+srcname: farmer-example
422 #+begin_src clojure :results silent
423 (in-ns 'pokemon.lpsolve)
424 (defn farmer-example []
425 (linear-program
426 (results
427 (doto lps
428 ;; name the columns
429 (.setColName 1 "wheat")
430 (.setColName 2 "barley")
431 (.setAddRowmode true)
432 ;; row 1 : 120x + 210y <= 15000
433 (.addConstraintex 2
434 (double-array [120 210])
435 (int-array [1 2])
436 LpSolve/LE
437 15e3)
438 ;; row 2 : 110x + 30y <= 4000
439 (.addConstraintex 2
440 (double-array [110 30])
441 (int-array [1 2])
442 LpSolve/LE
443 4e3)
444 ;; ;; row 3 : x + y <= 75
445 (.addConstraintex 2
446 (double-array [1 1])
447 (int-array [1 2])
448 LpSolve/LE
449 75)
450 (.setAddRowmode false)
452 ;; add constraints
453 (.setObjFnex 2
454 (double-array [143 60])
455 (int-array [1 2]))
457 ;; set this as a maximization problem
458 (.setMaxim)))))
460 #+end_src
462 #+begin_src clojure :results output :exports both
463 (clojure.pprint/pprint
464 (:solution (pokemon.lpsolve/farmer-example)))
465 #+end_src
467 #+results:
468 : {"barley" 53.12499999999999, "wheat" 21.875}
470 And it works as expected!
472 *** The Farmer Example in Clojure, Pass 2
473 We don't have to worry about memory management anymore, and the farmer
474 example is about half as long as the example from the =LpSolve=
475 website, but we can still do better. Solving linear problems is all
476 about the constraint matrix $A$ , the objective function $c$, and the
477 right-hand-side $b$, plus whatever other options one cares to set for
478 the particular instance of =lp_solve=. Why not make a version of
479 =linear-program= that takes care of initialization?
483 #+srcname: lp-solve
484 #+begin_src clojure :results silent
485 (in-ns 'pokemon.lpsolve)
486 (defn initialize-lpsolve-row-oriented
487 "fill in an lpsolve instance using a constraint matrix =A=, the
488 objective function =c=, and the right-hand-side =b="
489 [#^LpSolve lps A b c]
490 ;; set the name of the last column to _something_
491 ;; this appears to be necessary to ensure proper initialization.
492 (.setColName lps (count c) (str "C" (count c)))
494 ;; This is the recommended way to "fill-in" an lps instance from the
495 ;; documentation. First, set row mode, then set the objective
496 ;; function, then set each row of the problem, and then turn off row
497 ;; mode.
498 (.setAddRowmode lps true)
499 (.setObjFnex lps (count c)
500 (double-array c)
501 (int-array (range 1 (inc (count c)))))
502 (dorun
503 (for [n (range (count A))]
504 (let [row (nth A n)
505 row-length (int (count row))]
506 (.addConstraintex lps
507 row-length
508 (double-array row)
509 (int-array (range 1 (inc row-length)))
510 LpSolve/LE
511 (double (nth b n))))))
512 (.setAddRowmode lps false)
513 lps)
516 (defmacro lp-solve
517 "by default:,
518 minimize (* c x), subject to (<= (* A x) b),
519 using continuous variables. You may set any number of
520 other options as in the LpSolve API."
521 [A b c & lp-solve-forms]
522 ;; assume that A is a vector of vectors
523 (concat
524 (list 'linear-program
525 (list 'initialize-lpsolve-row-oriented 'lps A b c))
526 `~lp-solve-forms))
527 #+end_src
529 Now, we can use a much more functional approach to solving the
530 farmer's problem:
532 #+srcname: better-farmer
533 #+begin_src clojure :results silent
534 (in-ns 'pokemon.lpsolve)
535 (defn better-farmer-example []
536 (lp-solve [[120 210]
537 [110 30]
538 [1 1]]
539 [15000
540 4000
541 75]
542 [143 60]
543 (.setColName lps 1 "wheat")
544 (.setColName lps 2 "barley")
545 (.setMaxim lps)
546 (results lps)))
547 #+end_src
549 #+begin_src clojure :exports both :results verbatim
550 (vec (:solution (pokemon.lpsolve/better-farmer-example)))
551 #+end_src
553 #+results:
554 : [["barley" 53.12499999999999] ["wheat" 21.875]]
556 Notice that both the inputs to =better-farmer-example= and the results
557 are immutable.
559 * Using LpSolve to find Immortal Types
560 ** Converting the Pokemon problem into a linear form
561 How can the original question about pok\eacute{}mon types be converted
562 into a linear problem?
564 Pokemon types can be considered to be vectors of numbers representing
565 their susceptances to various attacking types, so Water might look
566 something like this.
568 #+begin_src clojure :results scalar :exports both
569 (:water (pokemon.types/defense-strengths))
570 #+end_src
572 #+results:
573 : [1 0.5 0.5 2 2 0.5 1 1 1 1 1 1 1 1 1 1 0.5]
575 Where the numbers represent the susceptibility of Water to the
576 attacking types in the following order:
578 #+begin_src clojure :results output :exports both
579 (clojure.pprint/pprint
580 (pokemon.types/type-names))
581 #+end_src
583 #+results:
584 #+begin_example
585 [:normal
586 :fire
587 :water
588 :electric
589 :grass
590 :ice
591 :fighting
592 :poison
593 :ground
594 :flying
595 :psychic
596 :bug
597 :rock
598 :ghost
599 :dragon
600 :dark
601 :steel]
602 #+end_example
605 So, for example, Water is /resistant/ (x0.5) against Fire, which is
606 the second element in the list.
608 To combine types, these sorts of vectors are multiplied together
609 pair-wise to yield the resulting combination.
611 Unfortunately, we need some way to add two type vectors together
612 instead of multiplying them if we want to solve the problem with
613 =lp_solve=. Taking the log of the vector does just the trick.
615 If we make a matrix with each column being the log (base 2) of the
616 susceptance of each type, then finding an immortal type corresponds to
617 setting each constraint (the $b$ vector) to -1 (since log_2(1/2) = -1)
618 and setting the constraint vector $c$ to all ones, which means that we
619 want to find the immortal type which uses the least amount of types.
621 #+srcname: pokemon-lp
622 #+begin_src clojure :results silent
623 (in-ns 'pokemon.lpsolve)
625 (require 'pokemon.types)
626 (require 'incanter.core)
628 (defn log-clamp-matrix [matrix]
629 ;; we have to clamp the Infinities to a more reasonable negative
630 ;; value because lp_solve does not play well with infinities in its
631 ;; constraint matrix.
632 (map (fn [row] (map #(if (= Double/NEGATIVE_INFINITY %) -1e3 %) row))
633 (incanter.core/log2
634 (incanter.core/trans
635 matrix))))
637 ;; constraint matrices
638 (defn log-defense-matrix []
639 (log-clamp-matrix
640 (doall (map (pokemon.types/defense-strengths)
641 (pokemon.types/type-names)))))
643 (defn log-attack-matrix []
644 (incanter.core/trans (log-defense-matrix)))
646 ;; target vectors
647 (defn all-resistant []
648 (doall (map (constantly -1) (pokemon.types/type-names))))
650 (defn all-weak []
651 (doall (map (constantly 1) (pokemon.types/type-names))))
653 (defn all-neutral []
654 (doall (map (constantly 0) (pokemon.types/type-names))))
657 ;; objective functions
658 (defn number-of-types []
659 (doall (map (constantly 1) (pokemon.types/type-names))))
661 (defn set-constraints
662 "sets all the constraints for an lpsolve instance to the given
663 constraint. =constraint= here is one of the LpSolve constants such
664 as LpSolve/EQ."
665 [#^LpSolve lps constraint]
666 (dorun (map (fn [index] (.setConstrType lps index constraint))
667 ;; ONE based indexing!!!
668 (range 1 (inc (.getNrows lps))))))
671 (defn set-discrete
672 "sets every variable in an lps problem to be a discrete rather than
673 continuous variable"
674 [#^LpSolve lps]
675 (dorun (map (fn [index] (.setInt lps index true))
676 ;; ONE based indexing!!!
677 (range 1 (inc (.getNcolumns lps))))))
679 (defn set-variable-names
680 "sets the variable names of the problem given a vector of names"
681 [#^LpSolve lps names]
682 (dorun
683 (map (fn [[index name]]
684 (.setColName lps (inc index) (str name)))
685 ;; ONE based indexing!!!
686 (indexed names))))
688 (defn poke-solve
689 ([poke-matrix target objective-function constraint min-num-types]
690 ;; must have at least one type
691 (let [poke-matrix
692 (concat poke-matrix
693 [(map (constantly 1)
694 (range (count (first poke-matrix))))])
695 target (concat target [min-num-types])]
696 (lp-solve poke-matrix target objective-function
697 (set-constraints lps constraint)
698 ;; must have more than min-num-types
699 (.setConstrType lps (count target) LpSolve/GE)
700 (set-discrete lps)
701 (set-variable-names lps (pokemon.types/type-names))
702 (results lps))))
703 ([poke-matrix target objective-function constraint]
704 ;; at least one type
705 (poke-solve poke-matrix target objective-function constraint 1)))
707 (defn solution
708 "If the results of an lpsolve operation are feasible, returns the
709 results. Otherwise, returns the error."
710 [results]
711 (if (not (= (:status results) "OPTIMAL"))
712 (:status results)
713 (:solution results)))
715 #+end_src
717 With this, we are finally able to get some results.
719 ** Results
720 #+srcname: results
721 #+begin_src clojure :results silent
722 (in-ns 'pokemon.lpsolve)
724 (defn best-defense-type
725 "finds a type combination which is resistant to all attacks."
726 []
727 (poke-solve
728 (log-defense-matrix) (all-resistant) (number-of-types) LpSolve/LE))
730 (defn worst-attack-type
731 "finds the attack type which is not-very-effective against all pure
732 defending types (each single defending type is resistant to this
733 attack combination"
734 []
735 (poke-solve
736 (log-attack-matrix) (all-resistant) (number-of-types) LpSolve/LE))
738 (defn worst-defense-type
739 "finds a defending type that is weak to all single attacking types."
740 []
741 (poke-solve
742 (log-defense-matrix) (all-weak) (number-of-types) LpSolve/GE))
744 (defn best-attack-type
745 "finds an attack type which is super effective against all single
746 defending types"
747 []
748 (poke-solve
749 (log-attack-matrix) (all-weak) (number-of-types) LpSolve/GE))
751 (defn solid-defense-type
752 "finds a defense type which is either neutral or resistant to all
753 single attacking types"
754 []
755 (poke-solve
756 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/LE))
758 (defn solid-attack-type
759 "finds an attack type which is either neutral or super-effective to
760 all single attacking types."
761 []
762 (poke-solve
763 (log-attack-matrix) (all-neutral) (number-of-types) LpSolve/GE))
765 (defn weak-defense-type
766 "finds a defense type which is either neutral or weak to all single
767 attacking types"
768 []
769 (poke-solve
770 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/GE))
772 (defn neutral-defense-type
773 "finds a defense type which is perfectly neutral to all attacking
774 types."
775 []
776 (poke-solve
777 (log-defense-matrix) (all-neutral) (number-of-types) LpSolve/EQ))
779 #+end_src
781 *** Strongest Attack/Defense Combinations
783 #+begin_src clojure :results output :exports both
784 (clojure.pprint/pprint
785 (pokemon.lpsolve/solution (pokemon.lpsolve/best-defense-type)))
786 #+end_src
788 #+results:
789 #+begin_example
790 {":normal" 0.0,
791 ":ground" 1.0,
792 ":poison" 2.0,
793 ":flying" 1.0,
794 ":fighting" 0.0,
795 ":dragon" 0.0,
796 ":fire" 0.0,
797 ":dark" 1.0,
798 ":ice" 0.0,
799 ":steel" 1.0,
800 ":ghost" 0.0,
801 ":electric" 0.0,
802 ":bug" 0.0,
803 ":psychic" 0.0,
804 ":grass" 0.0,
805 ":water" 2.0,
806 ":rock" 0.0}
807 #+end_example
809 # #+results-old:
810 # : [[":normal" 0.0] [":ground" 1.0] [":poison" 0.0] [":flying" 1.0] [":fighting" 0.0] [":dragon" 1.0] [":fire" 0.0] [":dark" 0.0] [":ice" 0.0] [":steel" 2.0] [":ghost" 1.0] [":electric" 0.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 2.0] [":rock" 0.0]]
813 This is the immortal type combination we've been looking for. By
814 combining Steel, Water, Poison, and three types which each have complete
815 immunities to various other types, we've created a type that is resistant to
816 all attacking types.
818 #+begin_src clojure :results output :exports both
819 (clojure.pprint/pprint
820 (pokemon.types/susceptibility
821 [:poison :poison :water :water :steel :ground :flying :dark]))
822 #+end_src
824 #+results:
825 #+begin_example
826 {:water 1/2,
827 :psychic 0,
828 :dragon 1/2,
829 :fire 1/2,
830 :ice 1/2,
831 :grass 1/2,
832 :ghost 1/4,
833 :poison 0,
834 :flying 1/2,
835 :normal 1/2,
836 :rock 1/2,
837 :electric 0,
838 :ground 0,
839 :fighting 1/2,
840 :dark 1/4,
841 :steel 1/8,
842 :bug 1/8}
843 #+end_example
845 # #+results-old:
846 # : {:water 1/4, :psychic 1/4, :dragon 1/2, :fire 1/2, :ice 1/2, :grass 1/2, :ghost 1/2, :poison 0, :flying 1/4, :normal 0, :rock 1/4, :electric 0, :ground 0, :fighting 0, :dark 1/2, :steel 1/16, :bug 1/16}
849 Cool!
851 #+begin_src clojure :results output :exports both
852 (clojure.pprint/pprint
853 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type)))
854 #+end_src
856 #+results:
857 #+begin_example
858 {":normal" 0.0,
859 ":ground" 0.0,
860 ":poison" 0.0,
861 ":flying" 0.0,
862 ":fighting" 0.0,
863 ":dragon" 0.0,
864 ":fire" 0.0,
865 ":dark" 1.0,
866 ":ice" 0.0,
867 ":steel" 0.0,
868 ":ghost" 1.0,
869 ":electric" 0.0,
870 ":bug" 0.0,
871 ":psychic" 0.0,
872 ":grass" 0.0,
873 ":water" 0.0,
874 ":rock" 0.0}
875 #+end_example
877 Dark and Ghost are the best dual-type combo, and are resistant or
878 neutral to all types.
880 #+begin_src clojure :results output :exports both
881 (clojure.pprint/pprint
882 (pokemon.types/old-school
883 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-defense-type))))
884 #+end_src
886 #+results:
887 #+begin_example
888 {":normal" 0.0,
889 ":ground" 0.0,
890 ":poison" 0.0,
891 ":flying" 0.0,
892 ":fighting" 0.0,
893 ":dragon" 0.0,
894 ":fire" 0.0,
895 ":ice" 0.0,
896 ":ghost" 1.0,
897 ":electric" 0.0,
898 ":bug" 0.0,
899 ":psychic" 1.0,
900 ":grass" 0.0,
901 ":water" 0.0,
902 ":rock" 0.0}
903 #+end_example
905 Ghost and Psychic are a powerful dual type combo in the original games,
906 due to a glitch which made Psychic immune to Ghost type attacks, even
907 though the game claims that Ghost is strong to Psychic.
909 #+begin_src clojure :results verbatim :exports both
910 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type))
911 #+end_src
913 #+results:
914 : INFEASIBLE
916 #+begin_src clojure :results verbatim :exports both
917 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))
918 #+end_src
920 #+results:
921 : INFEASIBLE
924 #+begin_src clojure :results verbatim :exports both
925 (pokemon.types/old-school
926 (pokemon.lpsolve/solution (pokemon.lpsolve/best-attack-type)))
927 #+end_src
929 #+results:
930 : INFEASIBLE
933 #+begin_src clojure :results output :exports both
934 (clojure.pprint/pprint
935 (pokemon.types/old-school
936 (pokemon.lpsolve/solution (pokemon.lpsolve/solid-attack-type))))
937 #+end_src
939 #+results:
940 #+begin_example
941 {":normal" 0.0,
942 ":ground" 0.0,
943 ":poison" 0.0,
944 ":flying" 0.0,
945 ":fighting" 0.0,
946 ":dragon" 1.0,
947 ":fire" 0.0,
948 ":ice" 0.0,
949 ":ghost" 0.0,
950 ":electric" 0.0,
951 ":bug" 0.0,
952 ":psychic" 0.0,
953 ":grass" 0.0,
954 ":water" 0.0,
955 ":rock" 0.0}
956 #+end_example
958 The best attacking type combination is strangely Dragon from the
959 original games. It is neutral against all the original types except
960 for Dragon, which it is strong against. There is no way to make an
961 attacking type that is strong against every type, or even one that is
962 strong or neutral against every type, in the new games.
965 *** Weakest Attack/Defense Combinations
967 #+begin_src clojure :results output :exports both
968 (clojure.pprint/pprint
969 (pokemon.types/old-school
970 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type))))
971 #+end_src
973 #+results:
974 #+begin_example
975 {":normal" 5.0,
976 ":ground" 0.0,
977 ":poison" 0.0,
978 ":flying" 0.0,
979 ":fighting" 0.0,
980 ":dragon" 0.0,
981 ":fire" 1.0,
982 ":ice" 2.0,
983 ":ghost" 1.0,
984 ":electric" 1.0,
985 ":bug" 1.0,
986 ":psychic" 0.0,
987 ":grass" 3.0,
988 ":water" 2.0,
989 ":rock" 0.0}
990 #+end_example
992 # #+results-old:
993 # : [[":normal" 5.0] [":ground" 1.0] [":poison" 0.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":ice" 4.0] [":ghost" 1.0] [":electric" 4.0] [":bug" 0.0] [":psychic" 0.0] [":grass" 0.0] [":water" 1.0] [":rock" 1.0]]
995 #+begin_src clojure :results output :exports both
996 (clojure.pprint/pprint
997 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-attack-type)))
998 #+end_src
1000 #+results:
1001 #+begin_example
1002 {":normal" 4.0,
1003 ":ground" 1.0,
1004 ":poison" 1.0,
1005 ":flying" 0.0,
1006 ":fighting" 1.0,
1007 ":dragon" 0.0,
1008 ":fire" 0.0,
1009 ":dark" 0.0,
1010 ":ice" 4.0,
1011 ":steel" 0.0,
1012 ":ghost" 1.0,
1013 ":electric" 3.0,
1014 ":bug" 0.0,
1015 ":psychic" 1.0,
1016 ":grass" 1.0,
1017 ":water" 1.0,
1018 ":rock" 2.0}
1019 #+end_example
1021 # #+results-old:
1022 # : [[":normal" 4.0] [":ground" 1.0] [":poison" 1.0] [":flying" 0.0] [":fighting" 2.0] [":dragon" 0.0] [":fire" 0.0] [":dark" 0.0] [":ice" 5.0] [":steel" 0.0] [":ghost" 1.0] [":electric" 5.0] [":bug" 0.0] [":psychic" 1.0] [":grass" 0.0] [":water" 1.0] [":rock" 2.0]]
1025 This is an extremely interesting type combination, in that it uses
1026 quite a few types.
1028 #+begin_src clojure :results verbatim :exports both
1029 (reduce + (vals (:solution (pokemon.lpsolve/worst-attack-type))))
1030 #+end_src
1032 #+results:
1033 : 20.0
1035 20 types is the /minimum/ number of types before the attacking
1036 combination is not-very-effective or worse against all defending
1037 types. This would probably have been impossible to discover using
1038 best-first search, since it involves such an intricate type
1039 combination.
1041 It's so interesting that it takes 20 types to make an attack type that
1042 is weak to all types that the combination merits further investigation.
1044 Unfortunately, all of the tools that we've written so far are focused
1045 on defense type combinations. However, it is possible to make every
1046 tool attack-oriented via a simple macro.
1048 #+srcname: attack-oriented
1049 #+begin_src clojure :results silent
1050 (in-ns 'pokemon.lpsolve)
1052 (defmacro attack-mode [& forms]
1053 `(let [attack-strengths# pokemon.types/attack-strengths
1054 defense-strengths# pokemon.types/defense-strengths]
1055 (binding [pokemon.types/attack-strengths
1056 defense-strengths#
1057 pokemon.types/defense-strengths
1058 attack-strengths#]
1059 ~@forms)))
1060 #+end_src
1062 Now all the tools from =pokemon.types= will work for attack
1063 combinations.
1065 #+begin_src clojure :results output :exports both
1066 (clojure.pprint/pprint
1067 (pokemon.types/susceptibility [:water]))
1068 #+end_src
1070 #+results:
1071 #+begin_example
1072 {:water 1/2,
1073 :psychic 1,
1074 :dragon 1,
1075 :fire 1/2,
1076 :ice 1/2,
1077 :grass 2,
1078 :ghost 1,
1079 :poison 1,
1080 :flying 1,
1081 :normal 1,
1082 :rock 1,
1083 :electric 2,
1084 :ground 1,
1085 :fighting 1,
1086 :dark 1,
1087 :steel 1/2,
1088 :bug 1}
1089 #+end_example
1092 #+begin_src clojure :results output :exports both
1093 (clojure.pprint/pprint
1094 (pokemon.lpsolve/attack-mode
1095 (pokemon.types/susceptibility [:water])))
1096 #+end_src
1098 #+results:
1099 #+begin_example
1100 {:water 1/2,
1101 :psychic 1,
1102 :dragon 1/2,
1103 :fire 2,
1104 :ice 1,
1105 :grass 1/2,
1106 :ghost 1,
1107 :poison 1,
1108 :flying 1,
1109 :normal 1,
1110 :rock 2,
1111 :electric 1,
1112 :ground 2,
1113 :fighting 1,
1114 :dark 1,
1115 :steel 1,
1116 :bug 1}
1117 #+end_example
1119 Now =pokemon.types/susceptibility= reports the /attack-type/
1120 combination's effectiveness against other types.
1122 The 20 type combo achieves its goal in a very clever way.
1124 First, it weakens its effectiveness to other types at the expense of
1125 making it very strong against flying.
1127 #+begin_src clojure :results output :exports both
1128 (clojure.pprint/pprint
1129 (pokemon.lpsolve/attack-mode
1130 (pokemon.types/susceptibility
1131 [:normal :normal :normal :normal
1132 :ice :ice :ice :ice
1133 :electric :electric :electric
1134 :rock :rock])))
1135 #+end_src
1137 #+results:
1138 #+begin_example
1139 {:water 1/2,
1140 :psychic 1,
1141 :dragon 2,
1142 :fire 1/4,
1143 :ice 1/4,
1144 :grass 2,
1145 :ghost 0,
1146 :poison 1,
1147 :flying 512,
1148 :normal 1,
1149 :rock 1/16,
1150 :electric 1/8,
1151 :ground 0,
1152 :fighting 1/4,
1153 :dark 1,
1154 :steel 1/1024,
1155 :bug 4}
1156 #+end_example
1158 Then, it removes it's strengths against Flying, Normal, and Fighting
1159 by adding Ghost and Ground.
1161 #+begin_src clojure :results output :exports both
1162 (clojure.pprint/pprint
1163 (pokemon.lpsolve/attack-mode
1164 (pokemon.types/susceptibility
1165 [:normal :normal :normal :normal
1166 :ice :ice :ice :ice
1167 :electric :electric :electric
1168 :rock :rock
1169 ;; Spot resistances
1170 :ghost :ground])))
1171 #+end_src
1173 #+results:
1174 #+begin_example
1175 {:water 1/2,
1176 :psychic 2,
1177 :dragon 2,
1178 :fire 1/2,
1179 :ice 1/4,
1180 :grass 1,
1181 :ghost 0,
1182 :poison 2,
1183 :flying 0,
1184 :normal 0,
1185 :rock 1/8,
1186 :electric 1/4,
1187 :ground 0,
1188 :fighting 1/4,
1189 :dark 1/2,
1190 :steel 1/1024,
1191 :bug 2}
1192 #+end_example
1194 Adding the pair Psychic and Fighting takes care of its strength
1195 against Psychic and makes it ineffective against Dark, which is immune
1196 to Psychic.
1198 Adding the pair Grass and Poison makes takes care of its strength
1199 against poison and makes it ineffective against Steel, which is immune
1200 to poison.
1202 #+begin_src clojure :results output :exports both
1203 (clojure.pprint/pprint
1204 (pokemon.lpsolve/attack-mode
1205 (pokemon.types/susceptibility
1206 [;; setup
1207 :normal :normal :normal :normal
1208 :ice :ice :ice :ice
1209 :electric :electric :electric
1210 :rock :rock
1211 ;; Spot resistances
1212 :ghost :ground
1213 ;; Pair resistances
1214 :psychic :fighting
1215 :grass :poison])))
1216 #+end_src
1218 #+results:
1219 #+begin_example
1220 {:water 1,
1221 :psychic 1/2,
1222 :dragon 1,
1223 :fire 1/4,
1224 :ice 1/2,
1225 :grass 1,
1226 :ghost 0,
1227 :poison 1/2,
1228 :flying 0,
1229 :normal 0,
1230 :rock 1/4,
1231 :electric 1/4,
1232 :ground 0,
1233 :fighting 1/2,
1234 :dark 0,
1235 :steel 0,
1236 :bug 1/2}
1237 #+end_example
1239 Can you see the final step?
1241 It's adding the Water type, which is weak against Water and Dragon and
1242 strong against Rock and Fire.
1244 #+begin_src clojure :results output :exports both
1245 (clojure.pprint/pprint
1246 (pokemon.lpsolve/attack-mode
1247 (pokemon.types/susceptibility
1248 [;; setup
1249 :normal :normal :normal :normal
1250 :ice :ice :ice :ice
1251 :electric :electric :electric
1252 :rock :rock
1253 ;; Spot resistances
1254 :ghost :ground
1255 ;; Pair resistances
1256 :psychic :fighting
1257 :grass :poison
1258 ;; completion
1259 :water])))
1260 #+end_src
1262 #+results:
1263 #+begin_example
1264 {:water 1/2,
1265 :psychic 1/2,
1266 :dragon 1/2,
1267 :fire 1/2,
1268 :ice 1/2,
1269 :grass 1/2,
1270 :ghost 0,
1271 :poison 1/2,
1272 :flying 0,
1273 :normal 0,
1274 :rock 1/2,
1275 :electric 1/4,
1276 :ground 0,
1277 :fighting 1/2,
1278 :dark 0,
1279 :steel 0,
1280 :bug 1/2}
1281 #+end_example
1283 Which makes a particularly beautiful combination which is ineffective
1284 against all defending types.
1287 # #+begin_src clojure :results scalar :exports both
1288 # (with-out-str (clojure.contrib.pprint/pprint (seq (attack-mode (pokemon.types/susceptibility [:normal :normal :normal :normal :ice :ice :ice :ice :electric :electric :electric :rock :rock :ground :ghost :psychic :fighting :grass :poison])))))
1289 # #+end_src
1291 # #+results:
1292 # | [:water 1] | [:psychic 1/2] | [:dragon 1] | [:fire 1/4] | [:ice 1/2] | [:grass 1] | [:ghost 0] | [:poison 1/2] | [:flying 0] | [:normal 0] | [:rock 1/4] | [:electric 1/4] | [:ground 0] | [:fighting 1/2] | [:dark 0] | [:steel 0] | [:bug 1/2] |
1295 Is there anything else that's interesting?
1297 #+begin_src clojure :exports both
1298 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type))
1299 #+end_src
1301 #+results:
1302 : INFEASIBLE
1304 #+begin_src clojure :exports both
1305 (pokemon.types/old-school
1306 (pokemon.lpsolve/solution (pokemon.lpsolve/worst-defense-type)))
1307 #+end_src
1309 #+results:
1310 : INFEASIBLE
1312 #+begin_src clojure :exports both
1313 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type))
1314 #+end_src
1316 #+results:
1317 : INFEASIBLE
1319 #+begin_src clojure :exports both
1320 (pokemon.types/old-school
1321 (pokemon.lpsolve/solution (pokemon.lpsolve/weak-defense-type)))
1322 #+end_src
1324 #+results:
1325 : INFEASIBLE
1327 #+begin_src clojure :exports both
1328 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type))
1329 #+end_src
1331 #+results:
1332 : INFEASIBLE
1334 #+begin_src clojure :exports both
1335 (pokemon.types/old-school
1336 (pokemon.lpsolve/solution (pokemon.lpsolve/neutral-defense-type)))
1337 #+end_src
1339 #+results:
1340 : INFEASIBLE
1342 There is no way to produce a defense-type that is weak to all types.
1343 This is probably because there are many types that are completely
1344 immune to some types, such as Flying, which is immune to Ground. A
1345 perfectly weak type could not use any of these types.
1347 * Summary
1349 Overall, the pok\eacute{}mon type system is slanted more towards defense
1350 rather than offense. While it is possible to create superior
1351 defensive types and exceptionally weak attack types, it is not possible to
1352 create exceptionally weak defensive types or very powerful attack
1353 types.
1355 Using the =lp_solve= library was more complicated than the best-first
1356 search, but yielded results quickly and efficiently. Expressing the
1357 problem in a linear form does have its drawbacks, however --- it's
1358 hard to ask questions such as "what is the best 3-type defensive combo
1359 in terms of susceptibility?", since susceptibility is not a linear
1360 function of a combo's types. It is also hard to get all the solutions
1361 to a particular problem, such as all the pokemon type combinations of
1362 length 8 which are immortal defense types.
1365 * COMMENT main-program
1366 #+begin_src clojure :tangle ../src/pokemon/lpsolve.clj :noweb yes :exports none
1367 <<intro>>
1368 <<body>>
1369 <<declares>>
1370 <<memory-management>>
1371 <<get-results>>
1372 <<solve>>
1373 <<farmer-example>>
1374 <<lp-solve>>
1375 <<better-farmer>>
1376 <<pokemon-lp>>
1377 <<results>>
1378 <<attack-oriented>>
1379 #+end_src
1382 * COMMENT Stuff to do.
1383 ** TODO fix namespaces to not use rlm.light-base