Atlas of Space

There’s a lot going on in our Solar System — so I built a digital orrery to explore it: atlasof.space!

This was a fun side project over Winter Break 2024 to learn orbital mechanics, browser animation, serverless deployment options, and of course facts about moons, asteroids, and comets. Building for yourself is a treat that I haven’t properly enjoyed in some time. Source code is available on GitHub at @gordonhart/atlasof.space.

Shared it on Hacker News on 1/8 and I was blown away by the positive feedback. Some stats:

  1. Visitors: ~36,000
  2. Bandwidth used: 361 GB
  3. Positive comments: 100+
  4. Social shares: 50+ across X, Bluesky, Mastodon
  5. Bill from Netlify: $180 (mostly bandwidth)

Shout-outs to a few enabling technologies/resources:

  • JPL for its Small-Body Database
  • Wikidata for its excellent query service
  • SolarSystemScope for planet, moon, star textures
  • Three.js for being an incredible library — performant, featureful, and straightforward
  • OpenAI and Anthropic LLMs for 10x’ing the process of learning orbital mechanics and Three.js

Click around and let me know what you learned, or what improvements you’d like to see.

Variable Arity, or: Fun with the OCaml Type System

Sometimes the best feature in ML—the type system—is precisely the thing standing between you and what you need to get done. In this article we’ll dive into the world of discrete math to understand how to achieve a function of variable arity in OCaml, allowing for the encoding of, say, a logical proposition with any number of terms.

The Math

Consider a problem set that asks you to evaluate the truth of the following:

Screen Shot 2017-06-21 at 1.57.05 PM

Applying a quick translation from set notation to formal logic notation:

  • Union: A ∪ B becomes A ∨ B
  • Set minus: A \ B becomes A ∧ ¬B
  • Oplus: A ⊕ B stays as is, XOR
  • Subset equal: A ⊆ B becomes A → B

And an encoding of these rules into OCaml:

let (!-) a = not a;; (* symbolic not *)
let (--) a b = a && (!- b);; (* logical minus *)
let (<+>) a b = (a || b) && (!- (a && b));; (* xor (oplus) *)
let (-->) a b = (a && b) || (!- a);; (* single implication *)

We can determine the truth of the posited logical expression by determining if the implication stated is or is not a tautology.

(b -- (a <+> c)) --> ((c || b) -- (c -- a))

We now have a logical representation of the given set expression that we can query within the OCaml toplevel!

To declare an expression a tautology, we must observe that every combination of terms yields a valid expression. In this case that ain’t so bad, there are only 23 possible combinations. But what if your expression has 5 terms, or 7? Manual enumeration of these truth tables would be excruciating.

The Type System

Wouldn’t it be nice if there were a function that could spit out the truth table of a given logical expression? The naive approach to this ends with fun messages like this one:

Error: This expression has type bool -> bool -> bool -> bool
       but an expression was expected of type bool -> bool -> bool
       Type bool -> bool is not compatible with type bool

When you try to provide an expression with three terms instead of two. Does your head hurt yet? Mine does!

In a nutshell, this is the problem of variable arity in a language with a strict type system like OCaml. In the truth table function, I want to be able to evaluate a boolean expression of arbitrary terms, but the type system wants me to explicitly state from the outset how many terms it can expect.

Let’s do just that: define a standard type that won’t upset the type inference machine while still allowing an arbitrary number of terms for our logical expression. Behold the mighty power of the recursive enum:

type 'a parfun =
  | Par of ('a -> 'a parfun)
  | Expr of 'a;;

What are we looking at here? We have the type 'a parfun that allows us to define an Expr that takes an arbitrary number of arguments of type 'a and evaluates to some 'a—exactly what we need to define our logical expressions.

There’s a bit of boilerplate involved, but our expression defined in the previous section looks like this as a bool parfun:

let our_expr = Par (fun a -> Par (fun b -> Par (fun c -> Expr (
  (b -- (a <+> c)) --> ((c || b) -- (c -- a))
))));;

It’s hideous. It’s beautiful. It’s a bool parfun.

We need a special way to evaluate our parfun that doesn’t upset the sleeping beast past which we just snuck. To do this, let’s provide our arguments to the nested Expr as a list.

(* evaluate a parfun f with the given var list v *)
let eval_parfun f vars =
  List.fold_left (fun facc v ->
    match facc with
    | Par pf -> pf v
    | Expr _ -> failwith "dimension mismatch: |parfun| < |vars|"
  ) f vars
  |>  function
      | Expr result -> result
      | Par _ -> failwith "dimension mismatch: |parfun| > |vars|";;

We can now evaluate our_expr:

> eval_parfun our_expr [true; false; false];;
- : bool = true

Back to the Math

The final steps in this example are to define the truth table function. For this we need to be able to determine the arity of the Expr buried beneath all of those Pars.

let rec arity ~atom:a = function
  | Expr _ -> 0
  | Par p -> 1 + (arity ~atom:a (p a));;

let arity_of_bool_parfun = arity ~atom:true;;

