Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Stores linestring's points in a kdtree. #52

Merged
merged 19 commits into from Feb 14, 2023

Conversation

francocipollone
Copy link
Contributor

@francocipollone francocipollone commented Feb 3, 2023

🎉 New feature

Related to #50 #47

Goes on top of #51

Summary

  • Linestring's points are stored in a kdtree to improve obtaining closest point when going from xyz to linestring's point.
  • The method using the kdtree is the maliput_sparse::geometry::GetClosestPoint
  • Consecuently, lane related methods relaying on that sees an improvement

Speed up

Calculating speed up on top of #51

Benchmark name cpu time[us] speed up map
francocipollone/segments_interval francocipollone/poc_line_string_kd_tree
MaliputOsmQueryTime/GetLane/0 0.026 0.023 1.13 SingleLane.osm
MaliputOsmQueryTime/GetLane/1 0.022 0.023 0.96 ArcLane.osm
MaliputOsmQueryTime/GetLane/2 0.023 0.023 1.00 ArcLaneDense.osm
MaliputOsmQueryTime/GetLane/3 0.024 0.023 1.04 Circuit.osm
MaliputOsmQueryTime/Length/0 0.003 0.003 1.00 SingleLane.osm
MaliputOsmQueryTime/Length/1 0.003 0.003 1.00 ArcLane.osm
MaliputOsmQueryTime/Length/2 0.003 0.003 1.00 ArcLaneDense.osm
MaliputOsmQueryTime/Length/3 0.003 0.003 1.00 Circuit.osm
MaliputOsmQueryTime/ToLanePosition/0 3.38 3.4 0.99 SingleLane.osm
MaliputOsmQueryTime/ToLanePosition/1 16.7 11.3 1.48 ArcLane.osm
MaliputOsmQueryTime/ToLanePosition/2 607 345 1.76 ArcLaneDense.osm
MaliputOsmQueryTime/ToLanePosition/3 37 25.4 1.46 Circuit.osm
MaliputOsmQueryTime/ToInertialPosition/0 1.13 1.15 0.98 SingleLane.osm
MaliputOsmQueryTime/ToInertialPosition/1 3.87 3.84 1.01 ArcLane.osm
MaliputOsmQueryTime/ToInertialPosition/2 117 115 1.02 ArcLaneDense.osm
MaliputOsmQueryTime/ToInertialPosition/3 7.63 7.59 1.01 Circuit.osm
MaliputOsmQueryTime/GetOrientation/0 0.948 0.975 0.97 SingleLane.osm
MaliputOsmQueryTime/GetOrientation/1 3.75 3.64 1.03 ArcLane.osm
MaliputOsmQueryTime/GetOrientation/2 119 113 1.05 ArcLaneDense.osm
MaliputOsmQueryTime/GetOrientation/3 7.68 7.46 1.03 Circuit.osm
MaliputOsmQueryTime/ToRoadPosition/0 8.68 8.99 0.97 SingleLane.osm
MaliputOsmQueryTime/ToRoadPosition/1 43.6 30.9 1.41 ArcLane.osm
MaliputOsmQueryTime/ToRoadPosition/2 1407 895 1.57 ArcLaneDense.osm
MaliputOsmQueryTime/ToRoadPosition/3 638 481 1.33 Circuit.osm
MaliputOsmQueryTime/FindRoadPositions/0 6.53 7.3 0.89 SingleLane.osm
MaliputOsmQueryTime/FindRoadPositions/1 34.2 23.7 1.44 ArcLane.osm
MaliputOsmQueryTime/FindRoadPositions/2 1160 730 1.59 ArcLaneDense.osm
MaliputOsmQueryTime/FindRoadPositions/3 588 438 1.34 Circuit.osm

Note: ToInertialPosition doesn't show an speed up because under the hood it relies on GetClosestPointUsing2dProjection which doesn't use the kdtree to obtain the closest point.

And what about the memory footprint?

Memory Usage[kb]
Maps francocipollone/segments_interval francocipollone/poc_line_string_kd_tree Speed up
ArcLaneDense 15988 17712 0.90

Test it

