Application Startup and Event FunctionsTopMaking ConnectionsAdd Another KSGoTo Top

Add Another KS

The last exercise made it easy to initialize and run our application repeatedly by simply starting the Agenda Shell. We also specified the dimensionality of our known-world space instance relative to the dimensional specifications of the location unit class. With these niceties in place, its time to move beyond our initial location unit instance.


This exercise shows you how to:


Prerequisites

  (in-package :gbbopen-user)

  (define-unit-class location ()
    (x y)
    (:dimensional-values
      (x :point x)
      (y :point y))
    (:initial-space-instances (known-world)))

  (defmethod print-instance-slots ((location location) stream)
    (call-next-method)
    (when (and (slot-boundp location 'x)
               (slot-boundp location 'y))
      (format stream " (~s ~s)"
              (x-of location)
              (y-of location))))

  ;;; ====================================================================
  ;;;   Startup KS

  (defun startup-ks-function (ksa)
    (declare (ignore ksa))
    ;; Create an initial location unit instance at (0,0):
    (make-instance 'location :x 0 :y 0))

  (define-ks startup-ks
      :trigger-events ((control-shell-started-event))
      :execution-function 'startup-ks-function)

  ;;; ====================================================================
  ;;;   Initializations (run at Agenda Shell startup)

  (defun initializations (event-name &key &allow-other-keys)
    (declare (ignore event-name))
    ;; Clean up any previous run:
    (delete-blackboard-repository)
    ;; Make a new known-world space instance:
    (make-space-instance 
     '(known-world)
     :dimensions (dimensions-of 'location)))

  (add-event-function 'initializations 'control-shell-started-event
                      ;; Initializations should be done first!
                      :priority 100)

Step 1: Add another dimension

It's time we introduce the notion of time to our application. Edit the location unit-class definition in tutorial-example.lisp, adding a new slot, time, to the location unit class definition and a corresponding time dimensional value:

  (define-unit-class location ()
    (time 
     x y)
    (:dimensional-values
      (time :point time)
      (x :point x)
      (y :point y))
    (:initial-space-instances (known-world)))

Recall that we specified that the dimensions of the known-world space instance that is created by our initializations function relative to the dimensions of the location unit class:

  (make-space-instance 
      '(known-world)
      :dimensions (dimensions-of 'location)))
Therefore, we don't need to modify our call to make-space-instance in order to add time as a dimension of known-world.

Next, modify startup-ks-function in tutorial-example.lisp so that it creates the initial location unit instance at time 0:

  (defun startup-ks-function (ksa)
    (declare (ignore ksa))
    ;; Create an initial location unit instance at (0,0) at time 0:
    (make-instance 'location :time 0 :x 0 :y 0))

Step 2: A test of time

Let's verify our work. Compile and load the tutorial-example.lisp file directly from the editor buffer (using C-c C-k in SLIME; C-c C-b in ELI) and start the Agenda Shell:

  gbbopen-user> (start-control-shell)
  ;; Control shell 1 started
  ;; No executable KSAs remain, exiting control shell
  ;; Control shell 1 exited: 3 cycles completed
  ;; Run time: 0 seconds
  ;; Elapsed time: 0 seconds
  :quiescence
  gbbopen-user>

Check that the initial location unit instance is at time zero:

  gbbopen-user> (describe-instance (find-instance-by-name 1 'location))
  Location #<location 1 (0 0)>
    Instance name: 1
    Space instances: ((known-world))
    Dimensional values:
      time:  0
      x:  40
      y:  60
    Non-link slots:
      time:  0
      x:  40
      y : 60
    Link slots: None
  gbbopen-user>

Step 3: Define another KS

Define a KS called random-walk-ks that:

Step 3a: Define a utility function

Begin implementing the random-walk-ks by adding the following utility function to the end of your tutorial-example.lisp file:

  ;;; ====================================================================
  ;;;   Random-walk KS

  (defun add-linear-variance (value max-variance)
    ;;; Returns a new random value in the interval
    ;;; [(- value max-variance), (+ value max-variance)]
    (+ value (- (random (1+ (* max-variance 2))) max-variance)))
Then compile the definition (using C-c C-c in SLIME or C-c C-x in ELI) and evaluate the following test in the REPL:
  gbbopen-user> (dotimes (i 15) (printv (add-linear-variance 0 10)))
  ;;  (add-linear-variance 0 10) => 8
  ;;  (add-linear-variance 0 10) => 9
  ;;  (add-linear-variance 0 10) => 4
  ;;  (add-linear-variance 0 10) => 3
  ;;  (add-linear-variance 0 10) => -4
  ;;  (add-linear-variance 0 10) => -10
  ;;  (add-linear-variance 0 10) => -1
  ;;  (add-linear-variance 0 10) => 0
  ;;  (add-linear-variance 0 10) => 4
  ;;  (add-linear-variance 0 10) => 5
  ;;  (add-linear-variance 0 10) => 8
  ;;  (add-linear-variance 0 10) => -5
  ;;  (add-linear-variance 0 10) => -3
  ;;  (add-linear-variance 0 10) => 7
  ;;  (add-linear-variance 0 10) => 6
  nil
  gbbopen-user>

Because add-linear-variance is stochastic, your results will be similar but not identical. Note that we used GBBopen's printv macro to display the result of each generated value. Printv can greatly assist debugging by printing forms and the results of evaluating them. Printv can be transparently wrapped around any form in a complex function definition, as it evaluates and displays all the forms in its body and returns the values resulting from evaluating the last form:

  gbbopen-user>  (printv "Some multiple values" (values 1 2) "Some more" (values 3 4 5))
  ;; Some multiple values
  ;;  (values 1 2) => 1; 2
  ;; Some more
  ;;  (values 3 4 5) => 3; 4; 5
  4
  5
  6
  gbbopen-user>

Step 3b: Define the random-walk-ks execution function

Next add the following KS-execution function to the end of your tutorial-example.lisp file:

  (defun random-walk-ks-function (ksa)
    ;;; Move to the next (random) location in the world
    (let* ((trigger-instance (sole-trigger-instance-of ksa))
           ;; The new time is one greater than the stimulus's time:
           (time (1+ (time-of trigger-instance))))
      (cond
       ;; If the maximum time value (75) is reached, tell the user we've
       ;; walked too long:
       ((>= time 75) (format t "~2&Walked too long.~%"))
       (t ;; The new location is +/- 10 of the stimulus's location:
        (let ((x (add-linear-variance (x-of trigger-instance) 10))
              (y (add-linear-variance (y-of trigger-instance) 10)))
          (cond
           ;; Check that the new location is within the known-world
           ;; boundaries.  If so, create the new location instance:
           ((and (<= -50 x 50) (<= -50 y 50))
            (make-instance 'location 
              :time time 
              :x x 
              :y y))
           ;; Otherwise, tell the user that we've walked too far away:
           (t (format t "~2&Walked off the world: (~d, ~d).~%" x y))))))))

Unlike the KS-execution functions that we have defined previously, random-walk-ks-function does not ignore its ksa argument. Instead, it calls sole-trigger-instance-of with the ksa unit-instance argument in order to obtain the location unit instance whose creation triggered the KSA. This pattern of obtaining the unit instance that triggered a KSA and then using that triggering unit instance as the context for the KS execution is typical of many KSs.

Step 3c: Add the random-walk-ks definition

Finally, add this define-ks form to the end of your tutorial-example.lisp file to complete the random-walk-ks definition:

  (define-ks random-walk-ks
     :trigger-events ((instance-created-event location))
     :rating 100
     :execution-function 'random-walk-ks-function)

Step 4: Run the application

Compile and load the random-walk-ks forms, and then start the Agenda Shell:

  gbbopen-user> (start-control-shell)
  ;; Control shell 1 started

  Walked off the world: (23, 55).
  ;; No executable KSAs remain, exiting control shell
  ;; Control shell 1 exited: 64 cycles completed
  ;; Run time: 0.01 seconds
  ;; Elapsed time: 0 seconds
  :quiescence
  gbbopen-user>

It looks like something happened! (Again, because add-linear-variance is stochastic, your results will be similar but not identical.) Let's look at the blackboard repository and see how many location unit instances were created:

  gbbopen-user> :dsbb

  Space Instance                Contents
  --------------                --------
  known-world                   61 instances (61 location)

  Unit Class                    Instances
  ----------                    ---------
  control-shell                         1 *
  ks                                    1 +
  ksa-queue                             2 +
  location                             61
  ordered-ksa-queue                     1 +
  standard-space-instance               1
                                ---------
                                       67 instances
  gbbopen-user>

The 61 location instances makes sense. Previously, it required 3 control-shell cycles to create the initial location unit instance (one to execute the initial-ks KSA followed by two additional cycles of quiescence before the Agenda Shell exits). We now create one additional location unit instance with every execution of random-walk-ks, so we always create 3 fewer location instances than the total number of control-shell cycles.

Step 5: Where have we been?

It would be interesting to see where our random walk has taken us. We could use GBBopen's map-instances-of-class iterator to print each of the location unit instances:

  gbbopen-user> (map-instances-of-class #'print 'location)

  #<location 58 (5 31)> 
  #<location 13 (-7 10)> 
  #<location 26 (-40 35)> 
  #<location 39 (-4 3)> 
  #<location 52 (2 23)> 
  #<location 7 (3 17)> 
  #<location 20 (2 27)> 
  #<location 33 (-25 6)> 
  #<location 46 (-2 32)> 
     ...
  #<location 31 (-22 18)> 
  #<location 44 (-7 14)> 
  #<location 57 (2 41)> 
  #<location 12 (-15 15)> 
  #<location 25 (-32 38)> 
  #<location 38 (-10 -4)> 
  #<location 51 (-2 16)> 
  #<location 6 (10 27)> 
  #<location 19 (-1 17)> 
  #<location 32 (-25 12)> 
  #<location 45 (-7 23)> 
  nil
  gbbopen-user>
Unfortunately, the order that unit instances are supplied to the print function is not controllable. Our walk would be much clearer if we printed the location unit instances in time order.

We might consider taking advantage of the instance names that GBBopen assigns to unit instances. We could do something like the following:

  gbbopen-user> (dotimes (i 76)
                  (let ((location (find-instance-by-name i 'location)))
                    (when location
                       (print location))))

  #<location 1 (0 0)> 
  #<location 2 (10 4)> 
  #<location 3 (19 10)> 
  #<location 4 (14 9)> 
  #<location 5 (14 18)> 
  #<location 6 (10 27)> 
  #<location 7 (3 17)> 
  #<location 8 (-6 20)> 
  #<location 9 (4 15)> 
  #<location 10 (-5 14)> 
     ...
  #<location 50 (5 26)> 
  #<location 51 (-2 16)> 
  #<location 52 (2 23)> 
  #<location 53 (9 33)> 
  #<location 54 (7 43)> 
  #<location 55 (-2 36)> 
  #<location 56 (0 46)> 
  #<location 57 (2 41)> 
  #<location 58 (5 31)> 
  #<location 59 (13 39)> 
  #<location 60 (17 41)> 
  #<location 61 (21 50)> 
  nil
  gbbopen-user>

This is a bad idea for several reasons. First, we are looking up every location unit instance by its instance name, which is less efficient than operating on location instances directly. While this isn't an significant issue in expressions that we evaluate in the REPL to investigate our application, we should seek to avoid such inefficiencies in application code. More importantly, however, the location instance name just happens to mirror the sequencing that we really want to display: the time value of the locations. We should find a way to sequence location printing that relies on the time values directly.

GBBopen provides a variant of map-instances-of-class, called map-sorted-instances-of-class, that sorts the unit instances based on a comparison predicate and an optional :key accessor function that suits our needs:

  gbbopen-user> (map-sorted-instances-of-class #'print 'location #'< 
                   :key #'time-of)

  #<location 1 (0 0)> 
  #<location 2 (10 4)> 
  #<location 3 (19 10)> 
  #<location 4 (14 9)> 
  #<location 5 (14 18)> 
  #<location 6 (10 27)> 
  #<location 7 (3 17)> 
  #<location 8 (-6 20)> 
  #<location 9 (4 15)> 
  #<location 10 (-5 14)> 
     ...
  #<location 50 (5 26)> 
  #<location 51 (-2 16)> 
  #<location 52 (2 23)> 
  #<location 53 (9 33)> 
  #<location 54 (7 43)> 
  #<location 55 (-2 36)> 
  #<location 56 (0 46)> 
  #<location 57 (2 41)> 
  #<location 58 (5 31)> 
  #<location 59 (13 39)> 
  #<location 60 (17 41)> 
  #<location 61 (21 50)> 
  nil
  gbbopen-user>

Using map-sorted-instances-of-class involves a sorting operation, so this approach still has some efficiency concerns for use in application code. However, it suits our REPL-exploration needs just fine. (There is a do-sorted-instances-of-class macro, if an iterative style is preferred over a mapper.) We will explore a more efficient approach to displaying the random walk in the next exercise.

Step 6: Run the application a few more times

If we run the application a few more times, we eventually encounter a case where we create the allotted 75 location unit instances without walking off the known-world:

  gbbopen-user> (start-control-shell)
  ;; Control shell 1 started

  Walked too long.
  ;; No executable KSAs remain, exiting control shell
  ;; Control shell 1 exited: 78 cycles completed
  ;; Run time: 0.04 seconds
  ;; Elapsed time: 0 seconds
  :quiescence
  gbbopen-user>

Here is one such random walk:

  gbbopen-user> (map-sorted-instances-of-class #'print 'location #'< 
                   :key #'time-of)

  #<location 1 (0 0)> 
  #<location 2 (2 7)> 
  #<location 3 (-1 5)> 
  #<location 4 (-1 0)> 
  #<location 5 (3 -2)> 
  #<location 6 (13 -7)> 
  #<location 7 (8 -5)> 
  #<location 8 (1 2)> 
  #<location 9 (8 0)> 
  #<location 10 (5 8)> 
     ...
  #<location 70 (-13 -11)> 
  #<location 71 (-13 -6)> 
  #<location 72 (-9 -6)> 
  #<location 73 (1 -4)> 
  #<location 74 (-8 -11)> 
  #<location 75 (-13 -15)> 
  nil
  gbbopen-user>


The GBBopen Project


Application Startup and Event FunctionsTopMaking ConnectionsAdd Another KSGoTo Top