Peter Kagey in a Rhombic Dodecahedron made from Truncated Octahedra

Hello! I’m Peter!


Blog


  • A π-estimating Twitter bot: Part I

    This is the first part of a three part series about making the Twitter bot @BotfonsNeedles. In this part, I will write a Python 3 program that

    In the second part, I’ll explain how to use the Twitter API to

    • post the images to Twitter via the Python library Tweepy, and
    • keep track of all of the Tweets to get an increasingly accurate estimate of \(\pi\).

    In the third part, I’ll explain how to

    • Package all of this code up into a Docker container
    • Push the Docker image to Amazon Web Services (AWS)
    • Set up a function on AWS Lambda to run the code on a timer
    An example of dropping 100 needles in Buffon’s needle problem. Needles that cross a vertical line are colored red.

    Buffon’s needle problem

    Buffon’s needle problem is a surprising way of computing \(\pi\). It says that if you throw \(n\) needles of length \(\ell\) randomly onto a floor that has parallel lines that are a distance of \(\ell\) apart, then the expected number of needles that cross a line is \(\frac{2n}\pi\). Therefore one way to approximate (\pi) is to divide \(2n\) by the number of needles that cross a line.

    I had my computer simulate 400 needle tosses, and 258 of them crossed a line. Thus this experiment approximates \(\pi \approx 2\!\left(\frac{400}{258}\right) \approx 3.101\), about a 1.3% error from the true value.

    63/100 needles cross a vertical line; approximates \(\pi \approx 200/63 \approx 3.174\).
    61/100 needles cross a vertical line; approximates \(\pi \approx 200/61 \approx 3.279\).
    68/100 needles cross a vertical line; approximates \(\pi \approx 200/68 \approx 2.941\).
    66/100 needles cross a vertical line; approximates \(\pi \approx 200/66 \approx 3.030\).

    Modeling in Python

    Our goal is to write a Python program that can simulate tossing needles on the floor both numerically (e.g. “258 of 400 needles crossed a line”) and graphically (i.e. creates the PNG images like in the above example).

    The RandomNeedle class.

    We’ll start by defining a RandomNeedle class which takes

    • a canvas_width, \(w\);
    • a canvas_height, \(h\);
    • and a line_spacing, \(\ell\).

    It then initializes by choosing a random angle (\theta \in [0,\pi]) and random placement for the center of the needle in \[(x,y) \in \left[\frac{\ell}{2}, w -\,\frac{\ell}{2}\right] \times \left[\frac{\ell}{2}, h -\,\frac{\ell}{2}\right]\] in order to avoid issues with boundary conditions.

    Next, it uses the angle and some plane geometry to compute the endpoints of the needle: \[\begin{bmatrix}x\\y\end{bmatrix} \pm \frac{\ell}{2}\begin{bmatrix}\cos(\theta)\\ \sin(\theta)\end{bmatrix}.\]

    The class’s first method is crosses_line, which checks to see that the \(x\)-values at either end of the needle are in different “sections”. Since we know that the parallel lines occur at all multiples of \(\ell\), we can just check that \[\left\lfloor\frac{x_\text{start}}{\ell}\right\rfloor \neq \left\lfloor\frac{x_\text{end}}{\ell}\right\rfloor.\]

    The class’s second method is draw which takes a drawing_context via Pillow and simply draws a line.

    import math
    import random
    
    class RandomNeedle:
      def __init__(self, canvas_width, canvas_height, line_spacing):
        theta = random.random()*math.pi
        half_needle = line_spacing//2
        self.x = random.randint(half_needle, canvas_width-half_needle)
        self.y = random.randint(half_needle, canvas_height-half_needle)
        self.del_x = half_needle * math.cos(theta)
        self.del_y = half_needle * math.sin(theta)
        self.spacing = line_spacing
    
      def crosses_line(self):
        initial_sector = (self.x - self.del_x)//self.spacing
        terminal_sector = (self.x + self.del_x)//self.spacing
        return abs(initial_sector - terminal_sector) == 1
    
      def draw(self, drawing_context):
        color = "red" if self.crosses_line() else "grey"
        initial_point  = (self.x-self.del_x, self.y-self.del_y)
        terminal_point = (self.x+self.del_x, self.y+self.del_y)
        drawing_context.line([initial_point, terminal_point], color, 10)
    

    By generating \(100\,000\) instances of the RandomNeedle class, and keeping a running estimation of (\pi) based on what percentage of the needles cross the line, you get a plot like the following:

    This estimates \(\pi\approx 2\left(\frac{10000}{63681}\right) \approx 3.1407\) an error of 0.03%.

    The NeedleDrawer class

    The NeedleDrawer class is all about running these simulations and drawing pictures of them. In order to draw the images, we use the Python library Pillow which I installed by running

    pip3 install Pillow

    When an instance of the NeedleDrawer class is initialized, makes a “floor” and “tosses” 100 needles (by creating 100 instances of the RandomNeedle class).

    The main function in this class is draw_image, which makes a \(4096 \times 2048\) pixel canvas, draws the vertical lines, then draws each of the RandomNeedle instances.

    (It saves the files to the /tmp directory in root because that’s the only place we can write file to our Docker instance on AWS Lambda, which will be a step in part 2 of this series.)

    from PIL import Image, ImageDraw
    from random_needle import RandomNeedle
    
    class NeedleDrawer:
      def __init__(self):
        self.width   = 4096
        self.height  = 2048
        self.spacing = 256
        self.random_needles = self.toss_needles(100)
    
      def draw_vertical_lines(self):
        for x in range(self.spacing, self.width, self.spacing):
          self.drawing_context.line([(x,0),(x,self.height)],width=10, fill="black")
    
      def toss_needles(self, count):
        return [RandomNeedle(self.width, self.height, self.spacing) for _ in range(count)]
     
      def draw_needles(self):
        for needle in self.random_needles:
          needle.draw(self.drawing_context)
    
      def count_needles(self):
        cross_count = sum(1 for n in self.random_needles if n.crosses_line())
        return (cross_count, len(self.random_needles))
    
      def draw_image(self):
        img = Image.new("RGB", (self.width, self.height), (255,255,255))
        self.drawing_context = ImageDraw.Draw(img)
        self.draw_vertical_lines()
        self.draw_needles()
        del self.drawing_context
        img.save("/tmp/needle_drop.png")
        return self.count_needles()
    

    Next Steps

    In the next part of this series, we’re going to add a new class that uses the Twitter API to post needle-drop experiments to Twitter. In the final part of the series, we’ll wire this up to AWS Lambda to post to Twitter on a timer.

  • Polytopes with Lattice Coordinates

    Problems 21, 66, and 116 in my Open Problem Collection concern polytopes with lattice coordinates—that is, polygons, polyhedra, or higher-dimensional analogs with vertices the square or triangular grids. (In higher dimensions, I’m most interested in the \(n\)-dimensional integer lattice and the \(n\)-simplex honeycomb).

    The \(A334581(4) = 84\) ways to place an equilateral triangle on the tetrahedral grid with four points per side.
    Illustration showing three of the \(\binom{7+2}{4} = 126\) equilateral triangles on a grid with seven points per side.

    This was largely inspired by one of my favorite mathematical facts: given a triangular grid with \(n\) points per side, you can find exactly \(\binom{n+2}{4}\) equilateral triangles with vertices on the grid. However, it turns out that there isn’t a similarly nice polynomial description of tetrahedra in a tetrahedron or of triangles in a tetrahedron. (Thanks to Anders Kaseorg for his Rust program that computed the number of triangles in all tetrahedra with 1000 or fewer points per side.)

    The \(4\)-simplex (the \(4\)-dimensional analog of a triangle or tetrahedron) with \(n-1\) points per side, has a total of \(\binom{n+2}{4}\) points, so there is some correspondence between points in some \(4\)-dimensional polytope, and triangles in the triangular grid. This extends to other analogs of this problem: the number of squares in the square grid is the same as the number of points in a \(4\)-dimensional pyramid.

    The \(\binom{n+2}{4}\) equilateral triangles

    I put a Javascript applet on my webpage that illustrates a bijection between size-\(4\) subsets of \(n+2\) objects and triangles in the \(n\)-points-per-side grid. You can choose different subsets and see the resulting triangles. (The applet does not work on mobile.)

    The solid blue triangle corresponding to the subset \(\{4,5,8,15\} \subseteq \{1,2,\dots,16\}\).
    The two smaller numbers in the subset give the size and orientation of the blue triangle, and the two larger numbers give the position.

    Polygons with vertices in \(\mathbb{Z}^n\)

    This was also inspired by Mathologer video “What does this prove? Some of the most gorgeous visual ‘shrink’ proofs ever invented”, where Burkard Polster visually illustrates that the only regular polygons with vertices in \(\mathbb{Z}^n\) (and thus the \(n\)-simplex honeycomb) are equilateral triangles, squares, and regular hexagons.

    Polyhedra with vertices in \(\mathbb{Z}^3\)

    There are some surprising examples of polyhedra in the grid, including cubes with no faces parallel to the \(xy\)-, \(xz\)-, or \(yz\)-planes.

    An example of a cube from Ionascu and Obando: the convex hull of \(\{(0,3,2),(1,1,4),(2,2,0),(2,5,3),(3,0,2),(3,3,5),(4,4,1),(5,2,3)\}\)

    While there are lots of polytopes that can be written with vertices in \(\mathbb{Z}^3\), Alaska resident and friend RavenclawPrefect cleverly uses Legendre’s three-square theorem to prove that there’s no way to write the uniform triangular prism this way! However, he provides a cute embedding in \(\mathbb{Z}^5\): the convex hull of $$\scriptsize{\{(0,0,1,0,0),(0,1,0,0,0),(1,0,0,0,0),(0,0,1,1,1),(0,1,0,1,1),(1,0,0,1,1)}\}.$$

    Polygons on a “centered \(n\)-gon”

    I asked a question on Math Stack Exchange, “When is it possible to find a regular \(k\)-gon in a centered \(n\)-gon“—where “centered \(n\)-gon” refers to the diagram that you get when illustrating central polygonal numbers. These diagrams are one of many possible generalizations of the triangular, square, and centered hexagonal grids. (Although it’s worth noting that the centered triangular grid is different from the ordinary triangular grid.)

    If you have any ideas about this, let me know on Twitter or post an answer to the Stack Exchange question above.

    A catalog of polytopes and grids

    On my OEIS wiki page, I’ve created some tables that show different kinds of polytopes in different kinds of grids. There are quite a number of combinations of polygons/polyhedra and grids that either don’t have an OEIS sequence or that I have been unable to find.

      Square Rectangular Centered Square Triangular Centered Hexagonal
    Equilateral Triangle A000332 A008893
    Square A002415 A130684 A006324
    Regular Hexagon A011779 A000537
    Regular Polygon A002415 A130684 A006324  ? A339483*
    Triangle A045996 A334705  ?  ? A241223
    Rectangle A085582 A289832  ?
    Right Triangle A077435  ?  ?  ? A241225
    OEIS sequences for polygons on 2-dimensional grids.
    Sequences marked with “*” are ones that I’ve authored, cells marked with “—” have no polygons, and cells marked with “?” do not have a corresponding sequence that I know of.
    CubicTetrahedralOctahedral
    Equilateral TriangleA102698A334581* A342353*
    SquareA334881*A334891* ?
    Regular HexagonA338322* ? ?
    Regular PolygonA338323* ? ?
    Triangle ? ? ?
    Rectangle ? ? ?
    Right Triangle ? ? ?
    Regular TetrahedronA103158A269747 ?
    CubeA098928 ? ?
    OctahedronA178797 ? ?
    Platonic SolidA338791 ? ?
    OEIS sequences for polytopes on 3-dimensional grids.
    Sequences marked with “*” are ones that I’ve authored, and cells marked with “?” do not have a corresponding sequence that I know of.

    If you’re interested in working on filling in some of the gaps in this table, I’d love it if you let me now! And if you’d like to collaborate or could use help getting started, send me a message on Twitter!

  • Parity Bitmaps from the OEIS

    My friend Alec Jones and I wrote a Python script that takes a two-dimensional sequence in the On-Line Encyclopedia of Integer Sequences and uses it to create a one-bit-per-pixel (1BPP) “parity bitmaps“. The program is simple: it colors a given pixel is black or white depending on whether the corresponding value is even or odd.

    A048152 Parity Bitmap
    A048152 parity bitmap, rescaled through Lospec’s Pixel Art Scaler.
    A207409 parity bitmap
    A207409 parity bitmap.

    An Unexpected Fractal

    We’ve now run the script on over a thousand sequences, but we still both agree on our favorite: the fractal generated by OEIS sequence A279212.

    Fill an array by antidiagonals upwards; in the top left cell enter \(a(0)=1\); thereafter, in the \(n\)-th cell, enter the sum of the entries of those earlier cells that can be “seen” from that cell.

    Notice that in the images below, increasing the rows and columns by a factor of \(2^n\) seems to increase the “resolution”, because the parity bitmap is self similar at 2x the scale. We still don’t have a good explanation for why we’d expect these images are fractals. If you know, please answer our question about it on Math Stack Exchange. (Alec and I have generated these images up to 16384 × 32768 resolution, roughly 536 megapixels.)

    A279212 parity bitmap
    512 rows and 256 columns of A279212.
    A279212 parity bitmap
    2048 rows and 1024 columns of A279212.

    The Construction of the Sequence

    The sequence is built up by “antidiagonals”, as shown in the GIF below. In the definition, “seen” means every direction a chess queen can move that already has numbers written down (i.e. north, west, northwest, or southwest). That is, look at all of the positions you can move to, add them all up, write that number in your square, move to the next square, and repeat. (The number in cell \(C\) also counts the number of paths a queen can make from \(C\) to the northwest corner using only N, NW, W, SW moves.)

    Animation of A279212 construction

    (Interestingly, but only tangentially related: Code Golf Stack Exchange User flawr noticed that the number of north/west rook walks is related to the number of ways of partitioning a \(1 \times n\) grid into triangles.)

    Parity Bitmaps for Other Sequences

    It’s worth noting that many sequences are all black, consist of simple repeating patterns, or look like static. However, chess-type constructions, as illustrated by the GIF above, the one above yield images that look like the Sierpiński triangle. (See A132439 and A334017 below, and look at A334016 and A334745 via their OEIS entries.) Look below for a couple other sequences with interesting images too.

    A132439 parity bitmap.
    A301851 parity bitmap.
    A334017 parity bitmap.
    A237620 parity bitmap.

    I ordered a poster-sized print of the A279212 fractal for Alec, and he framed it in his office.

    Alec’s fractal, framed above his fountain pen ink and tasteful books.

    Some ideas for further exploration: