Stochasitic Lattice and Booleans

hip_leader_2.png

Intro

In this tutorial, we’ll use the Genysis python library to add areas of bio-compatible stochastic lattice to a two-part hip replacement. This will allow spaces for tissue to integrate into the implant. Specifically, we’ll be using the boolean function and the stochastic variant of 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. There are two separate parts we’ll be dealing with, the bipolar head, which would be implanted into the pelvis, and the femoral stem, which would be implanted into the top of the femur. Each of those parts has three .obj files, the solid part and two helper volumes that we’ll use when running boolean operations.

First, download these six files:

hip-replacement_bipoloar-head.obj

hip-replacement_bipoloar-head_difference-1.obj

hip-replacement_bipoloar-head_intersection-1.obj

hip-replacement_femoral-stem.obj

hip-replacement_femoral-stem_difference-1.obj

hip-replacement_femoral-stem_intersection-1.obj

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

The full code for this tutorial is included at the bottom of the page and also at the following link:

hip-replacement_stochastic-lattice.py

Ok, ready to make some shapes!

The bipolar head and the femoral stem will both undergo the same process:

  1. Cutting a pre-defined volume out of the solid part. We will combine this with our lattice to create our final part.

  2. We’ll use a pre-defined intersection volume to select just the section of the solid part that we want to use for the lattice.

  3. We’ll generate a lattice using the output of step 2 and recombine it (union) with the output from step 1

  4. Profit?!?!?

I’ll go over the process in detail for the first part, the bipolar head, and then leave the second one with less granular code comments.

Let’s walk through the script one step at a time.

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"
bipolar_head_filename = "hip-replacement_bipolar-head.obj"
bipolar_head_difference_tool_filename = "hip-replacement_bipolar-head_difference-1.obj"
bipolar_head_intersection_tool_filename = "hip-replacement_bipolar-head_intersection-1.obj"
femoral_stem_filename = "hip-replacement_femoral-stem.obj"
femoral_stem_difference_tool_filename = "hip-replacement_femoral-stem_difference-1.obj"
femoral_stem_intersection_tool_filename = "hip-replacement_femoral-stem_intersection-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

this is how the bipolar head will end up

this is how the bipolar head will end up

Now, starting with the bipolar head, we are going to use the boolean function with the “difference” operation to cut off the outer shell of the solid part using our pre-prepared difference volume.


bipolar_head_difference_output_filename = "hip-replacement_bipolar-head_difference-1-applied.obj"

genysis.boolean(
    input1=bipolar_head_filename,
    input2=bipolar_head_difference_tool_filename,
    output=bipolar_head_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.


bipolar_head_intersection_output_filename = "hip-replacement_bipolar-head_intersection-1-applied.obj"

genysis.boolean(
    input1=bipolar_head_filename,
    input2=bipolar_head_intersection_tool_filename,
    output=bipolar_head_intersection_output_filename,
    operation="intersection",
    token=token)

Now we are ready to create our Stochastic Volume Lattice, which produces a lattice on a uniform cubic grid. We’ll create a lattice instance and then call a few of its methods to set parameters. Then, we will “run” the lattice to generate an output file.

Stochastic lattices are random by nature and don’t require us to define a grid unit (unlike a vanilla volume lattice). The main control for a stochastic lattice is the pore size, which controls the average diameter of a cell. I’ve used two different pore sizes on the two parts as a demonstration (1.0 for the bipolar head and 2.5 for the femoral stem).

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
bipolar_head_lattice = genysis.volumeLattice()

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

# set the pore size for the lattice
bipolar_head_lattice.setPoreSize(1.0)

# tell genysis where to save the lattice object
completed_bipolar_head_lattice_filename = "hip-replacement_bipolar-head_lattice-1-applied.obj"
bipolar_head_lattice.setOutput(completed_bipolar_head_lattice_filename)

# generate the lattice (this is a large part and it might take a min or two...)
bipolar_head_lattice.runStochastic(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 the pore size is, how large your overall lattice volume is, and how densely filled you want your lattice to be.

I found that for this project a resolution of 300 gives a good balance of mesh detail and computation time (a higher resolution will take more time and produce a smoother result). I used an edge thickness (memberThickness) of 0.2 . A good place to start when experimenting is 300 for resolution and somewhere around 1/8 of your pore size for your member thickness. Here’s how that looks in the code:


bipolar_head_meshed_lattice_filename = "hip-replacement_bipolar-head_lattice-1-meshed"

bipolar_head_stl_files = genysis.marchingCube(
    lines=completed_bipolar_head_lattice_filename,
    resolution=300,
    memberThickness=0.2,
    filename=bipolar_head_meshed_lattice_filename,
    token=token)

Ok now we’re done creating the parts for the bipolar head.

femoral_stem.png

Next up is the exact same process, but on the femoral stem files instead, here’s that code:


## FEMORAL STEM COMPONENT

# cut out the middle section of the femoral stem
# (so we can replace it with a biocompatible stochastic lattice)
# and store it in a new file
femoral_stem_difference_output_filename = "hip-replacement_femoral-stem_difference-1-applied.obj"

genysis.boolean(
    input1=femoral_stem_filename,
    input2=femoral_stem_difference_tool_filename,
    output=femoral_stem_difference_output_filename,
    operation="difference",
    token=token)

# grab an intersection with the femoral stem
# to use as the bounds of our lattice and store it in a new file
femoral_stem_intersection_output_filename = "hip-replacement_femoral-stem_intersection-1-applied.obj"

genysis.boolean(
    input1=femoral_stem_filename,
    input2=femoral_stem_intersection_tool_filename,
    output=femoral_stem_intersection_output_filename,
    operation="intersection",
    token=token)

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

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

# set the pore size of the lattice
femoral_stem_lattice.setPoreSize(2.5)

# tell genysis where to save the lattice object
completed_femoral_stem_lattice_filename = "hip-replacement_femoral-stem_lattice-1-applied.obj"
femoral_stem_lattice.setOutput(completed_femoral_stem_lattice_filename)

# generate the lattice (this is a large part and it might take a min or two...)
femoral_stem_lattice.runStochastic(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
femoral_stem_meshed_lattice_filename = "hip-replacement_femoral-stem_lattice-1-meshed"

femoral_stem_stl_files = genysis.marchingCube(
    lines=completed_femoral_stem_lattice_filename,
    resolution=300,
    memberThickness=0.5,
    filename=femoral_stem_meshed_lattice_filename,
    token=token)

Ok now we’ve got all the parts we’ll need to finish building the final parts. We just have to deal with one challenge before we can do that…

The marching cubes function is designed to split it’s 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 enormous 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:


[
    "hip-replacement_bipolar-head_lattice-1-meshed_0.stl",
    "hip-replacement_bipolar-head_lattice-1-meshed_1.stl",
    "hip-replacement_bipolar-head_lattice-1-meshed_2.stl",
    "hip-replacement_bipolar-head_lattice-1-meshed_3.stl",
    "hip-replacement_bipolar-head_lattice-1-meshed_4.stl"
]

I’ve written some code to download the .stl files for each of the parts, 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 femoral_stem_stl_files:
    genysis.download(src=file, dest=file, token=token)

for file in bipolar_head_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_bipolar_head = numpy.concatenate(
    [stl.stl.BaseStl.load(open(filename, "rb"))[1] for filename in bipolar_head_stl_files]
)

combined_femoral_stem = numpy.concatenate(
    [stl.stl.BaseStl.load(open(filename, "rb"))[1] for filename in femoral_stem_stl_files]
)

bipolar_head_faces = combined_bipolar_head['vectors']

femoral_stem_faces = combined_femoral_stem['vectors']

del combined_bipolar_head
del combined_femoral_stem

bipolar_head_verts = OrderedDict()
bipolar_head_tris = []

femoral_stem_verts = OrderedDict()
femoral_stem_tris = []

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

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

        tri.append(v_index)
    if(tri[0] == tri[1] or tri[0] == tri[2] or tri[1] == tri[2]):
        print("bipolar head tri has duplicate vertex index!")
        print("tri # " + str(len(bipolar_head_tris)))
        print(str(tri[0]) + " " + str(tri[1]) + " " + str(tri[2]))
    bipolar_head_tris.append(tri)

del bipolar_head_faces

cur_v_index = 0
for face in femoral_stem_faces:
    tri = []
    for vert in face:
        v = tuple(vert)

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

        tri.append(v_index)
    if(tri[0] == tri[1] or tri[0] == tri[2] or tri[1] == tri[2]):
        print("femoral stem tri has duplicate vertex index!")
        print("tri # " + str(len(femoral_stem_tris)))
        print(str(tri[0]) + " " + str(tri[1]) + " " + str(tri[2]))
    femoral_stem_tris.append(tri)

del femoral_stem_faces

# wite .obj files to local folder
print("writing bipolar head")
with open(bipolar_head_meshed_lattice_filename + ".obj", 'w') as f:
    f.write("# OBJ file\n")

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

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

print("writing femoral stem")
with open(femoral_stem_meshed_lattice_filename + ".obj", 'w') as f:
    f.write("# OBJ file\n")

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

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

Phew. This is a process that we intend to make easier in the future so keep an eye out!

Finally, we’ll use a special secret endpoint that is not yet exposed in the Genysis python library to upload these two huge files 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

bipolar_files = {'file': (bipolar_head_meshed_lattice_filename, open(bipolar_head_meshed_lattice_filename, 'rb'))}

r_bipolar = requests.post(upload_url, files=bipolar_files)

femoral_files = {'file': (femoral_stem_meshed_lattice_filename, open(femoral_stem_meshed_lattice_filename, 'rb'))}

r_femoral = requests.post(upload_url, files=femoral_files)

As our last step, we can perform the final boolean union operation, to combine our cutout parts (remember when we did that!) with the finalized lattices.


completed_bipolar_head_filename = "hip-replacement_bipolar-head-with-lattice_final.obj"

results = genysis.boolean(
    input1=bipolar_head_difference_output_filename,
    input2=bipolar_head_meshed_lattice_filename + ".obj",
    output=completed_bipolar_head_filename,
    operation="union",
    token=token)

print(results)

completed_femoral_stem_filename = "hip-replacement_femoral-stem-with-lattice_final.obj"

results = genysis.boolean(
    input1=femoral_stem_difference_output_filename,
    input2=femoral_stem_meshed_lattice_filename + ".obj",
    output=completed_femoral_stem_filename,
    operation="union",
    token=token)

print(results)

We’re done making shapes so give yourself a pat on the back! Now we can use the visualize function to have a look at our files 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 files down to our local folder.


# open a browser window to preview the final bipolar head
genysis.visualize(name=completed_bipolar_head_filename)
# download the final bipolar head file to the local folder
genysis.download(src=completed_bipolar_head_filename, dest=completed_bipolar_head_filename, token=token)

# open a browser window to preview the final femoral stem
genysis.visualize(name=completed_femoral_stem_filename)
# download the final femoral stem file to the local folder
genysis.download(src=completed_femoral_stem_filename, dest=completed_femoral_stem_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 = "PUT YOUR PRIVATE TOKEN HERE"
bipolar_head_filename = "hip-replacement_bipolar-head.obj"
bipolar_head_difference_tool_filename = "hip-replacement_bipolar-head_difference-1.obj"
bipolar_head_intersection_tool_filename = "hip-replacement_bipolar-head_intersection-1.obj"
femoral_stem_filename = "hip-replacement_femoral-stem.obj"
femoral_stem_difference_tool_filename = "hip-replacement_femoral-stem_difference-1.obj"
femoral_stem_intersection_tool_filename = "hip-replacement_femoral-stem_intersection-1.obj"

# this hip replacement is made of two parts, the bipolar head, which is surgically installed
# into the hip bone, and the femoral stem, which is surgically installed into the femur.
# both parts need to integrate with the bone around them, so we are adding a biocompatible
# stochastic lattice to a portion of both parts.

## BIPOLAR HEAD COMPONENT

# cut the surface off of the bipolar head
# (so we can replace it with a biocompatible stochastic lattice)
# and store it in a new file
bipolar_head_difference_output_filename = "hip-replacement_bipolar-head_difference-1-applied.obj"

genysis.boolean(
    input1=bipolar_head_filename,
    input2=bipolar_head_difference_tool_filename,
    output=bipolar_head_difference_output_filename,
    operation="difference",
    token=token)

# grab an intersection with the bipolar head
# to use as the bounds of our lattice and store it in a new file
bipolar_head_intersection_output_filename = "hip-replacement_bipolar-head_intersection-1-applied.obj"

genysis.boolean(
    input1=bipolar_head_filename,
    input2=bipolar_head_intersection_tool_filename,
    output=bipolar_head_intersection_output_filename,
    operation="intersection",
    token=token)

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

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

# set the pore size for the lattice
bipolar_head_lattice.setPoreSize(1.0)

# tell genysis where to save the lattice object
completed_bipolar_head_lattice_filename = "hip-replacement_bipolar-head_lattice-1-applied.obj"
bipolar_head_lattice.setOutput(completed_bipolar_head_lattice_filename)

# generate the lattice (this is a large part and it might take a min or two...)
bipolar_head_lattice.runStochastic(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
bipolar_head_meshed_lattice_filename = "hip-replacement_bipolar-head_lattice-1-meshed"

bipolar_head_stl_files = genysis.marchingCube(
    lines=completed_bipolar_head_lattice_filename,
    resolution=300,
    memberThickness=0.2,
    filename=bipolar_head_meshed_lattice_filename,
    token=token)

## FEMORAL STEM COMPONENT

# cut the surface off of the bipolar head
# (so we can replace it with a biocompatible stochastic lattice)
# and store it in a new file
femoral_stem_difference_output_filename = "hip-replacement_femoral-stem_difference-1-applied.obj"

genysis.boolean(
    input1=femoral_stem_filename,
    input2=femoral_stem_difference_tool_filename,
    output=femoral_stem_difference_output_filename,
    operation="difference",
    token=token)

# grab an intersection with the bipolar head
# to use as the bounds of our lattice and store it in a new file
femoral_stem_intersection_output_filename = "hip-replacement_femoral-stem_intersection-1-applied.obj"

genysis.boolean(
    input1=femoral_stem_filename,
    input2=femoral_stem_intersection_tool_filename,
    output=femoral_stem_intersection_output_filename,
    operation="intersection",
    token=token)

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

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

# set the pore size of the lattice
femoral_stem_lattice.setPoreSize(2.5)

# tell genysis where to save the lattice object
completed_femoral_stem_lattice_filename = "hip-replacement_femoral-stem_lattice-1-applied.obj"
femoral_stem_lattice.setOutput(completed_femoral_stem_lattice_filename)

# generate the lattice (this is a large part and it might take a min or two...)
femoral_stem_lattice.runStochastic(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
femoral_stem_meshed_lattice_filename = "hip-replacement_femoral-stem_lattice-1-meshed"

femoral_stem_stl_files = genysis.marchingCube(
    lines=completed_femoral_stem_lattice_filename,
    resolution=300,
    memberThickness=0.5,
    filename=femoral_stem_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.

# 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 femoral_stem_stl_files:
    genysis.download(src=file, dest=file, token=token)

for file in bipolar_head_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_bipolar_head = numpy.concatenate(
    [stl.stl.BaseStl.load(open(filename, "rb"))[1] for filename in bipolar_head_stl_files]
)

combined_femoral_stem = numpy.concatenate(
    [stl.stl.BaseStl.load(open(filename, "rb"))[1] for filename in femoral_stem_stl_files]
)

bipolar_head_faces = combined_bipolar_head['vectors']

femoral_stem_faces = combined_femoral_stem['vectors']

del combined_bipolar_head
del combined_femoral_stem

bipolar_head_verts = OrderedDict()
bipolar_head_tris = []

femoral_stem_verts = OrderedDict()
femoral_stem_tris = []

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

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

        tri.append(v_index)
    if(tri[0] == tri[1] or tri[0] == tri[2] or tri[1] == tri[2]):
        print("bipolar head tri has duplicate vertex index!")
        print("tri # " + str(len(bipolar_head_tris)))
        print(str(tri[0]) + " " + str(tri[1]) + " " + str(tri[2]))
    bipolar_head_tris.append(tri)

del bipolar_head_faces

cur_v_index = 0
for face in femoral_stem_faces:
    tri = []
    for vert in face:
        v = tuple(vert)

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

        tri.append(v_index)
    if(tri[0] == tri[1] or tri[0] == tri[2] or tri[1] == tri[2]):
        print("femoral stem tri has duplicate vertex index!")
        print("tri # " + str(len(femoral_stem_tris)))
        print(str(tri[0]) + " " + str(tri[1]) + " " + str(tri[2]))
    femoral_stem_tris.append(tri)

del femoral_stem_faces

# wite .obj files to local folder
print("writing bipolar head")
with open(bipolar_head_meshed_lattice_filename + ".obj", 'w') as f:
    f.write("# OBJ file\n")

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

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

print("writing femoral stem")
with open(femoral_stem_meshed_lattice_filename + ".obj", 'w') as f:
    f.write("# OBJ file\n")

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

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

# upload the now combined meshed lattice .OBJs 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

bipolar_files = {'file': (bipolar_head_meshed_lattice_filename, open(bipolar_head_meshed_lattice_filename, 'rb'))}

r_bipolar = requests.post(upload_url, files=bipolar_files)

femoral_files = {'file': (femoral_stem_meshed_lattice_filename, open(femoral_stem_meshed_lattice_filename, 'rb'))}

r_femoral = requests.post(upload_url, files=femoral_files)

# combine the lattices with the original shapes
completed_bipolar_head_filename = "hip-replacement_bipolar-head-with-lattice_final.obj"

results = genysis.boolean(
    input1=bipolar_head_difference_output_filename,
    input2=bipolar_head_meshed_lattice_filename + ".obj",
    output=completed_bipolar_head_filename,
    operation="union",
    token=token)

print(results)

completed_femoral_stem_filename = "hip-replacement_femoral-stem-with-lattice_final.obj"

results = genysis.boolean(
    input1=femoral_stem_difference_output_filename,
    input2=femoral_stem_meshed_lattice_filename + ".obj",
    output=completed_femoral_stem_filename,
    operation="union",
    token=token)

print(results)


# open a browser window to preview the final bipolar head
genysis.visualize(name=completed_bipolar_head_filename)
# download the final bipolar head file to the local folder
genysis.download(src=completed_bipolar_head_filename, dest=completed_bipolar_head_filename, token=token)

# open a browser window to preview the final femoral stem
genysis.visualize(name=completed_femoral_stem_filename)
# download the final femoral stem file to the local folder
genysis.download(src=completed_femoral_stem_filename, dest=completed_femoral_stem_filename, token=token)

#easy part upload at https://studiobitonti.appspot.com/
#see upload tutorial for more details
Francis Bitonti