Checklist

  • Signed all commits for DCO
  • Added tests
  • Added example and/or tutorial
  • Updated documentation (as needed)
  • Updated migration guide (as needed)
  • Consider updating Python bindings (if it affects the public API)

Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
@francocipollone
Copy link
Contributor Author

This is a proof of concept. Some docstrings should be added and a bit of code polishing could be also necessary. @agalbachicar

@francocipollone
Copy link
Contributor Author

CI is failing until maliput/maliput#541 is merged.

Copy link
Contributor

@agalbachicar agalbachicar left a comment

Choose a reason for hiding this comment

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

Great work indeed. Some nits only.

include/maliput_sparse/geometry/line_string.h Outdated Show resolved Hide resolved
include/maliput_sparse/geometry/line_string.h Outdated Show resolved Hide resolved
include/maliput_sparse/geometry/line_string.h Outdated Show resolved Hide resolved
include/maliput_sparse/geometry/line_string.h Outdated Show resolved Hide resolved
Segments segments_{};
double length_{};
std::shared_ptr<KDTreeData> kdtree_;
Copy link
Contributor

Choose a reason for hiding this comment

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

Here we have a shared_ptr but we are calling make_unique.

Some questions:

  • Why shared_ptr?
  • Why KDTreeData? I would just call it KDTree, the class namespacing will apply.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I went for using shared_ptr because:

  • It allows us to use the default copy constructor and avoid having multiple kdtree's in memory. (if you copy the references for the kdtree, will go the same space in memory)
  • In LaneGeometry's constructor we are copying the linestring. If we use unique_ptr we should move it.

We could refactor this to use unique_ptr, probably the lane geometry class now will need to receive unique_ptr and the class won't have the possibility to be copied.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why KDTreeData? I would just call it KDTree, the class namespacing will apply.

Indeed

Copy link
Contributor

Choose a reason for hiding this comment

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

I am not fond of shared_ptr and I think it will play against us any potential parallelization we would like to use.
Also, I would prefer to pass raw pointers and consider a clone method so as to control copies as well. We don't want to copy points everywhere but because of that we also don't want to extend the life of the points where we don't want it to go.
Let's make a case for shared_ptr in which we don't really know where the kdtree will be used and how, and I'll be more than happy to add it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unique ptr

If going for a unique_ptr here I wouldn't add a copy constructor that regenerates the kdtree because that would be way non-performance for the class.

