Skip to content
Haoxi Zhan edited this page Nov 20, 2013 · 2 revisions

Now, as you have known how to run a Clojush example, you must want to write one for yourself. This section exactly acts as a tutorial of writing Clojush examples.

A most basic Clojush example is consist of several parts:

namespace

create a namespace and use packages

define-registered

use define-registered macro to define some registered instructions.

argmap

The arguments map.

error-function

The function to test the error.

atom-generator

A list of atoms which represents the instructions which could be used in the Push program.

namespace

The namespace should be the same as your file name. For example, if your example is called test.clj, the name of the namespace should be clojush.examples.test. Also, you need to indicate which packages to use in this namespace. Normally, clojush.pushgp.pushgp, clojush.pushstate, clojush.interpreter and clojush.random are used. clojush.math.numerical-tower is also used in many examples.

This is an example code of a namespace segment:

(ns clojush.examples.test
  (:use [clojush.pushgp.pushgp]
        [clojush.pushstate]
        [clojush.interpreter]
        [clojush.random]))

define-registered

define-registered is a macro which appears in clojush.pushstate in the 44th line.

define-registered code
(defmacro define-registered
  [instruction definition]
  `(do (register-instruction '~instruction)
       (swap! instruction-table assoc '~instruction ~definition)))

It would register-instruction the instruction and then associate the instruction and definition in the instruction table.

register-instruction code
(defn register-instruction
  "Add the provided name to the global list of registered instructions."
  [name]
  (swap! registered-instructions conj name))

register-instruction would conjoint the name of the instruction onto the registered-instructions.

Both registered-instructions and instruction-table are atoms:

(def registered-instructions (atom #{})) ;;pushstate.clj:35
(def instruction-table (atom (hash-map))) ;;pushstate.clj:42

A most regular instruction to register here is in, which uses stack-ref to ref the stack :auxiliary and then push it into the :integer stack (or other stack according to the question to solve). The in instruction is used to re-push the input whenever its needed.

stack-ref appears at line 71 in clojush.pushstate.

stack-ref code
(defn stack-ref
  "Returns the indicated item of the type stack in state. Returns :no-stack-item if called
   on an empty stack. This is a utility, not for use as an instruction in Push programs.
   NOT SAFE for invalid positions."
  [type position state]
  (let [stack (type state)]
    (if (empty? stack)
      :no-stack-item
      (nth stack position))))

Obviously, it would ref the position in the type stack in state by using nth.

push-item is at line 56 in clojush.pushstate.

push-item code
(defn push-item
  "Returns a copy of the state with the value pushed on the named stack. This is a utility,
   not for use in Push programs."
  [value type state]
  (assoc state type (cons value (type state))))

It would return a copy of state in which value has been pushed on the type stack.

Note
It is a pure function which returns a new state rather swapping an atom.

argmap, error function, atom-generators

argmap is the pushGP arguments for the example to run. Two essential elements of the argmap are error-function and atom-generators.

error-function

An error-function is a function which is used to test the error of a generated program. If an automatically-evolved program returns the right answer of a test, the error is supposed to be 0. More far away is the running result from the standard solution, the higher the error is supposed to be.

To write an error-function, it’s always good to remember that it is a function. This function would have an argument program, which represents the program to test. Of course, the program to test would be those in the current generation.

A typical error-function would use doall, for and local bindings. A list of inputs to test would be defined inside the error function, while for would be used to bind all of them.

Note
for in Clojure is not for loop. It is a List comprehension.

For each input tests, local bindings with let would be used. Usually, state would be defined to be the state after running the program while a top-$stack is also defined to represent the top-item of the stack in which we want to find the result.

An example of a state binding
(run-push program
	(push-item input :auxiliary
		(push-item input :integer
			(make-push-state))))

make-push-state is defined in pushstate.clj which creates a new state. The nested push-items would push the input to both the :integer stack and the :auxiliary stack. Finally run-push would run the program in such a state.

run-push code
(defn run-push 
  "The top level of the push interpreter; calls eval-push between appropriate code/exec 
   pushing/popping. The resulting push state will map :termination to :normal if termination was 
   normal, or :abnormal otherwise."
  ([code state]
    (run-push code state false false))
  ([code state print-steps]
    (run-push code state print-steps false))
  ([code state print-steps trace]
    (run-push code state print-steps trace false))
  ([code state print-steps trace save-state-sequence]
    (let [s (if @global-top-level-push-code (push-item code :code state) state)]
      (let [s (push-item code :exec s)]
        (when print-steps
          (printf "\nState after 0 steps:\n")
          (state-pretty-print s))
        (when save-state-sequence
          (reset! saved-state-sequence [s]))
        (let [s (eval-push s print-steps trace save-state-sequence)]
          (if @global-top-level-pop-code
            (pop-item :code s)
            s))))))

The binding of top-$stack is much simpler. The following is an example of top-bool:

(top-item :boolean state)

top-item, which is defined in the 62th line of pushstate.clj, returns the top top-item of one stack.

top-item code
(defn top-item
  "Returns the top item of the type stack in state. Returns :no-stack-item if called on
   an empty stack. This is a utility, not for use as an instruction in Push programs."
  [type state]
  (let [stack (type state)]
    (if (empty? stack)
      :no-stack-item
      (first stack))))

After all these, you need to write something to give the program an error value. It would be based on the top-$stack you get with a standard solution calculated by your functions. If there’s not item in the expected stack, a penalty should be given. It’s usually a large value which is supposed to be bigger than the normal error value of other programs.

atom-generators

The atom-generators is not a function. It is a list of instructions which is allowed to be used in the generation of Push programs.

Two functions are defined in the end of pushtate.clj for the ease of writing this part.

registered-for-type code
(defn registered-for-type
  "Returns a list of all registered instructions with the given type name as a prefix."
  [type & {:keys [include-randoms] :or {include-randoms true}}]
  (let [for-type (filter #(.startsWith (name %) (name type)) @registered-instructions)]
    (if include-randoms
      for-type
      (filter #(not (.endsWith (name %) "_rand")) for-type))))

registered-for-type is a function which would return a list of all registered instructions for a specific type. For example, (registered-for-type :integer) would return a list of instructions for integer which contains those instructions for randoms. If you want to exclude the random instructions, use (registered-for-type :integer :include-randoms false).

registered-nonrandom
(defn registered-nonrandom
  "Returns a list of all registered instructions aside from random instructions."
  []
  (filter #(not (.endsWith (name %) "_rand")) @registered-instructions))

registered-nonrandom would return all the registered instructions which are not dealing with randoms.

Other instructions could also be defined and listed with list. A set of functions often used here is a set of lrand functions defined in random.clj. lrand-int would return a random int [0,n] (n is an argument). These functions are from the clj-random library.

Finally, use concat to concat all the instructions together and make them the atom-generators.

atom-generators example
(def atom-gen
      (concat (registered-nonrandom)
	      (list (fn [] (lrand-int 100))
		    'in)))
Other arguments

A lot of other arguments are available. They are defined in pushgp.clj in push-argmap. However, the most common arguments beside the 2 above are population-size and max-generations. population-size sets the size of the population when evolving. max-generations configures the maximum number of generations allowed. If no solutions is found when this limit is reached, the program would stop and print failure information.