Add Another KSTopCreating a GBBopen ApplicationMaking ConnectionsGoTo Top

Making Connections

We finally did some walking in the last exercise and learned how to display the location unit instances in our walk from the REPL. In this exercise, we learn how to use GBBopen's link capabilities to represent relationships among unit instances. Links are an important aspect of almost every GBBopen application, so it's time that we started taking advantage of them.


This exercise shows you how to:


Prerequisites

 (in-package :gbbopen-user)

  (define-unit-class location ()
    (time 
     x y)
    (:dimensional-values
      (time :point time)
      (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 :time 0 :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)

  ;;; ====================================================================
  ;;;   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)))

  (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))))))))

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

Step 1: Add a link

In the last exercise, we used map-sorted-instances-of-class to display the random walk. Another way that we could represent the walk is by connect each newly created location unit instance to the location unit instance that preceded it in the walk. We'll use GBBopen's link capabilities to do this.

A link is a bidirectional relationship between two unit instances that is implemented by two pointers. From the perspective of a particular unit instance, each link consists of an outgoing, or direct, pointer to another unit instance and an incoming, or inverse, pointer that is stored in unit instance pointed to by the direct pointer. GBBopen automatically maintains the bidirectional-link consistency of these pointers when creating new links, deleting existing links, or deleting unit instances. Links remove the possibility of “one-sided” relationships or “dangling” pointers to deleted unit instances.

Edit the location unit-class definition in your tutorial-example.lisp file, adding two link slots, next-location and previous-location, to the location unit class definition:

  (define-unit-class location ()
    (time 
     x y
     (next-location
      :link (location previous-location :singular t) 
      :singular t)
     (previous-location
      :link (location next-location :singular t)  
      :singular t))
    (:dimensional-values
      (time :point time)
      (x :point x)
      (y :point y))
    (:initial-space-instances (known-world)))

Each link-slot specification is a list whose first element is the name of the link slot. This is followed by the link slot option :link and a concise specification of the inverse link slot associated with that link slot. In this case, the next-location/previous-location link is between instances of the same (location) unit class, but often links are between instances of different unit classes.

Links can be many-to-many, many-to-one, one-to-many, or one-to-one. In this case, the next-location/previous-location link is one-to-one, which is specified by including the :singular t slot option in the link-slot definition (and the corresponding :singular t specification in the concise inverse-link-slot specification). To help clarify the specification of link slot arity, let's temporarily assume that we want a location instance that can have many next locations, but only a single previous location. This link relation would be specified as follows:

     ...
   (next-locations
    :link (location previous-location :singular t)))
   (previous-location
    :link (location next-locations)  
    :singular t)
     ...

We've followed the natural GBBopen convention of giving singular link slots a singular name (such as previous-location) and link slots that can contain multiple links a plural name (such as next-locations). Note that the :singular option is associated with the previous-location link slot as both a slot option in the previous-location link-slot definition and in the concise inverse-link-slot specification for previous-location in the next-locations link-slot definition.

Step 2: Break some links

The concise inverse-link-slot specification supplied by the :link slot option provides a “double entry” redundancy that is useful when links are between instances of different unit classes, as the link can be understood by viewing either class definition. The redundancy also helps GBBopen recognize inconsistencies in link specifications. The function check-link-definitions asks GBBopen to validate that all link definitions are consistent. Let's try it on our current random-walk application. Compile and load the latest changes in your tutorial-example.lisp file (including the new next-location and previous-location link slots). Then check link consistency:

  gbbopen-user> (check-link-definitions)
  ;; All link definitions are consistent.
  t
  gbbopen-user>
GBBopen reports that all link definitions are consistent.

Suppose that we had forgotten to add the previous-location end of the link in our location unit-class definition. Edit the location unit-class definition in your tutorial-example.lisp file, adding the line #+ignore immediately before the previous-location link-slot definition:

  (define-unit-class location ()
    (time 
     x y
     (next-location
      :link (location previous-location :singular t) 
      :singular t)
      #+ignore     
     (previous-location
      :link (location next-location :singular t)  
      :singular t))
    (:dimensional-values
      (time :point time)
      (x :point x)
      (y :point y))
    (:initial-space-instances (known-world)))

The #+ignore read-time conditionalization tells Common Lisp to skip over the next form if ignore is not an element of the feature list *features*. By convention, ignore is never added to *features*, so nobr#+ignore is a handy mechanism for temporarily “commenting out” a single form.

Compile the now-defective definition (using C-c C-c in SLIME or C-c C-x in ELI) and then recheck link consistency:

  gbbopen-user> (check-link-definitions)
  Warning: The inverse of link slot next-location in unit class location 
           refers to link slot previous-location which is not present in 
           unit class location.
  nil
  gbbopen-user>
As expected, GBBopen alerts us to the problem.

Remove the #+ignore that we just added and comment out the :singular t portion of the inverse link-slot specification in next-location:

  (define-unit-class location ()
    (time 
     x y
     (next-location
      :link (location previous-location) ; :singular t) 
      :singular t)
      #+ignore     
     (previous-location
      :link (location next-location :singular t)  
      :singular t))
    (:dimensional-values
      (time :point time)
      (x :point x)
      (y :point y))
    (:initial-space-instances (known-world)))
Compile the again-defective definition (using C-c C-c in SLIME or C-c C-x in ELI) and then recheck link consistency:
  gbbopen-user> (check-link-definitions)
  Warning: Link slot next-location in unit class location incorrectly 
           declares its inverse link slot previous-location in unit 
           class location as not singular.
  nil
  gbbopen-user>
Once again, GBBopen has alerted us to the problem.

Restore the :singular t portion of the inverse link-slot specification in next-location that we just commented out:

  (define-unit-class location ()
    (time 
     x y
     (next-location
      :link (location previous-location) ; :singular t) 
      :singular t)
     (previous-location
      :link (location next-location :singular t)  
      :singular t))
    (:dimensional-values
      (time :point time)
      (x :point x)
      (y :point y))
    (:initial-space-instances (known-world)))
Then recompile and recheck link consistency:
  gbbopen-user> (check-link-definitions)
  ;; All link definitions are consistent.
  t
  gbbopen-user>

Step 3: Create some links

Let's use our newly defined next-location/previous-location link to connect our location unit instances. Edit the random-walk-ks-function definition in your tutorial-example.lisp file, adding the trigger instance as a new :previous-location argument to make-instance:

  (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
              :previous-location trigger-instance))
           ;; Otherwise, tell the user that we've walked too far away:
           (t (format t "~2&Walked off the world: (~d, ~d).~%" x y))))))))

Compile the random-walk-ks-function (using C-c C-c in SLIME or C-c C-x in ELI) and then run the application:

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

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

Let's describe a couple of location unit instances to check our work. First, the initial location unit instance:

  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:
      next-location:  #<location 2 (-10 10)>
      previous-location:  nil
  gbbopen-user>
Note that the next-location link slot points to the next location unit instance in our random walk. Let's describe that unit instance:
  gbbopen-user> (describe-instance (find-instance-by-name 2 'location))
  Location #<location 2 (-10 10)>
    Instance name: 2
    Space instances: ((known-world))
    Dimensional values:
      time:  1
      x:  -10
      y:  10
    Non-link slots:
      time:  1
      x:  -10
      y:  10
    Link slots:
      next-location:  #<location 3 (-6 19)>
      previous-location:  #<location 1 (0 0)>
  gbbopen-user>
The next-location link slot in location 2 points to the third location unit instance in our random walk and its previous-location link slot points back to the initial location unit instance.

We can now follow the links to display the random walk:

  gbbopen-user> (loop with location = (find-instance-by-name 1 'location) 
                  do (print location)
                  while (setf location (next-location-of location)))

  #<location 1 (0 0)> 
  #<location 2 (-10 10)> 
  #<location 3 (-6 19)> 
  #<location 4 (0 14)> 
  #<location 5 (-1 14)> 
  #<location 6 (8 10)> 
  #<location 7 (17 3)> 
  #<location 8 (7 -6)> 
  #<location 9 (10 4)> 
  #<location 10 (5 -5)> 
     ...
  #<location 60 (29 17)> 
  #<location 61 (31 21)> 
  #<location 62 (40 23)> 
  #<location 63 (45 28)> 
  nil
  gbbopen-user>

Step 4: Define a “print walk” KS

Let's add a new KS, print-walk-ks, that displays the random walk once it is completed. Add the following KS to the end of your tutorial-example.lisp file:

  ;;; ====================================================================
  ;;;   Print-walk KS

  (defun print-walk-ks-function (ksa)
    ;;; Starting with the initial location instance, print the instance 
    ;;; name and location of the walk
    (declare (ignore ksa))
    (format t "~2&The random walk:~%")
    (let ((instance (find-instance-by-name 1 'location)))
      (while instance
        (format t "~s (~s ~s)~%"
                (instance-name-of instance)
                (x-of instance)
                (y-of instance))
        (setf instance (next-location-of instance))))
    ;; Tell the Agenda Shell to exit:
    ':stop)

  (define-ks print-walk-ks
    :trigger-events ((quiescence-event))
    :rating 100
    :execution-function 'print-walk-ks-function)
The print-walk-ks is triggered by a quiescence-event. Recall that the Agenda Shell signals that quiescence has occurred when no executable KSAs are available to be executed and then it continues for an additional KS-execution cycle in case any executable KSAs resulted from the quiescence event. So, print-walk-ks will be triggered once no random-walk-ks KSAs are triggered by newly created location unit instances.

The print-walk-ks-function follows the next-location/previous-location link to display the walk. More importantly, the function returns the keyword symbol :stop. The Agenda Shell checks the value returned by a KS execution function for this special indicator and, if it is returned, the control shell is exited. If we did not return :stop, the print-walk-ks KS would be triggered and activated on the first quiescence-event, the KSA would execute, then the Agenda Shell would detect another quiescence condition, signal a new quiescence-event, and our application would print the random walk over and over again.

Let's compile our latest changes and then run our application with the new print-walk-ks KS in place:

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

  Walked off the world: (54, 15).

  The random walk:
  1 (0 0)
  2 (-6 9)
  3 (-14 8)
  4 (-5 6)
  5 (-13 5)
  6 (-11 13)
  7 (-11 4)
  8 (-17 8)
  9 (-21 15)
  10 (-12 14)
     ...
  35 (40 28)
  36 (50 22)
  37 (49 12)
  38 (47 10)
  ;; Explicit :stop issued by KS print-walk-ks
  ;; Control shell 1 exited: 41 cycles completed
  ;; Run time: 0.01 seconds
  ;; Elapsed time: 0 seconds
  :stop
  gbbopen-user>


The GBBopen Project


Add Another KSTopCreating a GBBopen ApplicationMaking ConnectionsGoTo Top