So I purpose switching to a unique ptr but let me tackle this in a follow-up PR because it affects so many classes here and in maliput_osm. LineString3d is spread across this package and at the parser level in maliput_osm:

  • LaneGeometry will be the class that will hold the ownership of the linestrings
  • As LineString has a non-sharable member, all LineString instances will be a std::unique_ptr instance, and all classes holding a LineString will also need to be a unique_ptr.
    (Note that heap-allocated memory isn't as fast as stack memory)

Shared ptr

Let's make a case for shared_ptr in which we don't really know where the kdtree will be used and how, and I'll be more than happy to add it.

maliput_sparse isn't a final and contained backend, but a tool where other backends, relying on other formats, will build their castle above.
With this in mind, it is a bit hard to think ways in which LineString class will be used. HOWEVER, the case that quickly comes to my mind is the case where you want to use the same LineString to be used by two adjacent lanes.

maliput_sparse::geometry::LineString3d left_lane_0 = FromMyFormatToMaliputSparse(weird_format_left_line_string);
maliput_sparse::geometry::LineString3d right_lane_1 = left_lane_0; // wuala, kdtree isn't built again.

@agalbachicar ptal

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry for not coming back here before.

So I purpose switching to a unique ptr but let me tackle this in a follow-up PR because it affects so many classes here and in maliput_osm

I am OK with deferring to a follow up PR the conversion to a unique_ptr.

With this in mind, it is a bit hard to think ways in which LineString class will be used. HOWEVER, the case that quickly comes to my mind is the case where you want to use the same LineString to be used by two adjacent lanes.

Then, it makes sense to have a view-type of the LineString that allows you to access it but not necessarily own the memory. It could be arranged somewhere else. For that to happen, we can change the builder to construct LineStrings at the Segment level, and then organize how we want them to be used. That means, we could derive LaneGeometries from the set of LineStrings or explicitly organize the pair of LineStrings at the moment of construction the LaneGeometry. This does not require a shared_ptr.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#54

src/geometry/utility/geometry.cc Outdated Show resolved Hide resolved
// If the idx is the first or last then obtain the first or last segment.
// Otherwise, obtain the segment that contains the nearest point.
MALIPUT_THROW_UNLESS(nearest_point.idx() != std::nullopt);
if (nearest_point.idx().value() == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: we should try to refactor this if-else-elseif possible. That should help to refactor this code and speed it up a bit more.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactor on what sense?

We can do something with a switch case like

  const IdxType idx_type = nearest_point.idx().value() == 0
                               ? IdxType::kStart
                               : (nearest_point.idx().value() == line_string.size() - 1 ? IdxType::kEnd : IdxType::Middle);
  switch case:
      IdxType::kStart:
         // 
      IdxType::kEnd:
         // 
      default:
         // 

However I am not sure if that is an improvement.
Do you want to avoid the two if conditions before the else? To avoid two comparison most of the time?

Copy link
Contributor

Choose a reason for hiding this comment

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

In each clause we are setting closest_segment and segment_closest_point_result. The first two cases have a very similar way of obtaining the closest_segment and pretty much the same for segment_closest_point_result.
The last two, require more computation but do the same.

I wonder if we could obtain the closest_segment by passing the index always and save the special cases at the beginning and the end for a general one. That would simplify the four cases into two.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did some minor adjustments at cf00b78

However there is a trade-off: More simple code vs more performant, and I think that with this last addition, I making the execution run some more comparisons than before.

Maybe I am missing some clear refactor

Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
…/poc_line_string_kd_tree

Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
@francocipollone francocipollone marked this pull request as ready for review February 6, 2023 19:48
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
@francocipollone
Copy link
Contributor Author

francocipollone commented Feb 6, 2023

To also improve ToLanePosition method(therefore ToRoadPosition and FindRoadPosition by consecuence), I am thinking to also store a 2d kdtree to be used by the GetClosestPointUsing2dProjection. This is probably a bit cumbersome, however, using GetClosestPoint (with kdtree3) vs GetClosestPointUsing2dProjection for the LanePosition calculation, the different is massive in performance.
(ToLanePosition from 350 us to 15us 🌊 )

I will think a bit about the possibilities.

EDITED: I think I could take advantage of the kdtree 3d as it is for improving this query. I will tackle this in a future PR

Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Copy link
Contributor

@agalbachicar agalbachicar left a comment

Choose a reason for hiding this comment

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

PTAL

// If the idx is the first or last then obtain the first or last segment.
// Otherwise, obtain the segment that contains the nearest point.
MALIPUT_THROW_UNLESS(nearest_point.idx() != std::nullopt);
if (nearest_point.idx().value() == 0) {
Copy link
Contributor

Choose a reason for hiding this comment

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

In each clause we are setting closest_segment and segment_closest_point_result. The first two cases have a very similar way of obtaining the closest_segment and pretty much the same for segment_closest_point_result.
The last two, require more computation but do the same.

I wonder if we could obtain the closest_segment by passing the index always and save the special cases at the beginning and the end for a general one. That would simplify the four cases into two.

@francocipollone francocipollone changed the title PoC: Stores linestring's points in a kdtree. Stores linestring's points in a kdtree. Feb 7, 2023
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Base automatically changed from francocipollone/segments_interval to main February 9, 2023 17:24
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Signed-off-by: Franco Cipollone <franco.c@ekumenlabs.com>
Copy link
Contributor

@agalbachicar agalbachicar left a comment

Choose a reason for hiding this comment

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

LGTM. Thanks for bearing with me longer than you should have.

This PR is good on its own, we should just polish some details in future TD tickets.

@francocipollone francocipollone merged commit ae4bda5 into main Feb 14, 2023
@francocipollone francocipollone deleted the francocipollone/poc_line_string_kd_tree branch February 14, 2023 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

None yet

2 participants