← Back to Kevin's newslettersPublished: 2025 April 28

Towards the cutest neural network

I recently needed to use a microcontroller to estimate the pose (translation and orientation) of an object using readings from six different sensors. Since the readings were non-linear and coupled with each other, an explicit analytical solution was out of the question.

I figured I’d have a go at using a simple neural network to approximate it:

  1. Generate training data (on my computer) using a forward simulation (pose to sensor readings)
  2. Train a lil’ neural network (a few dense layers, 100’s of parameters, tops) to approximate the inverse mapping function (sensor readings to pose)
  3. Deploy this network on my microcontroller (Cortex-M0, 16 kB RAM, 32 kB flash) to actually do the inference

Since neural networks have been around since the 1980’s, I figured it’d be straightforward. A quick background search uncovered lots of promising leads too, especially regarding “quantization”, which I wanted to do as my microcontroller doesn’t have hardware support for floating point operations.

However, this turned out to be much more difficult than I’d anticipated. It seems like my use case — end-to-end training of a simple dense neural network with integer-only inference — is quite uncommon.

The vast majority of papers and software libraries I found turned out to be complex, heavyweight (in terms of inference code size), and have lots of unstated assumptions and background requirements.

To make a web design analogy: It felt like I kept falling into npm create-react-app rabbit holes rather than what I wanted: The moral equivalent of opening index.html with notepad.exe and typing <h1>Welcome to my Homepage</h1>.

I’m writing up my notes to:

  1. checkpoint my understanding of the space and current best solution
  2. help anyone else who simply wants a wholesome lil’ universal function approximator with low conceptual and hardware overhead
  3. solicit “why don’t you just …” emails from experienced practitioners who can point me to the library/tutorial I’ve been missing =D

Mathematics won’t render properly in an email, so if you’re interested you’ll have to click over to my website to see my plan for making the cutest neural network.

Dubious ideas for a code CAD language

Back in 2022 I explored a pen/gesture-based user interface for 2D drawing with constraint solvers and recently I decided to make a more general 3D, BRep-based system.

I’d ultimately like something graphical, but since I’ll be building the system with textual code, I’ll need some textual notation to use for debugging, recording test cases, etc. I don’t want to spend the next few months/years reading screenfuls of EDN, JSON, or (have mercy) that incomplete, unparsable stuff Rust’s Debug prints.

So that’s how I ended up starting to design a programming language. After all, I’ve got to come up with semantics for my system anyway, and if I’ve got to do that and have to come up with a concise notation, I might as well put them together into a language, right?

The closest prior art I’m aware of is Build123d, a Python library that provides “builder-style” and “algebra-style” APIs to the OpenCascade BRep kernel.

The primary differentiators I’ll be focusing on are:

For the language itself, I’m leaning towards Julia-style multiple dispatch based on types, with optional argument names for disambiguation.

My hope is that this mechanism will allow for concise specifications. For example, a cylinder could be specified with either a diameter or a radius:

(defn cylinder
  ([diameter: Length
    height:   Length]
   (extrude height (sketch (circle diameter))))

  ([radius: Length
    height: Length]
   (cylinder :diameter (* radius 2)
             height)))

Lots of details to sort out here. For example, both of these methods have the same type arity (length, length), so for an invocation like (cylinder 1mm 2mm), should I:

Multiple dispatch could also be used to simplify the “happy path” by having some methods define necessary conversions. E.g., a solid extrusion requires a closed profile, but in the general case a sketch may not have exactly one closed profile. So the extrude invoked by the first cylinder method above would only make sense if extrude was defined something like:

(defn extrude
   ([distance: Length
     sketch:   Sketch]
    (extrude distance (get-unique-closed-profile-or-throw sketch)))

  ([distance: Length
    profile:  Profile]
   ...))

That said, I haven’t implemented a language with multiple dispatch and type inference, and I expect there are tons of tricky interactions — if you have any recommendations, advice, or just want to get into trouble with me on this, please drop me a line!

Follow up: graph-directed autocomplete

Last newsletter I was wondering about graph-directed autocomplete as a way to have the computer “do what I mean” and figure out how to compose functions to map provided arguments to a desired type.

My motivating example was “figure out how to make an axis from a circular face”, and I’m happy to report that such a system can be implemented in a few lines of Clojure:

(def fns
  "A list of [function-name arg-types return-type]"
  [["plane, three point"                  [:point :point :point] :plane]
   ["axis, normal to plane through point" [:plane :point]        :axis]
   ["point, edge start"                   [:edge]                :point]
   ["point, edge mid"                     [:edge]                :point]
   ["point, edge end"                     [:edge]                :point]
   ["plane, containing edge"              [:edge]                :point]
   ["point, arc centerpoint"              [:arc]                 :point]
   ["point, circle centerpoint"           [:circle]              :point]
   ["edge, from points"                   [:point :point]        :edge]
   ["plane, circle"                       [:circle]              :plane]
   ["plane, arc"                          [:arc]                 :plane]])


(def all-types
  (->> (flatten fns) (filter keyword?) set))


(def all-fns
  (concat fns
          ;; add fns which construct a type from id
          (for [t all-types]
            [(name t) [:id] t])))


(def fn-name->type
  (into {} (for [[name _ return-type] all-fns]
             [name return-type])))


(defn possible-arglists
  "For a given function signature and possible expressions indexed by type, find all possible args"
  [signature type->exprs]
  (->> (apply clojure.math.combinatorics/cartesian-product (map type->exprs signature))
       ;;don't allow an expression to be used multiple times
       (filter (fn [args] (= (count args) (count (set args)))))))


(defn expand
  "expand set of exprs by applying all possible fns with all possible permutations of args"
  [exprs]
  (let [type->exprs (->> exprs
                         (group-by (comp fn-name->type first)))]

    (into exprs
          (for [[name signature _] all-fns
                args (possible-arglists signature type->exprs)]
            (into [name] args)))))

(comment
  ;; what can you do with two points?
  (expand #{["point" 1] ["point" 2]})
  ;; => #{["point" 1]
  ;;      ["point" 2]
  ;;      ["edge, from points" ["point" 1] ["point" 2]]
  ;;      ["edge, from points" ["point" 2] ["point" 1]]}
  ;; leave them as is or make two different directed edges
  )


(defn autocomplete
  ([exprs target-type]
   (autocomplete exprs target-type 3))

  ([exprs target-type max-iterations]
   (->> (iterate expand exprs)
        (take max-iterations)
        last
        (filter #(= target-type (fn-name->type (first %)))))))

(comment
  ;; the motivating problem: how can you get an arc from an axis?
  (autocomplete #{["arc" 1]} :axis)
  ;; => (["axis, normal to plane through point"
  ;;      ["plane, arc" ["arc" 1]]
  ;;      ["point, arc centerpoint" ["arc" 1]]])
  ;; just one solution: use the arc to get a plane and a point, then use those to get an axis.
  )

We all know computers can search graphs like this, but it’s still fun to actually write it out.

The next step would be to extend this with:

Also: shout out to newsletter reader Xavier who suggested I look at OnShape’s Mate Connector concept as another way to solve this selection problem. My programmer interpretation of mate connectors is that they’re reified interfaces for parts, such that rather than relating specific geometry (faces, edges, etc.) between parts, you define and relate mate connectors so that the relationship is robust to later changes to the underlying “implementation” (geometry/topology) of the parts.

Misc stuff.

Several people wrote after my last newsletter to ask where I find so many articles and esoteric YouTube videos. Well, over the winter I moved my 3x weekly 90 minute cardio routine onto a stationary bike in a gym.

The gym has wifi.