Note that we have to provide a hint as to what our type variable 'a is to be able to unravel our 'a parfun.

Next up we generate the values for the terms in every row of the truth table:

let combinations n =
  let rec comb_loop acc i =
    if i = 0 then acc
    else comb_loop ((List.map (fun l -> true :: l) acc)
         @ (List.map (fun l -> false :: l) acc)) (i-1) in
  comb_loop [[true];[false]] (n-1);;

Then defining the truth table function:

let truthtable expr =
  let open List in
  let b2i b = if b then 1 else 0 in
  let nvars = arity_of_bool_parfun expr in
  iter (printf "v%d\t") (1|..|nvars);
  printf "expr\n";
  iter (fun c ->
    iter (fun b -> printf "%d\t" (b2i b)) c;
    printf "%d\n" (b2i (eval_parfun expr c))
  ) (combinations nvars);;

To yield:

> truthtable our_expr;;
v1      v2      v3      expr
1       1       1       1
1       1       0       1
1       0       1       1
1       0       0       1
0       1       1       1
0       1       0       1
0       0       1       1
0       0       0       1
- : unit = ()

It’s a tautology!

Wrapping Up

Now we can generate all the truth tables our hearts desire without leaving the padded box of utop. It’s a tad verbose, but without macro capabilities there’s not much that can be done about that within the confines of the type system. Never change, OCaml.

If you want to fiddle around with it yourself, you can find the code here.

If you liked the article, found something I overlooked, came up with a novel application for your parfun, or want to discuss further, let me know in the comments!

Mortality in the United States: Past and Present

What do we really know about how the average American dies? The presentation in this post will hopefully illuminate some of the darker corners of that question through the analysis and visualization of over 100 million deaths since 1968 in the United States.

Check it out and see what you can find. Navigate with to move forward and to move back a slide (disclaimer: if you don’t use these keys certain slides won’t display properly). From wars to advances in medicine to changes in diet and exercise habits, the story of a nation’s health is told here through a lens of ash, dirt, and satin.

QUICK: Eyesight Saver

The Problem

Hour after hour, day after day staring at the same set of glowing rectangles takes a toll on your long-distance eyesight.

Most optometrists recommend taking a 15-30 second break to focus into the distance every 15 minutes or so to keep your eyes from “locking” into nearsighted mode.

Easy enough to follow, eh? Not really—we all know how easy it is to get lost in a project and pass hours without consideration of the outside world.

The Solution

Something needed to be done to remind me to look up and out every once in a while. I figured there had to exist an OS X alert command-line API.

Enter terminal-notifier: a simple CLI written in Ruby for triggering alarms. Plug this into a cron job, et voila, we have our solution. A couple of lines bugging you every 15 minutes is all you need.

The full implementation is dead-easy. Step by step:

  1. Install terminal-notifier:
    sudo gem install terminal-notifier
  2. Setup the notification script:
    vi ~/.my_assets/lookaway.sh

    I keep a hidden folder in my home directory containing various assets that I refer to globally. Inside this file:

    /usr/local/bin/terminal-notifier \
     -title "Protect Your Eyes" \
     -message "Look into the distance for 15 seconds" \
     -timeout 15 \
     -actions "dismiss"
    
    # allowed to look back again (15 second previous message timed out)
    /usr/local/bin/terminal-notifier \
     -title "15 Seconds Have Passed" \
     -message "OK to look at the screen again" \
     -timeout 15 \
     -actions "dismiss"

    Note: AppleScript can also create system alerts, but you don’t have as much control over them as with terminal-notifier, so I opted for the third-party solution. AppleScript goes as such:

    # using native applescript
    osascript -e \
      'display notification "Look into the distance for 15 seconds" with title "Protect Your Eyes"'
  3. Add a trigger cron job to run every 15 minutes:
    [EDITOR=/usr/bin/vim] crontab -e

    Insert a new line:

    0,15,30,45 * * * * /bin/bash /Users/gordonhart/.my_assets/lookaway.sh >/dev/null 2>&1

    The format for crontab entries is as follows:

    * * * * * <command>
    | | | | |
    | | | | +-- Day of the Week   (range: 1-7, 1 standing for Monday)
    | | | +---- Month of the Year (range: 1-12)
    | | +------ Day of the Month  (range: 1-31)
    | +-------- Hour              (range: 0-23)
    +---------- Minute            (range: 0-59)

    (source) where comma-separated lists representing multiple values for a single field.

    In the crontab entry you’ll also notice that the stdout and stderr output streams from running this shell script are redirected to /dev/null. This is done to suppress the /var/mail clutter that would otherwise accumulate every 15 minutes.

That’s it! Simple stuff can go a long way.

BUILD: Planar Tripod

 

 

This slideshow requires JavaScript.

For my last post I included a demo of the CNC Typewriter in action. In order to film this, I realized I needed a second pair of hands around. Rather than ask a roommate to hold the camera, I figured I’d have a little fun designing a piece—and so this tripod was born.

Continue reading BUILD: Planar Tripod