<< Hi, my name is Edwin and I’m a Grasshopper Expert at ShapeDiver. In this 2-part tutorial I will show you the role data trees play inside a Grasshopper model and how it directly translates into faster computation times and other benefits. >>
If you want to check out Part 1 of this tutorial, please click here.
Part 2: Simplification and Optimization
Using Data Trees For Optimization
At ShapeDiver, we focus on making Grasshopper models run online smoothly on the platform. For that purpose, we recommend using data trees in order to keep definitions as compact and optimized as possible.
In general, trees become very useful to keep items and lists organized before performing computationally intensive operations on them, such as intersections or boolean operations. The idea is always to avoid performing operations which are not necessary.
Simple operations, such as joining curves, can become really slow when no trees are used to sort curves and discard irrelevant candidates before effectively trying to join them. Similar issues happen with curve intersections, collision checks, finding the closest point to an object, etc.
Let’s look at two examples:
Example 1: Joining Curves
Joining curves with trees. Download this Grasshopper definition here.
In the example above there are pairs of open polygons that need to be closed by joining their ends with a line. This operation needs to join more than four thousand curves which take 374ms to compute if the tree is flattened, or just 47ms if the tree structure is kept as there are just four curves per branch to test.
Example 2: Collision Test
Region difference optimization. Download this Grasshopper definition here.
In this second example, a pattern is being created by using the Region Difference component. With the first method, the component takes 3.9 seconds as all circles and rectangles are taken at the same time to compute the difference. On the other hand, the second method just takes 132 milliseconds as it is first tested which circles actually collide with a particular rectangle, and just those ones are taken to compute the region difference by keeping them in different branches of a data tree.
Keeping Clean Data Flows
Finally, let's look at some tricks that will help you keep clean data flows. One of the main challenges with managing data is that Grasshopper doesn't always behave in the most explicit of ways. Here are some important points to consider, which always turn into deadly traps when they are ignored.
1) Every single piece of data is part of a tree.
Even if the Grasshopper canvas only shows you items and lists of items, each of them is implicitly assigned a tree branch. This often comes as a surprise when the time comes to merge different pieces of data together that end up in what looks like arbitrary separate tree branches. A good trick is to send items and lists to text panels, which will tell you about the branch they belong to.
2) Some components implicitly create new branches.
For example, offsetting a curve will add a level of depth in the input tree. This behaviour is logical once you understand it, but when you don’t know about the data structure of the inputs, the output paths can be puzzling.
If one tries to loft the initial curve with its offset, the operation fails because the curves are in different tree branches.
In this case, one can flatten the offset before merging the curves, making sure they are in the same branch. Then, the loft works as expected.
This solution is simple but not effective if we want to offset a list of curves instead of a single one. Flattening the lists before merging will destroy the tree structure that preserves the relation between each curve and its offset. A less destructive operation consists of simplifying the tree. However, this solution will also fail in some cases. Which leads us to the next point...
3) Simplifying a tree with a single branch is not possible.
Even though each item and list is assigned a tree branch, Grasshopper treats them as different types of data and therefore seems reluctant to simplify their branch when they are alone in the tree. It might seem like an edge case, but in practice one needs to be very careful to avoid breaking the definition in the case of trees with a single branch.
For this reason we have created a C# script which helps organize data paths in a complex tree. This C# script will trim small branches or add sub branches to the tree depending on the given Index number. The output tree will make sure all branches of the tree have the same depth. For example, if the required Index is equal to 1, the paths in this tree will be trimmed or extended to make sure all branches have a depth of two levels {i;j}. If some paths are not deep enough, an additional level of depth will be added.
In the example above, all branches are constrained to have a single depth {i} (Index=0). This produces a good solution for our curve offsetting problem, because it brings back all offsets in the same branch as their corresponding input curve.
4) Stream filters don’t work with trees.
One last tricky operation is to filter or pick and choose between different data tree paths as the Stream Filter component that GH has just accepts a single integer as Gate input. We have created another C# script to deal with this case. This alternative to the Stream Filter can take a data tree of gates (True/False) as input. If both streams have a matching data structure, the corresponding branch is selected from the correct stream depending on the gate value.
You can download the previous C# scripts here.
Conclusion
Even though data trees are complex to understand at the beginning, mastering them helps in the creation of efficient and compact GH definitions. Getting used to utilizing data trees in any possible situation instead of simplifying the data into lists and items will allow any definition to be reusable and expandable. At the end, all of these benefits will reflect at ShapeDiver when uploading models, making the experience more user friendly.