-
Notifications
You must be signed in to change notification settings - Fork 94
First example
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.
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
is a macro which appears in clojush.pushstate
in the 44th line.
(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.
(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
.
(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
.
(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
is the pushGP arguments for the example to run. Two essential elements of the argmap
are
error-function and atom-generators.
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.
(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.
(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.
(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.
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.
(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)
.
(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
.
(def atom-gen
(concat (registered-nonrandom)
(list (fn [] (lrand-int 100))
'in)))
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.