Skip to content
This repository has been archived by the owner on Feb 3, 2023. It is now read-only.

Speedup mesh_xsection more #6

Merged
merged 11 commits into from
Jun 26, 2017
Merged

Speedup mesh_xsection more #6

merged 11 commits into from
Jun 26, 2017

Conversation

algrs
Copy link
Contributor

@algrs algrs commented Jun 26, 2017

No description provided.

@algrs algrs requested review from sashaverma and jbwhite June 26, 2017 14:10

class Graph(object):
# A little utility class to build a symmetric graph
# A little utility class to build a symmetric graph and calcualate Euler Paths

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo: calcualate -> calculate 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I get for turning off the speel cheeker...

e0 = intersection_map.index(f[0], f[1])
e1 = intersection_map.index(f[0], f[2])
e2 = intersection_map.index(f[1], f[2])
if e0 is None:
Copy link

@maxdumas maxdumas Jun 26, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is operating on a mathematical certainty that exactly two of the edges of a face that intersects the plane will themselves intersect the plane. Can you make that explicit with a comment? The reasoning for this logic otherwise isn't immediately obvious from just the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can do


# 5: Find the paths for each component
components = []
components_closed = []
while len(G) > 0:
# This works because euler_path mutates the graph as it goes
path = euler_path(G.d)
# This works because pop_euler_path mutates the graph as it goes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment doesn't seem necessary anymore now that we have a more appropriate abstraction. 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And it's commented up in the function as well. Will remove.

Copy link

@maxdumas maxdumas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. The abstractions you added really make this function a lot more readable and less intimidating!

path.append(stack.pop())
return path
for f in fs:
# Since we're dealing with a triangle that intersects the plane, exactly of the edges

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exactly two of the edges

Copy link
Contributor Author

@algrs algrs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbwhite Some thoughts on profiling for you:

@@ -16,8 +16,8 @@ def __init__(self, point_on_plane, unit_normal):

unit_normal = vx.normalize(unit_normal)

self._r0 = point_on_plane
self._n = unit_normal
self._r0 = np.asarray(point_on_plane)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For those following along at home, moving the np.array from the getter to the constructor saved about half a million memory allocations. It's a 3 element array, but still those memory allocations added up.

if pt is not None:
if np.any(pt < np.min((a, b), axis=0)) or np.any(pt > np.max((a, b), axis=0)):
if any(np.logical_and(pt > a, pt > b)) or any(np.logical_and(pt < a, pt < b)):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an interesting example of a hotspot that profiling detected. We call this function half a million times, so tiny optimizations help a lot. Since numpy is optimized for large arrays, something like np.min is not as efficient as you'd expect when called many many times on tiny arrays.

Copy link
Contributor

@eerac eerac Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@algrs I get that we want things to be readable, but it's definitely more efficient to vectorize the calls to these. It appears to save a few seconds to use:

    def line_xsections(self, pts, rays):
        denoms = np.dot(rays, self.normal)
        denom_is_zero = denoms == 0
        denoms[denom_is_zero] = np.nan
        p = np.dot(self.reference_point - pts, self.normal) / denoms
        return np.vstack([p, p, p]).T * rays + pts, ~denom_is_zero

    def line_segment_xsections(self, a, b):
        pts, pt_is_valid = self.line_xsections(a, b-a)
        valid_pts = pts[pt_is_valid]
        pt_is_out_of_bounds = np.logical_or(np.any(np.logical_and(valid_pts > a, valid_pts > b), axis=1), 
                                            np.any(np.logical_and(valid_pts < a, valid_pts < b), axis=1))
        pt_is_valid[pt_is_valid] = ~pt_is_out_of_bounds
        pts[~pt_is_valid] = np.nan
        return pts, pt_is_valid

and then down below instead of the for loop over es you have

        pts, pt_is_valid = self.line_segment_xsections(m.v[es[:,0]], m.v[es[:,1]])
        valid_pts = pts[pt_is_valid]
        valid_es = es[pt_is_valid]
        for val, e in zip(valid_pts, valid_es):
            if not intersection_map.contains(e[0], e[1]):
                intersection_map.add(e[0], e[1], val)

And then on top of that I'd try to allow intersection_map to support batch operations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing wrong with vectorizing: #7

@@ -245,6 +242,11 @@ def mesh_xsection(self, m, neighborhood=None):
return Polyline(None)
return Polyline(np.vstack([x.v for x in components]), closed=True)

def mesh_intersecting_faces(self, m):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes when profiling in python it's useful to break a large function up into many small ones, so that the profiler can figure out which part of a large function has the slow code. Mostly I removed the subfunctions afterwards, because they didn't add clarity, but some of them turned out to be improvements and got left in.

def __init__(self):
self.d = {} # store indicies into self.values here, to make it easier to get inds or values
self.values = []
def _order(self, u, v):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another bit hotspot optimization. This method gets called a bazillion times, and it's faster to do a quick if statement than the more pythonic sorted((u, v)) or a use of min & max. This optimization saved a second and a half.

Copy link
Contributor

@eerac eerac Jun 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@algrs My hunch is that EdgeMap would be a good bit faster if it were built on top of a sparse matrix class. This is particularly true if edges are added in bulk using my proposed line_segment_xsections above.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I tried it. Using a sparse matrix for representing a graph is a huge gain when you're doing complex operations on a fixed graph. But here, a dictionary of sets implementation is faster, as you're closer to O(1) for insertion and deletion. There's room to play here, but it's maybe 3/4 of a second of slack; the lower hanging fruit is elsewhere.


intersection_map = EdgeMap()
for e in es:
if not intersection_map.contains(e[0], e[1]):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the initial implementation, we called line_segment_xsection 700000 times. By not duplicating calls (remember, most intersecting faces will share an edge or two with another intersecting face), we saved 250000 calls and almost 2 seconds. So sometimes your hotspot means "improve the function" and sometimes it means "call the function less often".

# Since we're dealing with a triangle that intersects the plane, exactly two of the edges
# will intersect (note that the only other sorts of "intersections" are one edge in
# plane or all three edges in plane, which won't be picked up by mesh_intersecting_faces).
e0 = intersection_map.index(f[0], f[1])
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wicked non-pythonic code here, but relying on the assumption that there are always three edges and two of them will always intersect lets us save several seconds over a more traditional list comprehension.

Copy link
Contributor

@jbwhite jbwhite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Appreciate the comments and explanations!

@algrs algrs merged commit a295a58 into master Jun 26, 2017
@algrs algrs deleted the 128_speedup_xsection branch June 26, 2017 17:07
paulmelnikow added a commit that referenced this pull request Sep 9, 2019
Tests actually were not running in Python 3. Fixing that revealed a dependency on baiji, which does not run in Python 3. This probably is not an easy dependency to remove, so drop Python 3 support for now.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants