screwlisp proposes kittens

cl-series ellipse sampling again (only read this one)

Eg.

Generating the image above. The article writes get-ellipse-hulls using the modern and historic common lisp cl-series macro package. This is my own native common lisp walkthrough attempt using series and is not a historic LISP ellipse/graphing demo. Source by itself in codeberg.org/tfw/screwlisps-knowledge.git.

(asdf:load-system :screwlisps-knowledge/ellipse-hulls)
(use-package :screwlisps-knowledge/ellipse-hulls)

(asdf:load-system :screwlisps-knowledge/simple-gnuplot)
(use-package :screwlisps-knowledge/simple-gnuplot)

(loop :repeat (1+ (random #o20))
      :for major-axis := (+ 2 (random 10))
      :for minor-axis := (1+ (random major-axis))
      :for rot := (* 2/360 pi (random 360))
      :for tx := (random 10)
      :for ty := (random 10)
      :for halves := (multiple-value-list
		      (get-ellipse-hulls
		       major-axis
		       minor-axis
		       rot
		       tx
		       ty))
      :nconcing halves :into result
      :finally (defparameter *all-halves*
		 result))
(apply 'gnuplot "all halves" *all-halves*)

Intro

At least as a point of comparison, let me retry just stating the points of an ellipse slightly better. And by better I mean more declaratively in terms of just cl-series.

I am going to use emacs eev eepitch in a slightly annoying way so that I can interleave out a single set of series declarations with dialogue about them in this kitten markdown page.

This document will also literally be my ~/common-lisp/screwlisps-knowledge/ellipse-hulls.lisp if tangled somehow to extract only the code fragments. I am still thinking about my compatibility to Eduardo’s eev online knowledge store.

My hope is that it can be very clear what I am trying to do and how I am doing it while also being the literal, tight, optimized common lisp code.

eepitch block

#|
 (eepitch-sbcl)
|#

Inferred package definition

(uiop:define-package :screwlisps-knowledge/ellipse-hulls
    (:import-from :series #:install)
  (:mix :series :asdf :cl)
  (:export #:get-ellipse-hulls))

(in-package :screwlisps-knowledge/ellipse-hulls)

Install series

Series is a macro package. What comes out of it is pure ansi common lisp. The macro can be added and removed from other packages after creating the native common lisp code.

(series::install)

Begin defining get-ellipse-hulls

(defun get-ellipse-hulls
    (major-axis-magnitude
     minor-axis-magnitude
     rotation-radians
     translation-x
     translation-y
     &optional
       (delta-degrees 1))

This is a totally normal lisp function definition of a function you would expect to describe an ellipse. The only oddity is that the function is going to return two lists of points, being the concave-down and concave-up portions of the rotated ellipse.

series::let* macro entrypoint

 (let* (

Remembering that all of cl-series happens at the macroexpansion step after reading, running install above clobbered cl:let* and similar with entry points to series’ macro time graph building and static analysis + code generation. From here on, you will notice series only constructs being used such as series.

scan-range of degrees sampling the ellipse

   (degrees
    (scan-range :below 360 :by delta-degrees)
    )
   (radians
    (#M* (series (* 2 pi 1/360))
	 degrees))

I guess you can interactively execute (<F8>) just the scan-range line to see it made #Z(0 1 2 ... 359). However, if the series was not finite, returning it is the same as collecting from an infinite loop. This is simply our sampling of the perimeter of the ellipse.

The degrees are obviously converted into radians. (series 'thing) creates an infinite series repeating 'thing. There are not normally constants per se.

The #M reader macro prefix turns a ‘normal function’ into a ‘series function’ by expanding into a mapping in series parlance.

xs and ys of points on the ellipse

    (x
     (#M* (series major-axis-magnitude)
	  (#Mcos radians)))
    (y
     (#M* (series minor-axis-magnitude)
	  (#Msin radians)))

Up til now, we have an unrotated ellipse centred on the (0 0) origin, and the angle in radians or degrees to each point.

Rotating the ellipse about the origin

   (rx
    (#M-
     (#M* x (#Mcos (series rotation-radians)))
     (#M* y (#Msin (series rotation-radians)))))
   (ry
    (#M+
     (#M* x (#Msin (series rotation-radians)))
     (#M* y (#Mcos (series rotation-radians)))))

In so many words.

Translating the ellipse

    (trx
     (#M+ rx (series translation-x)))
    (try
     (#M+ ry (series translation-y)))
    )

Up to here, we succeeded in sampling from, rotating and translating our ellipse in a single tight loop using cl-series.

Initially collecting the ellipse and its bounding box.

   (let ((x (collect trx))
	  (y (collect try))
	  (xmin (collect-min trx))
	  (xmax (collect-max trx))
	  (ymin (collect-min try))
	  (ymax (collect-max try))
	  (rads (#M+ (series rotation-radians)
		     radians)))
      (let* ((idx (search `(,xmin) x))
	     (len (length x))
	     (len/2 (truncate len 2))
	     (idx+len/2 (+ idx len/2)))
	
	(cond
	  ((< idx len/2)
	   (values
	    (mapcar
	     'list
	     (subseq x idx idx+len/2)
	     (subseq y idx idx+len/2))
	    (mapcar
	     'list
	     (append
	      (subseq x idx+len/2)
	      (subseq x 0 idx))
	     (append
	      (subseq y idx+len/2)
	      (subseq y 0 idx)))))
	  (:otherwise
	    (values
	     (mapcar
	      'list
	      (append
	       (subseq x idx)
	       (subseq x 0 (- idx+len/2 len)))
	      (append
	       (subseq y idx)
	       (subseq y 0 (- idx+len/2 len))))
	     (mapcar
	      'list
	      (subseq x (- idx+len/2 len) idx)
	      (subseq y (- idx+len/2 len) idx)))))))))

Altogether now.

(defun get-ellipse-hulls
    (major-axis-magnitude
     minor-axis-magnitude
     rotation-radians
     translation-x
     translation-y
     &optional
       (delta-degrees 1))
  (let* (
	 (degrees
	   (scan-range :below 360 :by delta-degrees)
	   )
	 (radians
	   (#M* (series (* 2 pi (/ 360)))
		degrees))
	 (x
	   (#M* (series major-axis-magnitude)
		(#Mcos radians)))
	 (y
	   (#M* (series minor-axis-magnitude)
		(#Msin radians)))
	 (rx
	   (#M-
	    (#M* x (#Mcos (series rotation-radians)))
	    (#M* y (#Msin (series rotation-radians)))))
	 (ry
	   (#M+
	    (#M* x (#Msin (series rotation-radians)))
	    (#M* y (#Mcos (series rotation-radians)))))
	 (trx
	   (#M+ rx (series translation-x)))
	 (try
	   (#M+ ry (series translation-y)))
	 )
    (let ((x (collect trx))
	  (y (collect try))
	  (xmin (collect-min trx))
	  (xmax (collect-max trx))
	  (ymin (collect-min try))
	  (ymax (collect-max try))
	  (rads (#M+ (series rotation-radians)
		     radians)))
      (let* ((idx (search `(,xmin) x))
	     (len (length x))
	     (len/2 (truncate len 2))
	     (idx+len/2 (+ idx len/2)))
	
	(cond
	  ((< idx len/2)
	   (values
	    (mapcar
	     'list
	     (subseq x idx idx+len/2)
	     (subseq y idx idx+len/2))
	    (mapcar
	     'list
	     (append
	      (subseq x idx+len/2)
	      (subseq x 0 idx))
	     (append
	      (subseq y idx+len/2)
	      (subseq y 0 idx)))))
	  (:otherwise
	    (values
	     (mapcar
	      'list
	      (append
	       (subseq x idx)
	       (subseq x 0 (- idx+len/2 len)))
	      (append
	       (subseq y idx)
	       (subseq y 0 (- idx+len/2 len))))
	     (mapcar
	      'list
	      (subseq x (- idx+len/2 len) idx)
	      (subseq y (- idx+len/2 len) idx)))))))))

Feedback about what I did here on the Mastodon please

I would really like commentary and advice on the above. From what I can tell right now, what I wrote seems declarative and fairly sensible if verbose, though I can feel that it is really quite a bit too long.

I did try to do a good job here (the one yesterday was me coming down with a cold, is my excuse for it). However, I take the point

This should have been 6 lines of code in either python or lisp

seriously. Allowing that I am not trying to show I know Euler’s formula or about cool symmetries or integration packages here, what do you think, can you show me where I went off on a weird tangent, possibly with a counterexample or reference?

screwlisp proposes kittens