Volume Lattice and Booleans

finalPiston.png

Intro

In this tutorial, we’ll use the Genysis python library to modify an existing mechanical part, replacing some of its solid material with a strong, yet light weight, uniform lattice structure. Specifically, we’ll be using the boolean function and the volume lattice function.

The full source code for the python library is available on GitHub at: 

https://github.com/francisbitontistudio/genysis_documentation/blob/master/python_package/genysis_pkg/genysis/genysis.py

Prerequisites

In order to follow along with this tutorial, you will need a copy of python/pip installed (either 2.7+ or 3.6+).

You will also need a Genysis API token, which you can request here:

https://studiobitonti.appspot.com/subscription.html

To use the tutorial code, you’ll need to install the Genysis package via pip. At the command line, type:


$ pip install genysis

Before we dive into the code, you’ll need to upload the 3D files needed for the tutorial to your Genysis account. We’ll be using three .obj files, one is the solid connecting rod and the other two are helper shapes that we’ll use for our boolean operations. First download these:
connecting-rod.obj

connecting-rod_difference-tool-1.obj

connecting-rod_intersection-tool-1.obj

Then upload them to your private Genysis storage using our web interface provided here:
https://studiobitonti.appspot.com/

We’ll be working with the volume colored red here.

We’ll be working with the volume colored red here.

The full code for this tutorial is included in an embedded Gist at the bottom of the page and also at the following link.

connecting-rod_volume-lattice.py

Ok, ready to make some shapes!

Now I’ll walk through the script explaining each step along the way.

First, we will import the Genysis library and set a variable for our private token, which will be passed to each function for authentication purposes. Also, we’ll define some variables that hold the filenames of the .obj files we uploaded earlier.


import genysis

token = "PUT YOUR PRIVATE TOKEN HERE"
connecting_rod_filename = "connecting-rod.obj"
difference_tool_filename = "connecting-rod_difference-tool-1.obj"
intersection_tool_filename = "connecting-rod_intersection-tool-1.obj"

We’ll be repeating the same basic process each time we use one of the Genysis functions:

  • define a filename where the function will save its output in your private Genysis cloud storage

  • run the Genysis function, passing in:

    • the filename(s) from some previously uploaded or computed file(s)

    • the output filename

    • some other parameters for the function

Now, we are going to use the boolean function with the “difference” operation to cut the middle section out of the connecting rod using our pre-prepared difference volume.


difference_output_filename = "connecting-rod_difference-1-applied.obj"

genysis.boolean(
    input1=connecting_rod_filename,
    input2=difference_tool_filename,
    output=difference_output_filename,
    operation="difference",
    token=token)

Next, we will use the boolean function again, this time with the “intersection” operation, to generate a conformal volume for our lattice structure.


intersection_output_filename = "connecting-rod_intersection-1-applied.obj"

genysis.boolean(
    input1=connecting_rod_filename,
    input2=intersection_tool_filename,
    output=intersection_output_filename,
    operation="intersection",
    token=token)

Next, we need to define a lattice unit, which will be replicated over and over along a grid to create our Volume Lattice. While it is possible to provide a custom mesh to use for this unit’s shape, Genysis provides a handy tool for generating parametric lattice units. Read more about this function in the documentation here.


low_density_unit_filename = "low_density_lattice_unit.obj"

genysis.genLatticeUnit(
    case=3,
    chamfer=0.0,
    centerChamfer=0.0,
    bendIn=0.0,
    cBendIn=0.0,
    connectPt=0.0,
    output=low_density_unit_filename,
    token=token)

Genysis refers to these units as components in many of its functions.

lattice_close.png

Now we are ready to create our Volume Lattice, which produces a lattice on a uniform cubic grid. We’ll create a lattice instance and then call some of its methods to set various parameters. Then, we will “run” the lattice to generate an output file. Lattice functions create a wireframe representation of the lattice, which has no volume and thus is not directly manufacturable. We’ll deal with that in the next step.


# create a volume lattice object
cutoutLattice = genysis.volumeLattice()

# use the previously computed intersection volume as the bounds of our lattice
cutoutLattice.setVolume(intersection_output_filename)

# set the default component/unit for the lattice
cutoutLattice.setComponent(low_density_unit_filename)

# set the size of one lattice unit (the units of this are based on the units of the bounding volume)
cutoutLattice.setComponentSize(4.0)

# tell genysis where to save the lattice object
completed_lattice_filename = "connecting-rod_lattice-1-applied.obj"
cutoutLattice.setOutput(completed_lattice_filename)

# generate the lattice (this is a large part and it might take a min or two...)
cutoutLattice.run(token)

Ok, now we have a wireframe representation of the lattice. In order to transform it into a manufacturable part, we need to “mesh” this skeleton, giving it form. To do this we’ll use the marching cubes function, which uses a voxel representation to create a guaranteed manifold mesh.

We have a few choices now. First, we’ll have to choose a resolution for our voxel grid, which will affect both the surface quality (and polygon count!) of our output as well as the time it takes to compute that output. We also need to decide how thick the edges of our lattice will be. These two variables affect each other, so it can take some experimentation to get the right balance of detail, polygon count, and computation time. There are no hard and fast rules here, it really depends on how large each lattice component is, how large your overall lattice volume is, and how densely filled you want your lattice to be.

Previously, we set our component size (aka. the length of one side of a cube unit of our lattice) to 4.0 . I found that for this project a resolution of 600 gives a nice smooth mesh (a higher resolution will take more time and produce a smoother result) using an edge thickness (memberThickness) of 0.4 . A good place to start when experimenting is 300 for resolution and somewhere around 1/8 of your component size for your member thickness. Here’s how that looks in the code:


meshed_lattice_filename = "connecting-rod_lattice-1-meshed"

stl_files = genysis.marchingCube(
    lines=completed_lattice_filename,
    resolution=600,
    memberThickness=0.4,
    filename=meshed_lattice_filename,
    token=token)

The marching cubes function is designed to split this computationally heavy process apart, so instead of returning a single .obj file it instead returns a list of .stl files (each no more than 90MB) which can be recombined into a huge, hi-definition lattice. This allows Genysis to compute truely gargantuan lattice structures, but does mean we have to do a little extra work if we want to use the meshed lattice output as an input for other operations. The result from the meshing operation is a list of values that looks something like this:


[
    "connecting-rod_lattice-1-meshed_0.stl",
    "connecting-rod_lattice-1-meshed_1.stl",
    "connecting-rod_lattice-1-meshed_2.stl",
    "connecting-rod_lattice-1-meshed_3.stl",
    "connecting-rod_lattice-1-meshed_4.stl"
]

I’ve written some code to download these .stl files, then clean and combine them into a single .obj file, which we can then upload back to Genysys file storage for future use.


# download the .stl files
for file in stl_files:
    genysis.download(src=file, dest=file, token=token)

#$ pip install numpy
import numpy
#$ pip install numpy-stl
import stl

from collections import OrderedDict

# combine all of the stls
combined = numpy.concatenate(
    [stl.stl.BaseStl.load(open(filename, "rb"))[1] for filename in stl_files]
)

faces = combined['vectors']
print("size of faces in KB: " + str(faces.nbytes/1000))

del combined

verts = OrderedDict()
tris = []

cur_v_index = 0

# building vert and tri list
print("deduping verts, this could take a few minutes")
for face in faces:
    tri = []
    for vert in face:
        v = tuple(vert)

        if v not in verts:
            v_index = cur_v_index
            verts[v] = v_index
            cur_v_index += 1
        else:
            v_index = verts[v]

        tri.append(v_index)
    tris.append(tri)

del faces


meshed_lattice_filename_obj = meshed_lattice_filename + ".obj"

# wite .obj file to a local file
with open(meshed_lattice_filename_obj, 'w') as f:
    f.write("# OBJ file\n")

    print("writing verts")
    for v in list(verts.items()):
        f.write("v %.8f %.8f %.8f\n" % v[0][:])

    print("writing tris")
    for t in tris:
        f.write("f")
        for i in t:
            f.write(" %d" % (i + 1))
        f.write("\n")

Finally, we’ll use a special secret endpoint that is not yet exposed in the Genysis python library to upload this large file back up to the cloud file storage.


import requests
# get the upload path for large files
upload_url = requests.get('http://studiobitonti.appspot.com/compute/getNode?t=' + token).text
upload_url += '/storage/upload?t=' + token

files = {'file': (meshed_lattice_filename_obj, open(meshed_lattice_filename_obj, 'rb'))}

r = requests.post(upload_url, files=files)

As our last step, we can perform the final boolean union operation, to combine our solid connecting rod minus the cutout section (remember when we did that!) with the finalized lattice mesh.


completed_connecting_rod_filename = "connected-rod-with-lattice_final.obj"

results = genysis.boolean(
    input1=difference_output_filename,
    input2=meshed_lattice_filename_obj,
    output=completed_connecting_rod_filename,
    operation="union",
    token=token)

We’re done making shapes! Now we can use the visualize function to have a look at our file using a browser (large files may have a hard time being displayed this way). We can also use the Genysis download function to easily pull our final file down to our local folder.


# open a browser window that lets us inspect the file
genysis.visualize(name=completed_connecting_rod_filename)
# download the file to our local machine
genysis.download(src=completed_connecting_rod_filename, dest=completed_connecting_rod_filename, token=token)

Thanks for following along! If you have any questions or get stuck anywhere, reach out to us using our contact page: https://www.genysis.cloud/contact-2/

Full Code


#$ pip install genysis
import genysis

#easy part upload at https://studiobitonti.appspot.com/
#see upload tutorial for more details
token = "YOUR TOKEN GOES HERE"
connecting_rod_filename = "connecting-rod.obj"
difference_tool_filename = "connecting-rod_difference-tool-1.obj"
intersection_tool_filename = "connecting-rod_intersection-tool-1.obj"

