My sister recently commisioned me to make a 3D printed model of a mathematical shape. This shape is from Alex McDonough’s research paper: “A Combinatorial Mapping For The Higher-Dimensional Matrix-Tree Theorem”.


Identifying the Challenge

In the past when I have been asked to print 3D parts, the requests have come from engineers and technologists with some experience using 3D CAD software such as Autodesk Inventor, Solidworks, FreeCAD, etc. These requests are usually made with them providing me an STL file of their model. I can simply import this STL file into my slicer program, generate the code for the 3D printer, and start printing the part.

My sister who studies math, comes from a very different educational background, and as a result does not have experience using CAD software to provide me with an STL file. This led to challenges when she gave me the input information for the model she wanted me to fabricate. The model is composed of three parallelepipeds, one large one in the back, a smaller one in the front, and a small cube on top.

The figure used in Alex's research paper.[1]
The figure used in Alex's research paper.

Alex McDonough made the animation below in Blender. It is showing how the shape can be used to tile space.

The figure tiling space. Animation by Alex McDonough.
The figure tiling space. Animation by Alex McDonough.

My sister gave me the figure from above with all of the points labeled with their x, y, z coordinates as shown in the image below.

The figure with its points labeled as x, y, z coordinates
The figure with its points labeled as x, y, z coordinates

While this seems simple enough, I quickly realized that I had never created a model in 3D CAD from coordinates alone. Drawings in 3D CAD software are typically made using dimensions rather than coordinates. I knew that it wouldn’t be very difficult or time consuming to derive the dimensions of the shape from the input data, but I knew that there must be a way to generate the STL file from the coordinates alone through code. If mathematicians can incorporate STL file generation algorithmically through code in their workflow, it would allow for them to much more easily get figures from their work 3D printed. Sure enough, after a quick google search, I found a library called numpy-stl.


Generating the STL File

To start off I installed numpy-stl globally on my computer using the following command.

pip install numpy-stl


Cube Example

In the numpy-stl documentation there is an example called “Creating Mesh objects from a list of vertices and faces”. This example was a perfect starting point for generating a STL file from a list of coordinates. The full example is shown below. It generates a STL file for a cube.

import numpy as np
from stl import mesh

# Define the 8 vertices of the cube
vertices = np.array([\
    [-1, -1, -1],
    [+1, -1, -1],
    [+1, +1, -1],
    [-1, +1, -1],
    [-1, -1, +1],
    [+1, -1, +1],
    [+1, +1, +1],
    [-1, +1, +1]])
# Define the 12 triangles composing the cube
faces = np.array([\
    [0,3,1],
    [1,3,2],
    [0,4,7],
    [0,7,3],
    [4,5,6],
    [4,6,7],
    [5,1,2],
    [5,2,6],
    [2,3,6],
    [3,7,6],
    [0,1,5],
    [0,5,4]])

# Create the mesh
cube = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
    for j in range(3):
        cube.vectors[i][j] = vertices[f[j],:]

# Write the mesh to file "cube.stl"
cube.save('cube.stl')

I saved this example code into a file called generate_cube.py. Running the file generates the STL file “cube.stl”. In the gif below, I import the STL file into my slicer, and as you can see, it appears as expected.

Importing the example cube into my slicer, Simplify3D.
Importing the example cube into my slicer, Simplify3D.


Generating the Shape

Now that I have confirmed the example worked, I can start trying to generate the figure from the paper. First I imported all of the necessary Python libraries.

import numpy as np
from stl import mesh
from mpl_toolkits import mplot3d
from matplotlib import pyplot

Then, I created a numpy array of all of the vertices that make up the shape. I started at the top corner of the shape and moved down through all of the points in the shape, trying to keep adjacent points in order as much as possible. I also commented the index of each coordinate in the array on the same line. This will make it easier to access their indices when defining the faces.

vertices = np.array([
    [0,0,1], #0
    [0,1,1], #1
    [1,1,1], #2
    [1,0,1], #3
    [0,0,0], #4
    [0,1,0], #5 
    [1,1,0], #6
    [1,0,0], #7
    [3,3,0], #8
    [3,2,0], #9
    [0,1,-3], #10
    [3,3,-3], #11
    [4,2,0], #12
    [0,0,-2], #13
    [1,0,-2], #14
    [0,0,-3], #15
    [3,2,-3], #16
    [3,2,-2], #17
    [4,2,-2], #18
])

Next, I created a numpy array of faces. STL files require all faces to be defined as triangles using the right hand rule to define the normal. This simply means that triangles should be defined with their coordinates ordered counter-clockwise to make sure that the face of the triangle is pointing outward. Here are the steps for defining a triangle face in the model:

  1. Identify the points of the triangle and arrange them in a counter-clockwise order.
  2. Translate the points into indices in the vertices array (defined above).
  3. Add this list of vertex indices to the faces array.

Now, let’s follow these steps to define the first face which is highlighted in blue in the image below:

  1. Looking at the image, I can see that the points are (0,0,1), (1,1,1), and (0,1,1) in counter-clockwise order.
  2. In the vertices array, I can see that (0,0,1) is at index 0, (1,1,1) is at index 2, and (0,1,1) is at index 1.
  3. From steps one and two I can conclude that the first face I need to add to the faces array is [0,2,1].
The right-hand rule is used to define triangle faces for STL files.
The right-hand rule is used to define triangle faces for STL files.

Below is the faces array after repeating this process for every triangle making up the shape.

faces = np.array([
    [0,2,1],
    [0,3,2],
    
    [0,1,5],
    [0,5,4],
    
    [5,1,2],
    [5,2,6],
    
    [3,6,2],
    [3,7,6],
    
    [0,4,7],
    [0,7,3],
    
    [6,8,5],
    [6,9,8],
    
    [5,8,11],
    [5,11,10],
    
    [6,7,12],
    [6,12,9],
    
    [4,13,14],
    [4,14,7],
    
    [4,10,15],
    [4,5,10],
    
    [8,16,11],
    [8,9,16],
    
    [9,12,18],
    [9,18,17],
    
    [7,18,12],
    [7,14,18],
    
    [13,17,18],
    [13,18,14],
    
    [13,15,16],
    [13,16,17],
    
    [15,10,11],
    [15,11,16],
])

Now that I have a list of faces and vertices I can start generating the vectors that compose the shape. To start off, I can initialize a variable called shape to be a numpy-stl Mesh object by using the following line.

shape = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))

Then, the vectors for the shape can be generated with the following nested for loops.

for i, f in enumerate(faces):
    for j in range(3):
        shape.vectors[i][j] = vertices[f[j], :]

Lastly, the shape can be saved using the Mesh.save function. This function also updates the normal vectors by default.

shape.save("parallelepipeds.stl")

I can now import this STL file into my slicer software and generate the code for my 3D printer to make the figure.

The full figure imported into Simplify3D.
The full figure imported into Simplify3D.

Below is the full Python script that I used for generating the shape.

import numpy as np
from stl import mesh
from mpl_toolkits import mplot3d
from matplotlib import pyplot

vertices = np.array([
    [0,0,1], #0
    [0,1,1], #1
    [1,1,1], #2
    [1,0,1], #3
    [0,0,0], #4
    [0,1,0], #5 
    [1,1,0], #6
    [1,0,0], #7
    [3,3,0], #8
    [3,2,0], #9
    [0,1,-3], #10
    [3,3,-3], #11
    [4,2,0], #12
    [0,0,-2], #13
    [1,0,-2], #14
    [0,0,-3], #15
    [3,2,-3], #16
    [3,2,-2], #17
    [4,2,-2], #18
])

faces = np.array([
    [0,2,1],
    [0,3,2],
    
    [0,1,5],
    [0,5,4],
    
    [5,1,2],
    [5,2,6],
    
    [3,6,2],
    [3,7,6],
    
    [0,4,7],
    [0,7,3],
    
    [6,8,5],
    [6,9,8],
    
    [5,8,11],
    [5,11,10],
    
    [6,7,12],
    [6,12,9],
    
    [4,13,14],
    [4,14,7],
    
    [4,10,15],
    [4,5,10],
    
    [8,16,11],
    [8,9,16],
    
    [9,12,18],
    [9,18,17],
    
    [7,18,12],
    [7,14,18],
    
    [13,17,18],
    [13,18,14],
    
    [13,15,16],
    [13,16,17],
    
    [15,10,11],
    [15,11,16],
])

shape = mesh.Mesh(np.zeros(faces.shape[0], dtype=mesh.Mesh.dtype))
for i, f in enumerate(faces):
    for j in range(3):
        shape.vectors[i][j] = vertices[f[j], :]

shape.save("parallelepipeds.stl")


Creating the Shape

In this section I’m going to describe the process I used to print, touch-up, and paint the shape.

The slicer software that I use is called Simplify3D. Slicer software is used for “slicing” the model into the layers to be printed by a 3D printer. This slicer software outputs a gcode file which is read by the 3D printer to perform the actions to print the model. My process for printing parts is to save my gcode files to an SD card and then insert the SD card into my printers SD card slot. Then I can simply select a file to print, and my printer will start creating the part layer by layer.

Printing the model.
Printing the model.

After printing the figure, I brushed on a coating called XTC-3D to smooth out the imperfections and layer lines on the print. After this coating was dry, I sanded and filed the part smooth.

The model after being coated with XTC-3D and sanded smooth.
The model after being coated with XTC-3D and sanded smooth.

To paint the model, I started off by covering it with several coats of white primer. Then I isolated each of the individual parallelepipeds, and applied several coats of their designated color. Lastly, I applied a matte finish to the model. Below is an image of the final model.

The final model after painting.
The final model after painting.


Conclusion

I hope that people from many different backgrounds can learn something new from this article. I know that mathematicians put a lot of work into writing papers, and may want a 3D print to remember their work by. I hope that this article was effective in explaining the process from a fabricators point of view.


1 McDonough, Alex. “A Combinatorial Mapping for the Higher-Dimensional Matrix-Tree Theorem.” ArXiv.org, 30 July 2020, arxiv.org/abs/2007.09501. Link