--- layout: ../Site.layout.js --- # Leonardo Calculus Knowledge Representation: Defining `sensors` `sense` and `sense2` `lispdef` helper action entities We can define an action for our knowledgebase by adding an entity with a lisp source form using the `leodef` macro which lets us specify whether this is intended as a lisp `defun` or new `action`. This article follows from [this one setting up and creating a knowledgebase](/lispgames/plant-insect-bird-ontology/) and [this one filling in some organism ontology attributes](/lispgames/LCKR-fleshing-out-organisms-attributes/). Writing actions in lisp like this makes their insides opaque to the Leonardo system itself, though the form itself being stored by the leonardo system is obviously available for analysis. The idiom of containing `lispdef` entities that contain source for concrete implementations of our ontology in our ontology, e.g. making it self-evidently useful and useable is quite a point of distinction for [Sandewall's Leonardo system](https://www.ida.liu.se/ext/leonardo/) ([my openaccess academic bibliography here](/complex/Sandewall-caisor-bibliography/)). I am choosing to use the [powerful macro package `cl-series`](https://gitlab.common-lisp.net/rtoy/cl-series/\-/wikis/Series-User's-Guide) here, which expands declarative expressions into an efficient, lazy traversal while clearly indicating and assigning error codes to any incorrect statements via its compilation-time static analysis. This does not introduce any runtime dependencies to our ontology's concrete realization, while adding additional compilation-time checks and purely ANSI CL optimizations using a prolog approach. I consider this to ideally complement writing the lowest level new atomic actions for our knowledgebase. Here let us implement a helper, `sense`, in our `sensors` `entityfile`. ADDENDUM: I had written `values` somewhere I meant to write `lisp` in `sense2`, fixed now. ## Setup [Same setup as always (refer first article)](/lispgames/plant-insect-bird-ontology/). The [attribute definitions being referred to are from this second article](/lispgames/LCKR-fleshing-out-organisms-attributes/). The way *this* and other articles work for me is [Eduardo's eev eepitching](https://anggtwu.net/eepitch.html) to slime buffers.  (setq eepitch-buffer-name "*slime-repl clisp*") ## Loading `sensors` and adding a `lispdef` to it `loadk organisms-kb` `setk organisms-kb` `loadk organisms` `loadk sensors` `put sense type lispdef` `addmember (get sensors contents) sense` `writefil sensors` `loadk sensors` Alright, now *we have an entity to contain a lisp form defining an action* ## Get `cl-series` ready from the lisp side `. (require :series)` ## Write the lisp form To start with, let's just make a list defun that prints the contents of the `world` `entityfile`. This is a helper lisp function that belongs to our knowledgebase. Admittedly, I find this step annoying. Sandewall specifically leaves it to you how you physically write your lisp code. In particular, we have to visit the `sensors` `entityfile` ourselves, which if you followed me will be in `#p"~/leocommunity/Plantworld/demus/Organisms/sensors.leo"`. ### The `sensors` `entityfile` to begin with ``` --------------------------------------------------------- -- sensors [: type entityfile] [: latest-written "2025-07-13/23:43.+12"] [: contents ] [: changed-since-archived t] [: nullvalued {has-purpose has-author requires mustload removed-entities leos-extension has-profile overlay-on overlay-types overlay-own leos-use dont-display sections local-ents purpose author latest-archived-entity latest-rearchived}] --------------------------------------------------------- -- sense [: type lispdef] [: latest-rearchived nil] ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ``` We are meant to add our `leodef` or whatever in that `sense` section. ### cl-series `sense` definition that just prints `world`'s `contents` in the `sensors` `entityfile` ``` --------------------------------------------------------- -- sensors [: type entityfile] [: latest-written "2025-07-13/23:43.+12"] [: contents ] [: changed-since-archived t] [: nullvalued {has-purpose has-author requires mustload removed-entities leos-extension has-profile overlay-on overlay-types overlay-own leos-use dont-display sections local-ents purpose author latest-archived-entity latest-rearchived}] --------------------------------------------------------- -- sense [: type lispdef] [: latest-rearchived nil] (leodef sense nil (organism) (series::let* ((world (series::scan (cdadr (get 'world 'contents))))) (series::iterate ((thing world)) (print thing)))) ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo ``` Remember, cl-series' mechanism is to replace forms like `let*` with special entypoints into native-code-generating compilation-time static analysis that turn placeholder `series` expressions into an efficient in-order traversal. We have to use the namespace manually, since series' symbols would collide with the Leonardo system if we tried to `series::install` it. As you can see, the arguement currently gets ignored and we just iteratively `print` the contents of `world`. From the lisp view, sequences `` ⇔ the tagged list: `(seq& (elements of the sequence))`. Hence the `cdadr`. Making full use of the underlying lisp (and *just* the underlying lisp ; remember series expands to *just lisp*) in low-level, atomic new actions is idiomatic (though regretful, in that they are fairly opaque). ### Using this `sense` lisp-side ``` ses.070) loadk sensors Load-ef: sensors at ../../../demus/Organisms/sensors.leo ses.071) ...> . (1+ 1) 2 ses.072) . (sense 'foo) coelacanth nil ses.073) ``` It's the `coelacanth` we made in the second article. ## A simulatively useful *real* `sense2` definition `put sense2 type lispdef` `addmember (get sensors contents) sense2` `writefil sensors` The last one was an indicative example of generating the native lisp at compile-time using the series macro package. I'm imagining (allowing, it's my simulation and I can just pick what I want a `sensor` `sense` to mean). (Actually, it ended up `loop`y rather than series; and it's big but keep scrolling down and we will continue talking) ``` --------------------------------------------------------- -- sensors [: type entityfile] [: latest-written "2025-07-14/21:01.+12"] [: contents ] [: changed-since-archived t] [: nullvalued {has-purpose has-author requires mustload removed-entities leos-extension has-profile overlay-on overlay-types overlay-own leos-use dont-display sections local-ents purpose author latest-archived-entity latest-rearchived}] --------------------------------------------------------- -- sense [: type lispdef] [: latest-rearchived nil] (leodef sense nil (organism) (series::let* ((world (series::scan (cdadr (get 'world 'contents))))) (series::iterate ((thing world)) (print thing)))) --------------------------------------------------------- -- sense2 [: type lispdef] [: latest-rearchived nil] (leodef sense2 sense2 (the-organism) (let* ((scale (get the-organism 'sensor-scale)) (off-direction-counts `((ene 0 ; current count ,(* scale 2 ) ; xmin ,(* scale 3 ) ; xmax ,(* scale 1 ) ; ymin ,(* scale 2 ) ; ymax ) (nne 0 ,(* scale 1) ,(* scale 2) ,(* scale 2) ,(* scale 3)) (nnw 0 ,(* scale -1) 0 ,(* scale 2) ,(* scale 3)) (sse 0 ,(* scale 1) ,(* scale 2) ,(* scale -2) ,(* scale -1)) (ssw 0 ,(* scale -1) 0 ,(* scale -2) ,(* scale -1)) (ene 0 ,(* scale 2) ,(* scale 3) ,(* scale 1) ,(* scale 2)) (ese 0 ,(* scale 2) ,(* scale 3) ,(* scale -1) ,(* scale 0)) (wnw 0 ,(* scale -2) ,(* scale -1) ,(* scale 1) ,(* scale 2)) (wsw 0 ,(* scale -2) ,(* scale -1) ,(* scale -1) ,(* scale 0))))) (flet ((choose-dir (lastdir openangle results) (let* ((directions '(e ene ne nne n nnw nw wnw w wsw sw ssw s sse se ese)) (idx (search `(,lastdir) directions)) (valid (loop :With len := (length directions) :for n :from 1 :by 2 :for theta :from 22.5 :by 45 :below openangle :nconc (list (nth (mod (+ idx n) len) directions) (nth (mod (- idx n) len) directions)))) (score (loop :for compass :in '(e ne n nw w sw s se) :for right :in '(ene nne nnw wnw wsw ssw sse ese) :for left :in '(ese ene nne nnw wnw wsw ssw sse) :collect (let ((sum 0)) (when (member right valid) (incf sum (or (cadr (assoc right results)) 0))) (when (member left valid) (incf sum (or (cadr (assoc left results)) 0))) (list compass sum))))) (loop :for (dir score2) := (car score) :then (if (> nscore score2) (list ndir nscore) (list dir score2)) :for (ndir nscore) :in (cdr score) :finally (setf (get the-organism 'current-direction) dir))))) (loop :with xo := (get the-organism 'x-position) :with yo := (get the-organism 'y-position) :for organism :in (cdadr (get 'world 'contents)) :for x := (get organism 'x-position) :for y := (get organism 'y-position) :do (print organism) (loop :for (dir cur xmin xmax ymin ymax) :in off-direction-counts :for dx := (- x xo) :for dy := (- y yo) :do (print (list dir cur xmin '<= dx '< xmax '& ymin '<= dy '< ymax)) :when (and (<= xmin dx) (< dx xmax) (<= ymin dy) (< dy ymax)) :do (print "success") (incf (cadr (assoc dir off-direction-counts)) (or (cadadr (find (get organism 'type) (cadr (get the-organism 'sensor-weights)) :key 'caadr)) 0)) (print (find (get organism 'type) (cadr (get the-organism 'sensor-weights)) :key 'caadr)) (print off-direction-counts) (return)) :finally (choose-dir (get the-organism 'current-direction) (get the-organism 'opening-angle) off-direction-counts))))) ooooooooooooooooooooooooooooooooooooooooooooooooooooooooo By the way, I can put whatever I want after the oooooendofentities. ``` Well, I guess `sense` is about three different actions smooshed into one, and I ended up using [the loop facility](https://www.lispworks.com/documentation/HyperSpec/Body/m_loop.htm) instead of *cl-series* here. I spent quite a lot of time inside [sldb](https://slime.common-lisp.dev/doc/html/Debugger.html#Debugger) debugger looking at values in stackframes, as well as Leonardo system to get the huge function working. Sorry for the excuse. The meaning is that in-my-universe, "there's one `sense` action that just does all this". I guess it 1. ∀ organisms in `(get world contents)` 1. Tallies organisms by their 2° intercardinal sensor direction, with some sensor characteristic scale 1. Zeroes sensor readings outside the viewer's opening angle in front of it 1. Nonlinearly sets a new 8-compass direction for the viewer by `max` of adjacent readings Where `(get world contents)` is thought to be sparse. I believe this [spans Braitenberg's Vehicles, and in Braitenbergian fashion](/complex/book-review-braitenberg-vehicles/), we could compose meta-vehicles, a sort of higher order animal with multiple sensors rules (different scales, opening angles, signal weights..). # Try out `sensor` `put blackbird type organism` `addmember (get world contents) blackbird` `put snail type organism` `addmember (get world contents) snail` `. (sense 'blackbird)` `loadk sensors` `. (sense2 'blackbird)` obviously we didn't actually define anything yet. We see that the Leonardo system's CLE provides its own notions of returns and error resolution. `put blackbird current-direction e` `put blackbird opening-angle 23` `put blackbird sensor-scale 1` `put blackbird sensor-weights <>` (I actually forgot how to do maps momentarily but w/e a sequence of sequences will do). `put snail y-position 0` `put snail x-position 0` `put coelacanth y-position 2` `put coelacanth x-position 1` ``` ses.146) put blackbird current-direction e put: blackbird current-direction e ses.147) . (sense2 'blackbird) coelacanth (ene 0 2 <= 1 < 3 & 1 <= 2 < 2) (nne 0 1 <= 1 < 2 & 2 <= 2 < 3) "success" snail (ene 0 2 <= 0 < 3 & 1 <= 0 < 2) (nne 2 1 <= 0 < 2 & 2 <= 0 < 3) (nnw 0 -1 <= 0 < 0 & 2 <= 0 < 3) (sse 0 1 <= 0 < 2 & -2 <= 0 < -1) (ssw 0 -1 <= 0 < 0 & -2 <= 0 < -1) (ene 0 2 <= 0 < 3 & 1 <= 0 < 2) (ese 0 2 <= 0 < 3 & -1 <= 0 < 0) (wnw 0 -2 <= 0 < -1 & 1 <= 0 < 2) (wsw 0 -2 <= 0 < -1 & -1 <= 0 < 0) nil ses.148) ``` With my verbose output, we can see that the coelacanth is discovered by a sensor, but the snail, sitting immediately beneath the blackbird, is not. It has occurred to me that a positive reading on a sensor facing `ssw` cannot on its own under our `sense2` choose between `s` and `sw`, since I sum the 2° reading into each adjacent 8-compass reading before `max`ing. So to turn left with the minimum, 23° opening angle would require a positive reading to the left, and a negative reading to the right, or a 68° opening angle with stronger positive readings both on the left to pick up more than one sensor in order to break the tie that happens. # Conclusions `sense` was a better lisp-side verb than `sense2` was, but on the whole `sense2` basically wholly defines a simulation game world reflecting Braitenberg's vehicles on its own. We have seen that we can put lisp definitions (i.e. of `type` `lispdef`) in our `entityfile`s, and use the `leodef` macro to make them available in *lisp* as helper functions. For all its faults, my triffid `sense2` definition seems to reflect Braitenberg's Vehicles quite well, and I am excited to explore its implications for our simulation game. # Fin. [Talk on the Mastodon thread as always please](https://gamerplus.org/@screwlisp/114854122212783389). I am very happy to hear from people who also started using the Leonardo calculus, and everyone else as well. You are most welcome to use and share this article and my articles where and how occurs to you.