# cut a hole out of the original solid connecting rod
# and store it in a new file
difference_output_filename = "connecting-rod_difference-1-applied.obj"

genysis.boolean(
    input1=connecting_rod_filename,
    input2=difference_tool_filename,
    output=difference_output_filename,
    operation="difference",
    token=token)

# grab an intersection with the original solid connecting rod
# to use as the bounds of our lattice and store it in a new file
intersection_output_filename = "connecting-rod_intersection-1-applied.obj"

genysis.boolean(
    input1=connecting_rod_filename,
    input2=intersection_tool_filename,
    output=intersection_output_filename,
    operation="intersection",
    token=token)

# create a low density lattice unit to replace the solid center of the connecting rod
low_density_unit_filename = "low_density_lattice_unit.obj"

genysis.genLatticeUnit(
    case=3,
    chamfer=0.0,
    centerChamfer=0.0,
    bendIn=0.0,
    cBendIn=0.0,
    connectPt=0.0,
    output=low_density_unit_filename,
    token=token)

# create a volume lattice object
cutoutLattice = genysis.volumeLattice()

# use the previously computed intersection volume as the bounds of our lattice
cutoutLattice.setVolume(intersection_output_filename)

# set the default component/unit for the lattice
cutoutLattice.setComponent(low_density_unit_filename)

# set the size of one lattice unit
cutoutLattice.setComponentSize(4.0)

# tell genysis where to save the lattice object
completed_lattice_filename = "connecting-rod_lattice-1-applied.obj"
cutoutLattice.setOutput(completed_lattice_filename)

# generate the lattice (this is a large part and it might take a min or two...)
cutoutLattice.run(token)

# the lattice is just a wireframe structure now
# we need to create a mesh around the wireframe in order to have a manufacturable part
meshed_lattice_filename = "connecting-rod_lattice-1-meshed"

stl_files = genysis.marchingCube(
    lines=completed_lattice_filename,
    resolution=600,
    memberThickness=0.4,
    filename=meshed_lattice_filename,
    token=token)

#the function will return a list of STL files, each one no larger than 90 MB
#high density meshes are computed distriubted for speed.
#for example...
# [
#     "connecting-rod_lattice-1-meshed_0.stl",
#     "connecting-rod_lattice-1-meshed_1.stl",
#     "connecting-rod_lattice-1-meshed_2.stl",
#     "connecting-rod_lattice-1-meshed_3.stl",
#     "connecting-rod_lattice-1-meshed_4.stl"
# ]

# In order to perform the final boolean operation
# we need to download the .stl files, combine them, save them as an .obj file and
# finally upload it so we can use it with genysis

# download the .stl files
for file in stl_files:
    genysis.download(src=file, dest=file, token=token)

#$ pip install numpy
import numpy
#$ pip install numpy-stl
import stl

from collections import OrderedDict

# combine all of the stls
combined = numpy.concatenate(
    [stl.stl.BaseStl.load(open(filename, "rb"))[1] for filename in stl_files]
)

faces = combined['vectors']
print("size of faces in KB: " + str(faces.nbytes/1000))

del combined

verts = OrderedDict()
tris = []

cur_v_index = 0

# building vert and tri list
print("deduping verts, this could take a few minutes")
for face in faces:
    tri = []
    for vert in face:
        v = tuple(vert)

        if v not in verts:
            v_index = cur_v_index
            verts[v] = v_index
            cur_v_index += 1
        else:
            v_index = verts[v]

        tri.append(v_index)
    tris.append(tri)

del faces


meshed_lattice_filename_obj = meshed_lattice_filename + ".obj"

# wite .obj file to a local file
with open(meshed_lattice_filename_obj, 'w') as f:
    f.write("# OBJ file\n")

    print("writing verts")
    for v in list(verts.items()):
        f.write("v %.8f %.8f %.8f\n" % v[0][:])

    print("writing tris")
    for t in tris:
        f.write("f")
        for i in t:
            f.write(" %d" % (i + 1))
        f.write("\n")

# upload the now combined meshed lattice obj to the genysis server
import requests
# get the upload path for large files
upload_url = requests.get('http://studiobitonti.appspot.com/compute/getNode?t=' + token).text
upload_url += '/storage/upload?t=' + token

files = {'file': (meshed_lattice_filename_obj, open(meshed_lattice_filename_obj, 'rb'))}

r = requests.post(upload_url, files=files)

# combine the lattice with the connecting rod with a hole cut out
# to get our final part
completed_connecting_rod_filename = "connected-rod-with-lattice_final.obj"

results = genysis.boolean(
    input1=difference_output_filename,
    input2=meshed_lattice_filename_obj,
    output=completed_connecting_rod_filename,
    operation="union",
    token=token)

# we should get back a single obj file as the boolean result
print(results)

# open a browser window that lets us inspect the file
genysis.visualize(name=completed_connecting_rod_filename)
# download the file to our local machine
genysis.download(src=completed_connecting_rod_filename, dest=completed_connecting_rod_filename, token=token)

Francis Bitonti