screwlisp proposes kittens

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 and this one filling in some organism ontology 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 (my openaccess academic bibliography here).

I am choosing to use the powerful macro package cl-series 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.

Setup

Same setup as always (refer first article). The attribute definitions being referred to are from this second article.

The way this and other articles work for me is Eduardo’s eev eepitching 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 <sensors sense>]
[: 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 <sensors sense>]
[: 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 <elements of the sequence> ⇔ 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 loopy 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 <sensors sense sense2>]
[: 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 nil (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 score) := score
		 :then
		 (if (> nscore score)
		     (values ndir nscore)
		     (values dir score))
	       :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))
	   (return))
     :finally (return
	       (values
		(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 instead of cl-series here.

I spent quite a lot of time inside sldb 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)
  2. Tallies organisms by their 2° intercardinal sensor direction, with some sensor characteristic scale
  3. Zeroes sensor readings outside the viewer’s opening angle in front of it
  4. 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, 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 <<organism 3>>

(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 maxing. 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 entityfiles, 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. 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.

screwlisp proposes kittens