3D mesh models in the browser using python & dash_vtk | by Steve Kiefer | Towards Data Science

Python needs no real introduction. Its mature, has lots of powerful libraries and is easy to read and code. Plotly’s Dash framework is an easy to use way to develop web applications using only python. VTK is a mature C++ 3D library with a python port and VTK.js is VTK reimplemented for the browser that leverages webGL. VTK & Plotly recently announced the release of dash_vtk and hosted a webinar showing what it can do.
Years ago I dabbled in VTK-python building a couple applications using pyQt that could display and slice structural Finite Element Analysis mesh & results. Recently, I have been pacing a lot of analysis related tools or databases into web-apps using Dash. Why? For starters its much easier to get something interactive & good looking. Looks may not be everything, but they are certainly something! The web apps are also easier to control and update (no old copies floating around), and the Plotly graphing libraries look good without much tweaking.
The one thing missing with this new toolset has been good 3D visualization. Plotly has some capabilities, and I have gotten by but its a bit lacking in 3D model support. So when I heard about dash_vtk I was excited! I could integrate my 3D results into some new apps, except.....I didn’t see much support or discussion for what vtk calls unstructured meshes. An unstructured mesh is a general 3D mesh that is comprised of different element types & shapes (hexahedron, tetrahedron, quadrilateral, etc) and the grid distances are not necessarily regular and there can be a mix of element shapes in any mesh. This is the most common (if not only) only mesh type used in structural analysis of general, physical parts. So I decided to see if I could adapt what little I could remember from previously working with VTK (via python) and make it work in dash_vtk.
This article walks through creating a Dash web app that allows a user to upload mesh files (nodes & element data) and view results. To keep it relatively short and focused I’m going to assume the reader has some familiarity with Dash. If not, check out the official Dash tutorial and documentation, they have lots of examples posted.
Below is gif that shows the app in action. Here is the link to the github repo.
VTK is not a super user-friendly library, at least for a novice like me. There are actors, mappers, controllers, all kinds of objects that need to be created and connected to be able to display and interact with some 3D data. Soon after starting to explore dash_vtk I found the pyvista library (its pretty easy to find, its mentioned a bunch on the dash_vtk documentation). Pyvista either wasn’t around when I was last using VTK or I just didn’t find it. I’m glad it exists and I sincerely thank the developers. It makes working with VTK objects much easier and more familiar.
Quoting the pyvista documentation:
PyVista is...
“VTK for humans”: a high-level API to the Visualization Toolkit (VTK)
mesh data structures and filtering methods for spatial datasets
3D plotting made simple and built for large/complex data geometries
The basic workflow for getting an unstructured mesh from some other tool into VTK is to:
Export the grid ID’s and locations relative to a common coordinate systemExport the element IDs and the associated nodesMap the node ordering / element topology into VTK’s orderRenumber / re-index so that element and grid IDs match the index position in their respective lists
Export the grid ID’s and locations relative to a common coordinate system
Export the element IDs and the associated nodes
Map the node ordering / element topology into VTK’s order
Renumber / re-index so that element and grid IDs match the index position in their respective lists
To convert any other source mesh into VTK you will need to know the element node ordering & connectivity. You will need to reorder your grids assigned to each element so they fall in the order VTK expects or you will get twisted elements. No one wants twisted elements. These links show the VTK ordering:
Points, Lines, & Linear ElementsQuadratic+ Elements
Points, Lines, & Linear Elements
Quadratic+ Elements
I use Ansys Mechanical for most of my structural analysis. It is not open source and definitely not cheap. If you don’t use it and/or don’t care how I am getting the data out feel free to skip this section. This isn’t the main focus so I’ll be brief. The steps to exporting data from Ansys mechanical are:
Create a node named selection that contains all nodes of the part/body you want. A worksheet named selection can help convert bodies to nodes.Repeat step 1 for the elementsRight click on each of those named selections and select Export Test FileCreate a results object with the same body of interest. Once the solve is complete, right-click and again Export Text File
Create a node named selection that contains all nodes of the part/body you want. A worksheet named selection can help convert bodies to nodes.
Repeat step 1 for the elements
Right click on each of those named selections and select Export Test File
Create a results object with the same body of interest. Once the solve is complete, right-click and again Export Text File
Below are the first few lines of each file this app is setup to process.
We will be using dcc.Upload components to allow the user to upload the files into the web app. When using these components, the file objects get passed as base64 strings and must be decoded. Once decoded they can be added to a io.StringIO object and used like a filename. I created a function outside of the callbacks to process the base64 string data and convert it to a VTK / pyvista objects that we can use with dash_vtk.
Below is an abbreviated version of the function that does the main work getting our mesh data into VTK format. Let’s walk through it!
The function expects at least 2 files for creating the mesh and optionally a results file. lets start with creating the mesh. In the first few lines we get the base64 string decoded and read into a Pandas Dataframe. On lines 7–10 we are re-indexing so the node ids match the position in the list of nodes but allow for a single offset at the start. If your mesh (or portion you are processing) is not continuous this helps maintain alignment from the nodes to cells and results.
See the repo for converting and re-indexing the element data from the base64 object. We also need the number of nodes that defines each element and for this data we can pull it from the element names (eg Tet10).
At line 18 of this gist we have the dictionary that maps the element name to the VTK element type. The keys are specific to the Ansys Mechanical export, you would replace these with whatever your tool FE tool names its element types. On line 25 we perform the mapping into the element dataframe and drop any elements that didn’t get mappped. At this point we have a nodes dataframe and element dataframe reindexed with all the info we need.
On line 28 we convert the portion of the nodes dataframe we need into a 2D numpy array that only contains XYZ locations. For the elements we need a separate array with just the VTK element types: cell_types. Next we convert the node id portion of the element dataframe to a numpy array and subtract the lowest grid id. This is again due to VTK using positions as the mapping rather than explicit ids. We then hstack the number of nodes per element array in front of the modified node ids array to get a 2D array:
[[n_nodes, node_index_1, node_index_2, node_index_3....],[n_nodes, node_index_1, node_index_2, node_index_3....],...]
Pyvista wants this as a flattened list so we ravel() on line 37. If your mesh is a mix of different element types (with different numbers of nodes) then you will have nans in this array. We remove them at line 39, then convert to int and pass them (along with the points array and cell types list) to the pyvista UnstructuredGrid function. Pyvista lets us work easily with numpy arrays and not have to convert to VTK data object in between. We now have an unstructured grid object!
Now we need to add the element results.
This gist picks up where the last one left off. We have a uGrid unstructured grid object. If the call to this function includes a results file we then enter this logic block to add the results to the grid.
Lines 2–12 of this gist imports the data from the base64 object and reindexes to once again map the results to the appropriate location in the list so the location and associated element id are the same. Here we are filling missing values with 0.0. Then we convert to a numpy array. Line 15 is where we can see the ease of use of pyvista, just give the uGrid object a name (here I’m using ‘my_array’) and pass the numpy array! Easy-Peezy. We return the uGrid object and the min/max range of data (or a default) for the colorscale.
Now we have a function that can return a pyvista UnstructuredGrid object (with or without results) we can then pass to dash_vtk! Time to layout the app.
I really like to use dash_bootstrap_components to layout my apps. It makes them look nice with minimal work. A big thank you to those developers!
This app is arranged in 2 main columns. The left has upload & UI components and the right is the view object for dash_vtk.
Lines 8, 19, & 31 have the upload components for nodes, elements, and element results. Line 44 has the dropdown that controls the contour when plotting results. That function was lifted with love from one of the dash_vtk examples. On line 52 we have the RangeSlider that allows the user to show only elements that fall within the..... range. The range is defined from 0.-1.0 and is relative to the maximum value in the results set. On line 68 we have a checklist that turns on and off element edges in the plot. I really like the switch mode from dash_bootstrap_components! I rarely seem to need more than 2 choices for any given option anyway, and its nice and compact.
Here (line 81) I have nested the dash_vtk.View object in a couple of Divs. It appears to be a bit finnicky (or not obvious to me) when working directly inside dash_bootstrap_component divs. Again, I leveraged the dash_vtk examples for div settings.
The callback is a bit...uncreative. There is certainly room for improvement and efficiency. Its basically driven by 1 large multioutput callback that leverages some advanced callback techniques to determine which callback fired and then checking the state of the uploads and selectively updating. I also use a dcc.Store object to store the condition if both nodes and elements have been uploaded. This basic logic allows the user to upload a new mesh file (nodes or elements) that in the condition there is already a mesh it clears the other file out. That way you don’t mix and match element and node files which would throw an error. Its not great, but it works.
Scenario 5 (mesh but no results) & 6 (mesh and results) are the interesting ones that use the function we setup earlier. In Scenario 5 we call the ns_export_to_uGrid function and then use the dash_vtk helper function dash_vtk.to_mesh_state to convert the uGrid pyvista/VTK object to something dash_vtk can use. We send this mesh_state object into a dash_vtk.Mesh object which in-turn is in a dash_vtk.GeometryReprsentation object that we send back to the dash_vtk.View object we have in the layout. I have also set some opacity and edgeVisibility properties specific to the mesh-only view.
In Scenario 6 we do much of the same thing as Scenario 5 and get a basic mesh_state1 object for just the mesh (no results). In addition to that we perform some thresholding to create a new grid thresh based on the RangeSlider values. Pyvista doesn’t like performing the relative threshold function with 0. or 1. in the range so we have some logic to choose how to handle 0. or 1. in the RangeSlider value passed to the callback (lines 59–69). Pyvista is really helping here by allowing us to threshold directly on the grid object. It took me a while to wrap my head around this after having some experience with vanilla VTK-python. When we send the new thresh object (which is also an UnstructuredGrid object) to the dash_vtk.to_mesh_state function we use the optional kwarg field_to_keep and give it the name of the results array we assigned back when we added the results to the uGrid object (with the boring name of my_array).
Now we have 2 mesh_state objects, 1 without results and 1 with. In lines 80 I create a dash_vtk.GeometryRepresentation object basically the same as in Scenario 5 but with the properties adjusted. Here we have edgeVisbility to False and the opacity way down to 0.15. This creates a ghost-like part so when you view really small subsets of the mesh you can still tell where the elements are (a few floating elements in space isn’t that helpful). For the thresholded mesh_state object we assign some settings for the colorMapPreset and edgeVisibility based on the user inputs (they get passed to the callback. We are sending back 2 dash_vtk.GeometryRepresentation objects for the same mesh to 1 dash_vtk.View object and it just works!
Good question! Other than spinning models in a new way you might be wondering why would you want to view 3D unstructured mesh objects in a custom web-app. If you are already using another tool for solving, it probably has some pre/post processing tools baked in right?
This is true, but there are a few reasons why you might want to do this (at least that I can think of):
You have compiled the Nastran-95 source code and have a sweet well-established structural solver, but no way to view the mesh or results.You have some custom criteria that you post process outside of the original solver and want to display (eg failure criteria for special materials)You perform superposition to combine unit loads results from the original solver, but do it outside of the original software and want to show worst case (element by element) results over multiple load cases in 1 view.
You have compiled the Nastran-95 source code and have a sweet well-established structural solver, but no way to view the mesh or results.
You have some custom criteria that you post process outside of the original solver and want to display (eg failure criteria for special materials)
You perform superposition to combine unit loads results from the original solver, but do it outside of the original software and want to show worst case (element by element) results over multiple load cases in 1 view.
dash_vtk is actively being worked. I built the original app using version 0.0.7 but noticed 0.0.9 is already out. Check it out, maybe more features are there already!
Well I hope you found this helpful and appreciate yo taking the time to read!