From 7d50e5afe0e6d54811a139e93f245aa18124b64f Mon Sep 17 00:00:00 2001 From: Patrick Niklaus Date: Sat, 21 Apr 2018 16:26:38 +0000 Subject: [PATCH] Refactor parameters to be selected at runtime --- .../alternative_path_mld.cpp | 201 ++++++++++-------- 1 file changed, 115 insertions(+), 86 deletions(-) diff --git a/src/engine/routing_algorithms/alternative_path_mld.cpp b/src/engine/routing_algorithms/alternative_path_mld.cpp index 349dc21e5..a55e05a06 100644 --- a/src/engine/routing_algorithms/alternative_path_mld.cpp +++ b/src/engine/routing_algorithms/alternative_path_mld.cpp @@ -34,22 +34,24 @@ using Facade = DataFacade; namespace { -// Alternative paths candidate via nodes are taken from overlapping search spaces. -// Overlapping by a third guarantees us taking candidate nodes "from the middle". -const constexpr auto kSearchSpaceOverlapFactor = 1.33; -// Unpack n-times more candidate paths to run high-quality checks on. -// Unpacking paths yields higher chance to find good alternatives but is also expensive. -const constexpr auto kAlternativesToUnpackFactor = 2.0; -// Alternative paths length requirement (stretch). -// At most 25% longer then the shortest path. -const constexpr auto kAtMostLongerBy = 0.25; -// Alternative paths similarity requirement (sharing). -// At least 15% different than the shortest path. -const constexpr auto kAtLeastDifferentBy = 0.85; -// Alternative paths are still reasonable around the via node candidate (local optimality). -// At least optimal around 10% sub-paths around the via node candidate. -const /*constexpr*/ auto kAtLeastOptimalAroundViaBy = 0.10; -// gcc 7.1 ICE ^ +struct Parameters +{ + // Alternative paths candidate via nodes are taken from overlapping search spaces. + // Overlapping by a third guarantees us taking candidate nodes "from the middle". + double kSearchSpaceOverlapFactor; + // Unpack n-times more candidate paths to run high-quality checks on. + // Unpacking paths yields higher chance to find good alternatives but is also expensive. + double kAlternativesToUnpackFactor; + // Alternative paths length requirement (stretch). + // At most 25% longer then the shortest path. + double kAtMostLongerBy; + // Alternative paths similarity requirement (sharing). + // At least 15% different than the shortest path. + double kAtLeastDifferentBy; + // Alternative paths are still reasonable around the via node candidate (local optimality). + // At least optimal around 10% sub-paths around the via node candidate. + double kAtLeastOptimalAroundViaBy; +}; // Represents a via middle node where forward (from s) and backward (from t) // search spaces overlap and the weight a path (made up of s,via and via,t) has. @@ -76,6 +78,54 @@ struct WeightedViaNodeUnpackedPath UnpackedEdges edges; }; +// Scale the maximum allowed weight increase based on its magnitude: +// - Shortest path 10 minutes, alternative 13 minutes => Factor of 0.30 ok +// - Shortest path 10 hours, alternative 13 hours => Factor of 0.30 unreasonable +double getLongerByFactorBasedOnDuration(const EdgeWeight duration) +{ + BOOST_ASSERT(duration != INVALID_EDGE_WEIGHT); + + // We only have generic weights here and no durations without unpacking. + // We also have restricted way penalties which are huge and will screw scaling here. + // + // Users can pass us generic weights not based on durations; we can't do anything about + // it here other than either generating too many or no alternatives in these cases. + // + // We scale the weights with a step function based on some rough guestimates, so that + // they match tens of minutes, in the low hours, tens of hours, etc. + + // Todo: instead of a piecewise constant function should this be a continuous function? + // At the moment there are "hard" jump edge cases when crossing the thresholds. + + const constexpr double DEFAULT_LONGER_BY = 0.5; + const constexpr auto minutes = 60.; + const constexpr auto hours = 60. * minutes; + + if (duration < EdgeWeight(10 * minutes)) + return DEFAULT_LONGER_BY * 1.20; + else if (duration < EdgeWeight(30 * minutes)) + return DEFAULT_LONGER_BY * 1.00; + else if (duration < EdgeWeight(1 * hours)) + return DEFAULT_LONGER_BY * 0.90; + else if (duration < EdgeWeight(3 * hours)) + return DEFAULT_LONGER_BY * 0.70; + else if (duration > EdgeWeight(10 * hours)) + return DEFAULT_LONGER_BY * 0.50; + + return DEFAULT_LONGER_BY; +} + +Parameters getDefaultParameters() +{ + Parameters parameters; + parameters.kSearchSpaceOverlapFactor = 1.33; + parameters.kAlternativesToUnpackFactor = 2.0; + parameters.kAtMostLongerBy = 0.25; + parameters.kAtLeastDifferentBy = 0.85; + parameters.kAtLeastOptimalAroundViaBy = 0.10; + return parameters; +} + // Filters candidates which are on not unique. // Returns an iterator to the uniquified range's new end. // Note: mutates the range in-place invalidating iterators. @@ -110,49 +160,13 @@ RandIt filterViaCandidatesByRoadImportance(RandIt first, RandIt last, const Faca return last; } -// Scale the maximum allowed weight increase based on its magnitude: -// - Shortest path 10 minutes, alternative 13 minutes => Factor of 0.30 ok -// - Shortest path 10 hours, alternative 13 hours => Factor of 0.30 unreasonable -double scaledAtMostLongerByFactorBasedOnDuration(EdgeWeight duration) -{ - BOOST_ASSERT(duration != INVALID_EDGE_WEIGHT); - - // We only have generic weights here and no durations without unpacking. - // We also have restricted way penalties which are huge and will screw scaling here. - // - // Users can pass us generic weights not based on durations; we can't do anything about - // it here other than either generating too many or no alternatives in these cases. - // - // We scale the weights with a step function based on some rough guestimates, so that - // they match tens of minutes, in the low hours, tens of hours, etc. - - // Todo: instead of a piecewise constant function should this be a continuous function? - // At the moment there are "hard" jump edge cases when crossing the thresholds. - - auto scaledAtMostLongerBy = kAtMostLongerBy; - - const constexpr auto minutes = 60.; - const constexpr auto hours = 60. * minutes; - - if (duration < EdgeWeight(10 * minutes)) - scaledAtMostLongerBy *= 1.20; - else if (duration < EdgeWeight(30 * minutes)) - scaledAtMostLongerBy *= 1.00; - else if (duration < EdgeWeight(1 * hours)) - scaledAtMostLongerBy *= 0.90; - else if (duration < EdgeWeight(3 * hours)) - scaledAtMostLongerBy *= 0.70; - else if (duration > EdgeWeight(10 * hours)) - scaledAtMostLongerBy *= 0.50; - - return scaledAtMostLongerBy; -} - // Filters candidates with much higher weight than the primary route. Mutates range in-place. // Returns an iterator to the filtered range's new end. template -RandIt -filterViaCandidatesByStretch(RandIt first, RandIt last, EdgeWeight weight, double weight_multiplier) +RandIt filterViaCandidatesByStretch(RandIt first, + RandIt last, + const EdgeWeight weight, + const Parameters ¶meters) { util::static_assert_iter_category(); util::static_assert_iter_value(); @@ -160,9 +174,7 @@ filterViaCandidatesByStretch(RandIt first, RandIt last, EdgeWeight weight, doubl // Assumes weight roughly corresponds to duration-ish. If this is not the case e.g. // because users are setting weight to be distance in the profiles, then we might // either generate more candidates than we have to or not enough. But is okay. - const auto scaled_at_most_longer_by = - scaledAtMostLongerByFactorBasedOnDuration(weight / weight_multiplier); - const auto stretch_weight_limit = (1. + scaled_at_most_longer_by) * weight; + const auto stretch_weight_limit = (1. + parameters.kAtMostLongerBy) * weight; const auto over_weight_limit = [=](const auto via) { return via.weight > stretch_weight_limit; @@ -198,7 +210,10 @@ filterViaCandidatesByViaNotOnPath(const WeightedViaNodePackedPath &path, RandIt // Filters packed paths with similar cells between each other. Mutates range in-place. // Returns an iterator to the filtered range's new end. template -RandIt filterPackedPathsByCellSharing(RandIt first, RandIt last, const Partition &partition) +RandIt filterPackedPathsByCellSharing(RandIt first, + RandIt last, + const Partition &partition, + const Parameters ¶meters) { util::static_assert_iter_category(); util::static_assert_iter_value(); @@ -226,14 +241,13 @@ RandIt filterPackedPathsByCellSharing(RandIt first, RandIt last, const Partition return last; std::unordered_set cells; - cells.reserve(size * (shortest_path.path.size() + 1) * (1. + kAtMostLongerBy)); + cells.reserve(size * (shortest_path.path.size() + 1) * (1.25)); cells.insert(get_cell(std::get<0>(shortest_path.path.front()))); for (const auto &edge : shortest_path.path) cells.insert(get_cell(std::get<1>(edge))); const auto over_sharing_limit = [&](const auto &packed) { - if (packed.path.empty()) { // don't remove routes with single-node (empty) path return false; @@ -253,7 +267,7 @@ RandIt filterPackedPathsByCellSharing(RandIt first, RandIt last, const Partition const auto sharing = 1. - difference; - if (sharing > kAtLeastDifferentBy) + if (sharing > parameters.kAtLeastDifferentBy) { return true; } @@ -277,7 +291,8 @@ RandIt filterPackedPathsByLocalOptimality(const WeightedViaNodePackedPath &path, const Heap &forward_heap, const Heap &reverse_heap, RandIt first, - RandIt last) + RandIt last, + const Parameters ¶meters) { util::static_assert_iter_category(); util::static_assert_iter_value(); @@ -376,7 +391,7 @@ RandIt filterPackedPathsByLocalOptimality(const WeightedViaNodePackedPath &path, const auto detour_length = forward_heap.GetKey(via) - forward_heap.GetKey(a) + reverse_heap.GetKey(via) - reverse_heap.GetKey(b); - return plateaux_length < kAtLeastOptimalAroundViaBy * detour_length; + return plateaux_length < parameters.kAtLeastOptimalAroundViaBy * detour_length; }; return std::remove_if(first, last, is_not_locally_optimal); @@ -384,7 +399,11 @@ RandIt filterPackedPathsByLocalOptimality(const WeightedViaNodePackedPath &path, // Filters unpacked paths compared to all other paths. Mutates range in-place. // Returns an iterator to the filtered range's new end. -template RandIt filterUnpackedPathsBySharing(RandIt first, RandIt last, const Facade& facade) +template +RandIt filterUnpackedPathsBySharing(RandIt first, + RandIt last, + const Facade &facade, + const Parameters ¶meters) { util::static_assert_iter_category(); util::static_assert_iter_value(); @@ -400,12 +419,11 @@ template RandIt filterUnpackedPathsBySharing(RandIt first, Ran return last; std::unordered_set edges; - edges.reserve(size * shortest_path.edges.size() * (1. + kAtMostLongerBy)); + edges.reserve(size * shortest_path.edges.size() * (1.25)); edges.insert(begin(shortest_path.edges), end(shortest_path.edges)); const auto over_sharing_limit = [&](const auto &unpacked) { - if (unpacked.edges.empty()) { // don't remove routes with single-node (empty) path return false; @@ -422,13 +440,14 @@ template RandIt filterUnpackedPathsBySharing(RandIt first, Ran return weight; }; - const auto shared_weight = std::accumulate(begin(unpacked.edges), end(unpacked.edges), 0, add_if_seen); + const auto shared_weight = + std::accumulate(begin(unpacked.edges), end(unpacked.edges), 0, add_if_seen); const auto sharing = shared_weight / static_cast(total_weight); BOOST_ASSERT(sharing >= 0.); BOOST_ASSERT(sharing <= 1.); - if (sharing > kAtLeastDifferentBy) + if (sharing > parameters.kAtLeastDifferentBy) { return true; } @@ -445,8 +464,10 @@ template RandIt filterUnpackedPathsBySharing(RandIt first, Ran // Filters annotated routes by stretch based on duration. Mutates range in-place. // Returns an iterator to the filtered range's new end. template -RandIt -filterAnnotatedRoutesByStretch(RandIt first, RandIt last, const InternalRouteResult &shortest_route) +RandIt filterAnnotatedRoutesByStretch(RandIt first, + RandIt last, + const InternalRouteResult &shortest_route, + const Parameters ¶meters) { util::static_assert_iter_category(); util::static_assert_iter_value(); @@ -454,9 +475,7 @@ filterAnnotatedRoutesByStretch(RandIt first, RandIt last, const InternalRouteRes BOOST_ASSERT(shortest_route.is_valid()); const auto shortest_route_duration = shortest_route.duration(); - const auto scaled_at_most_longer_by = - scaledAtMostLongerByFactorBasedOnDuration(shortest_route_duration); - const auto stretch_duration_limit = (1. + scaled_at_most_longer_by) * shortest_route_duration; + const auto stretch_duration_limit = (1. + parameters.kAtMostLongerBy) * shortest_route_duration; const auto over_duration_limit = [=](const auto &route) { return route.duration() > stretch_duration_limit; @@ -581,7 +600,8 @@ void unpackPackedPaths(InputIt first, inline std::vector makeCandidateVias(SearchEngineData &search_engine_data, const Facade &facade, - const PhantomNodes &phantom_node_pair) + const PhantomNodes &phantom_node_pair, + const Parameters ¶meters) { Heap &forward_heap = *search_engine_data.forward_heap_1; Heap &reverse_heap = *search_engine_data.reverse_heap_1; @@ -613,7 +633,7 @@ makeCandidateVias(SearchEngineData &search_engine_data, while (forward_heap.Size() + reverse_heap.Size() > 0) { if (shortest_path_weight != INVALID_EDGE_WEIGHT) - overlap_weight = shortest_path_weight * kSearchSpaceOverlapFactor; + overlap_weight = shortest_path_weight * parameters.kSearchSpaceOverlapFactor; // Termination criteria - when we have a shortest path this will guarantee for our overlap. const bool keep_going = forward_heap_min + reverse_heap_min < overlap_weight; @@ -678,7 +698,7 @@ makeCandidateVias(SearchEngineData &search_engine_data, return candidate_vias; } -} // anon. ns +} // namespace // Alternative Routes for MLD. // @@ -699,9 +719,11 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear const PhantomNodes &phantom_node_pair, unsigned number_of_alternatives) { + auto parameters = getDefaultParameters(); + const auto max_number_of_alternatives = number_of_alternatives; const auto max_number_of_alternatives_to_unpack = - kAlternativesToUnpackFactor * max_number_of_alternatives; + parameters.kAlternativesToUnpackFactor * max_number_of_alternatives; BOOST_ASSERT(max_number_of_alternatives > 0); BOOST_ASSERT(max_number_of_alternatives_to_unpack >= max_number_of_alternatives); @@ -715,7 +737,8 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear Heap &reverse_heap = *search_engine_data.reverse_heap_1; // Do forward and backward search, save search space overlap as via candidates. - auto candidate_vias = makeCandidateVias(search_engine_data, facade, phantom_node_pair); + auto candidate_vias = + makeCandidateVias(search_engine_data, facade, phantom_node_pair, parameters); const auto by_weight = [](const auto &lhs, const auto &rhs) { return lhs.weight < rhs.weight; }; auto shortest_path_via_it = @@ -738,6 +761,9 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear NodeID shortest_path_via = shortest_path_via_it->node; EdgeWeight shortest_path_weight = shortest_path_via_it->weight; + const double duration_estimation = shortest_path_weight / facade.GetWeightMultiplier(); + parameters.kAtMostLongerBy = getLongerByFactorBasedOnDuration(duration_estimation); + // Filters via candidate nodes with heuristics // Note: filter pipeline below only makes range smaller; no need to erase items @@ -746,8 +772,7 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear it = filterViaCandidatesByUniqueNodeIds(begin(candidate_vias), it); it = filterViaCandidatesByRoadImportance(begin(candidate_vias), it, facade); - it = filterViaCandidatesByStretch( - begin(candidate_vias), it, shortest_path_weight, facade.GetWeightMultiplier()); + it = filterViaCandidatesByStretch(begin(candidate_vias), it, shortest_path_weight, parameters); // Pre-rank by weight; sharing filtering below then discards by similarity. std::sort(begin(candidate_vias), it, [](const auto lhs, const auto rhs) { @@ -791,10 +816,11 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear forward_heap, // paths for s, via reverse_heap, // paths for via, t begin(weighted_packed_paths) + 1, - alternative_paths_last); + alternative_paths_last, + parameters); alternative_paths_last = filterPackedPathsByCellSharing( - begin(weighted_packed_paths), alternative_paths_last, partition); + begin(weighted_packed_paths), alternative_paths_last, partition, parameters); BOOST_ASSERT(weighted_packed_paths.size() >= 1); @@ -824,7 +850,8 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear auto unpacked_paths_last = end(unpacked_paths); - unpacked_paths_last = filterUnpackedPathsBySharing(begin(unpacked_paths), end(unpacked_paths), facade); + unpacked_paths_last = filterUnpackedPathsBySharing( + begin(unpacked_paths), end(unpacked_paths), facade, parameters); const auto unpacked_paths_first = begin(unpacked_paths); const auto number_of_unpacked_paths = @@ -858,7 +885,9 @@ InternalManyRoutesResult alternativePathSearch(SearchEngineData &sear if (routes.size() > 1) { - routes_last = filterAnnotatedRoutesByStretch(routes_first + 1, routes_last, *routes_first); + parameters.kAtMostLongerBy = getLongerByFactorBasedOnDuration(routes_first->duration()); + routes_last = filterAnnotatedRoutesByStretch( + routes_first + 1, routes_last, *routes_first, parameters); routes.erase(routes_last, end(routes)); }