LCOV - code coverage report
Current view: top level - src/utils/router - CCHRouter.h (source / functions) Coverage Total Hit
Test: lcov.info Lines: 91.2 % 114 104
Test Date: 2026-10-12 15:53:19 Functions: 85.7 % 28 24

            Line data    Source code
       1              : /****************************************************************************/
       2              : // Eclipse SUMO, Simulation of Urban MObility; see https://eclipse.dev/sumo
       3              : // Copyright (C) 2001-2026 German Aerospace Center (DLR) and others.
       4              : // This program and the accompanying materials are made available under the
       5              : // terms of the Eclipse Public License 2.0 which is available at
       6              : // https://www.eclipse.org/legal/epl-2.0/
       7              : // This Source Code may also be made available under the following Secondary
       8              : // Licenses when the conditions for such availability set forth in the Eclipse
       9              : // Public License 2.0 are satisfied: GNU General Public License, version 2
      10              : // or later which is available at
      11              : // https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html
      12              : // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-or-later
      13              : /****************************************************************************/
      14              : /// @file    CCHRouter.h
      15              : /// @author  Pranav Sateesh
      16              : /// @date    2026
      17              : ///
      18              : // A SUMOAbstractRouter that answers queries via a RoutingKit Customizable
      19              : // Contraction Hierarchy over the edge graph. The metric is re-customized
      20              : // out-of-band (MSRoutingEngine::adaptEdgeEfforts) and published as a
      21              : // shared_ptr snapshot; each query snapshots the current metric, so a
      22              : // publish that lands mid-query is safe (double buffer).
      23              : //
      24              : // Following the AStarRouter<E, V, M> lookup-table pattern, the graph-mapping
      25              : // class is the GRAPH template parameter (the simulation supplies its concrete
      26              : // mapper, e.g. microsim's CCHGraph), so this header carries no simulation
      27              : // dependencies. GRAPH must provide:
      28              : //   unsigned nodeOf(const E*)                          (INVALID_NODE if not a node)
      29              : //   const E* edgeOf(unsigned node)
      30              : //   const std::vector<unsigned>& tazSources(const E*)  (entry-edge node sets)
      31              : //   const std::vector<unsigned>& tazSinks(const E*)    (exit-edge node sets)
      32              : //   void expandNodePath(const std::vector<unsigned>&, std::vector<const E*>&)
      33              : //   static const unsigned INVALID_NODE
      34              : //
      35              : // Every vehicle class routes on CCH via its own published metric. Closures are
      36              : // permission changes the per-class metric encodes as inf_weight once they have
      37              : // been customized in; the simulation's metric provider hands out nullptr while
      38              : // a permission flip is still pending, so those queries route exactly. The
      39              : // embedded A* fallback is used when a class has no (fresh) metric, when the
      40              : // vehicle routes on the ORIGINAL permissions (ignoreTransientPermissions --
      41              : // the shared metric bakes in the live ones), or when a prohibition is NOT
      42              : // expressible as a live permission closure (an arbitrary per-query
      43              : // prohibition, which no shared CCH metric can represent) -- see compute() /
      44              : // prohibitionsCoveredByMetric().
      45              : /****************************************************************************/
      46              : #pragma once
      47              : #include <config.h>
      48              : 
      49              : 
      50              : #include <memory>
      51              : #include <vector>
      52              : #include <cmath>
      53              : #include <utils/common/Named.h>
      54              : #include <utils/common/SUMOTime.h>
      55              : #include <utils/common/MsgHandler.h>
      56              : #include <utils/common/SUMOVehicleClass.h>
      57              : #include <utils/router/SUMOAbstractRouter.h>
      58              : #ifdef __GNUC__
      59              : #pragma GCC diagnostic push
      60              : #pragma GCC diagnostic ignored "-Wunused-parameter"
      61              : #endif
      62              : #ifdef _MSC_VER
      63              : #pragma warning(push)
      64              : #pragma warning(disable: 4267 4458)
      65              : #endif
      66              : #include <routingkit/customizable_contraction_hierarchy.h>
      67              : #ifdef _MSC_VER
      68              : #pragma warning(pop)
      69              : #endif
      70              : #ifdef __GNUC__
      71              : #pragma GCC diagnostic pop
      72              : #endif
      73              : 
      74              : 
      75              : // ===========================================================================
      76              : // class definitions
      77              : // ===========================================================================
      78              : /**
      79              :  * @class CCHRouter
      80              :  * @brief Contraction-hierarchy router over the edge graph mapped by GRAPH.
      81              :  */
      82              : template<class E, class V, class GRAPH>
      83              : class CCHRouter : public SUMOAbstractRouter<E, V> {
      84              : public:
      85              :     typedef const RoutingKit::CustomizableContractionHierarchyMetric* MetricPtr;
      86              :     /// @brief supplies the metric for a vehicle class at a query time, or
      87              :     /// nullptr => fall back. The simulation ignores time and vehicle (its
      88              :     /// single metric tracks the live speeds); duarouter keys one metric per
      89              :     /// (class, restriction profile, weight period) with them.
      90              :     typedef MetricPtr(*MetricProvider)(SUMOVehicleClass, SUMOTime, const V*);
      91              :     /// @brief the end of the weight period containing the given time
      92              :     /// (SUMOTime_MAX = weights are static). Lets compute() detect queries
      93              :     /// whose trip crosses into the next period and re-query on that
      94              :     /// period's metric (see the boundary handling in compute()).
      95              :     typedef SUMOTime(*PeriodEnd)(SUMOTime);
      96              :     /// @brief the host's efforts changed behind the metrics and it reset the
      97              :     /// router (SUMOAbstractRouter::reset -- marouter does so after every
      98              :     /// assignment iteration, which is where CH rebuilds its hierarchy): the
      99              :     /// metric store should re-customize before the next query
     100              :     typedef void (*ResetHook)(const V*);
     101              :     typedef typename SUMOAbstractRouter<E, V>::Operation Operation;
     102              :     typedef typename SUMOAbstractRouter<E, V>::Prohibitions Prohibitions;
     103              : 
     104              :     /** @brief Constructor.
     105              :      * @param[in] graph     the shared immutable CCH topology mapper (not owned)
     106              :      * @param[in] provider  static accessor for the published metric snapshot
     107              :      * @param[in] operation the effort callback (same one A-star and CH use)
     108              :      * @param[in] fallback  embedded router for non-CCH cases (OWNED)
     109              :      * @param[in] periodEnd weight-period boundary hook (nullptr = static weights)
     110              :      * @param[in] onReset   hook run by reset() (nullptr = nobody to notify)
     111              :      */
     112          894 :     CCHRouter(const GRAPH* graph, MetricProvider provider,
     113              :               Operation operation, const bool unbuildIsWarning,
     114              :               SUMOAbstractRouter<E, V>* fallback,
     115              :               PeriodEnd periodEnd = nullptr, ResetHook onReset = nullptr) :
     116              :         SUMOAbstractRouter<E, V>("CCHRouter", unbuildIsWarning, operation, nullptr, false, false),
     117         1788 :         myGraph(graph), myMetricProvider(provider), myFallback(fallback),
     118         1788 :         myPeriodEnd(periodEnd), myOnReset(onReset), myProhibitionActive(false) {
     119          894 :     }
     120              : 
     121              :     /// @brief clone constructor: share graph + provider, clone the fallback, fresh query scratch
     122           19 :     CCHRouter(CCHRouter* other) :
     123              :         SUMOAbstractRouter<E, V>(other),
     124           19 :         myGraph(other->myGraph), myMetricProvider(other->myMetricProvider),
     125           38 :         myFallback(other->myFallback->clone()), myPeriodEnd(other->myPeriodEnd),
     126           19 :         myOnReset(other->myOnReset),
     127           19 :         myProhibitionActive(other->myProhibitionActive), myProhibited(other->myProhibited) {
     128           19 :     }
     129              : 
     130         1826 :     virtual ~CCHRouter() {
     131          913 :         delete myFallback;
     132         2739 :     }
     133              : 
     134           19 :     virtual SUMOAbstractRouter<E, V>* clone() {
     135           19 :         return new CCHRouter<E, V, GRAPH>(this);
     136              :     }
     137              : 
     138              :     /// @brief the host's efforts changed (see ResetHook): notify the metric
     139              :     /// store and reset the fallback's caches. Must not run while another
     140              :     /// clone is mid-query; marouter resets once its worker threads have
     141              :     /// finished the iteration.
     142           48 :     virtual void reset(const V* const vehicle) {
     143           48 :         if (myOnReset != nullptr) {
     144           48 :             myOnReset(vehicle);
     145              :         }
     146           48 :         myFallback->reset(vehicle);
     147           48 :     }
     148              : 
     149        80504 :     bool compute(const E* from, const E* to, const V* const vehicle,
     150              :                  SUMOTime msTime, std::vector<const E*>& into, bool silent = false) {
     151              :         // A CCH query can only avoid an edge the metric already encodes as inf:
     152              :         // RoutingKit's query has no per-arc blacklist, and a query-time skip is
     153              :         // unsound because shortcut arcs bake in shortest paths THROUGH the edge
     154              :         // (SUMO's own CHRouter refuses prohibitions for the same reason). So we
     155              :         // run CCH when the metric can express the request and fall back to the
     156              :         // exact A* router otherwise: (a) no metric for this class, or (b) an
     157              :         // active prohibition that is NOT already a live permission-closure this
     158              :         // class's metric encodes (prohibitionsCoveredByMetric).
     159        80504 :         const SUMOVehicleClass vClass = vehicle == nullptr ? SVC_PASSENGER : vehicle->getVClass();
     160              :         // A vehicle that routes on the ORIGINAL permissions (routing mode bit
     161              :         // ROUTING_MODE_IGNORE_TRANSIENT_PERMISSIONS) cannot use the shared
     162              :         // metric, which bakes in the LIVE permissions, transient closures
     163              :         // included -- route it exactly, like Dijkstra/A* do natively.
     164        75691 :         if (vehicle != nullptr && vehicle->ignoreTransientPermissions()) {
     165          381 :             return myFallback->compute(from, to, vehicle, msTime, into, silent);
     166              :         }
     167        80123 :         MetricPtr metric = myMetricProvider(vClass, msTime, vehicle);
     168        80123 :         if (metric == nullptr || (myProhibitionActive && !prohibitionsCoveredByMetric(vClass))) {
     169          192 :             return myFallback->compute(from, to, vehicle, msTime, into, silent);
     170              :         }
     171              :         // Endpoints -> node SETS: a district star connector expands to its
     172              :         // member edges (phantom-node seeding); everything that is a graph
     173              :         // node -- including legacy function="connector" net edges -- routes
     174              :         // as a single node. TAZ member edges are the class-union set, so
     175              :         // filter to edges this class may enter.
     176        80184 :         const bool fromTaz = from->isTazConnector() && myGraph->nodeOf(from) == GRAPH::INVALID_NODE;
     177        80199 :         const bool toTaz = to->isTazConnector() && myGraph->nodeOf(to) == GRAPH::INVALID_NODE;
     178              :         // Mirror the exact routers' endpoint rejection (AStarRouter::compute):
     179              :         // an endpoint the vehicle may not use fails loudly instead of quietly
     180              :         // seeding a query on it. The metric masks only arcs BETWEEN nodes --
     181              :         // the source edge's own closure never enters it (its effort is added
     182              :         // as a plain source seed), so without this check a query from a
     183              :         // hard-closed edge would "succeed". TAZ members are filtered below.
     184        79931 :         if (vehicle != nullptr) {
     185        79931 :             if (!fromTaz && from->prohibits(vehicle)) {
     186            6 :                 if (!silent && this->myErrorMsgHandler != nullptr) {
     187           18 :                     this->myErrorMsgHandler->inform("Vehicle '" + Named::getIDSecure(vehicle) + "' is not allowed on source edge '" + from->getID() + "'.");
     188              :                 }
     189            6 :                 return false;
     190              :             }
     191        79925 :             if (!toTaz && to->prohibits(vehicle)) {
     192            5 :                 if (!silent && this->myErrorMsgHandler != nullptr) {
     193            6 :                     this->myErrorMsgHandler->inform("Vehicle '" + Named::getIDSecure(vehicle) + "' is not allowed on destination edge '" + to->getID() + "'.");
     194              :                 }
     195            5 :                 return false;
     196              :             }
     197              :         }
     198         4808 :         std::vector<unsigned> srcBuf, tgtBuf;
     199              :         const std::vector<unsigned>* sources;
     200              :         const std::vector<unsigned>* targets;
     201        79920 :         if (fromTaz) {
     202          660 :             for (const unsigned m : myGraph->tazSources(from)) {
     203          407 :                 if ((myGraph->edgeOf(m)->getPermissions() & vClass) != 0) {
     204          407 :                     srcBuf.push_back(m);
     205              :                 }
     206              :             }
     207              :             sources = &srcBuf;
     208              :         } else {
     209        79667 :             const unsigned s = myGraph->nodeOf(from);
     210        79667 :             if (s != GRAPH::INVALID_NODE) {
     211        79667 :                 srcBuf.push_back(s);
     212              :             }
     213              :             sources = &srcBuf;
     214              :         }
     215        79920 :         if (toTaz) {
     216          702 :             for (const unsigned m : myGraph->tazSinks(to)) {
     217          434 :                 if ((myGraph->edgeOf(m)->getPermissions() & vClass) != 0) {
     218          434 :                     tgtBuf.push_back(m);
     219              :                 }
     220              :             }
     221              :             targets = &tgtBuf;
     222              :         } else {
     223        79652 :             const unsigned t = myGraph->nodeOf(to);
     224        79652 :             if (t != GRAPH::INVALID_NODE) {
     225        79652 :                 tgtBuf.push_back(t);
     226              :             }
     227              :             targets = &tgtBuf;
     228              :         }
     229        79920 :         if (sources->empty() || targets->empty()) {
     230              :             // unmappable endpoint (e.g. internal edge, or a TAZ with no member
     231              :             // in this class's graph) -> exact fallback.
     232            0 :             return myFallback->compute(from, to, vehicle, msTime, into, silent);
     233              :         }
     234              : 
     235              :         this->startQuery();
     236        79920 :         const double t = STEPS2TIME(msTime);
     237        79920 :         if (!runQuery(metric, *sources, *targets, vehicle, t)) {
     238              :             this->endQuery(0);
     239          166 :             if (!silent && this->myErrorMsgHandler != nullptr) {
     240          284 :                 this->myErrorMsgHandler->informf(TL("No connection between edge '%' and edge '%' found."),
     241              :                                                  from->getID(), to->getID());
     242              :             }
     243          166 :             return false;
     244              :         }
     245         4690 :         std::vector<const E*> path;
     246        79754 :         buildPath(from, to, fromTaz, toTaz, path);
     247        79754 :         int visited = (int)path.size();
     248              :         // Weight-period boundary: the metric of the depart period is exact as
     249              :         // long as the trip finishes inside that period (efforts are constant
     250              :         // within it, and no time-propagated optimum can cross the boundary if
     251              :         // this trip does not: any path entering the next period costs at
     252              :         // least the remaining period, which already exceeds this result).
     253              :         // When the trip does cross, ALSO query the next period's metric and
     254              :         // keep whichever path is cheaper under the true time-propagating
     255              :         // walk (recomputeCosts). Exact for the common single-crossing case
     256              :         // where the optimum follows one period's structure; trips spanning
     257              :         // several periods keep the better of the two candidates.
     258        79754 :         if (myPeriodEnd != nullptr && myQuery.get_distance() > 0) {
     259          788 :             const SUMOTime boundary = myPeriodEnd(msTime);
     260              :             if (boundary != SUMOTime_MAX
     261          788 :                     && msTime + TIME2STEPS((double)myQuery.get_distance() / 100.) >= boundary) {
     262            0 :                 MetricPtr altMetric = myMetricProvider(vClass, boundary, vehicle);
     263            0 :                 if (altMetric != nullptr && altMetric != metric
     264            0 :                         && runQuery(altMetric, *sources, *targets, vehicle, t)) {
     265            0 :                     std::vector<const E*> altPath;
     266            0 :                     buildPath(from, to, fromTaz, toTaz, altPath);
     267            0 :                     visited += (int)altPath.size();
     268            0 :                     if (this->recomputeCosts(altPath, vehicle, msTime)
     269            0 :                             < this->recomputeCosts(path, vehicle, msTime)) {
     270              :                         path.swap(altPath);
     271              :                     }
     272            0 :                 }
     273              :             }
     274              :         }
     275        79754 :         into.insert(into.end(), path.begin(), path.end());
     276              :         this->endQuery(visited);
     277              :         return true;
     278        79920 :     }
     279              : 
     280              :     /// @brief prohibitions: a closure that is already a live permission change
     281              :     /// is served by the per-class metric (CCH); anything else routes via the
     282              :     /// exact fallback. Keep our own copy of the set so compute() can classify
     283              :     /// each query -- deliberately NOT delegating to the base implementation,
     284              :     /// which writes per-edge prohibition state into myEdgeInfos that this
     285              :     /// router never populates.
     286        27291 :     void prohibit(const Prohibitions& toProhibit) {
     287              :         myProhibited = toProhibit;
     288        27291 :         myProhibitionActive = !toProhibit.empty();
     289        27291 :         myFallback->prohibit(toProhibit);
     290        27291 :     }
     291              : 
     292           10 :     bool supportsProhibitions() const {
     293           10 :         return true;  // handled by delegating to the fallback
     294              :     }
     295              : 
     296          161 :     void setBulkMode(const bool mode) {
     297              :         SUMOAbstractRouter<E, V>::setBulkMode(mode);
     298          161 :         myFallback->setBulkMode(mode);
     299          161 :     }
     300              : 
     301              :     /// @brief keep the fallback reporting through the same handler (duarouter
     302              :     /// swaps handlers around route repair; CHRouterWrapper propagates likewise)
     303         4727 :     void setMsgHandler(MsgHandler* const errorMsgHandler) {
     304              :         SUMOAbstractRouter<E, V>::setMsgHandler(errorMsgHandler);
     305         4727 :         myFallback->setMsgHandler(errorMsgHandler);
     306         4727 :     }
     307              : 
     308              : private:
     309              :     /** @brief Bind the query to @p metric (rebinding only on change), seed
     310              :      * all sources and targets and run it; true iff a connection was found.
     311              :      *
     312              :      * Source seed = the entry edge's OWN centisecond effort. The arc-weight
     313              :      * convention (arc weight = effort of destination) omits the source
     314              :      * edge's effort, so choosing the cheapest zone-entry among members
     315              :      * requires adding it back per source -- otherwise the argmin ignores
     316              :      * how expensive each entry edge is and picks a different entry than A*.
     317              :      * Targets seed 0: the arc INTO a target already carries its effort. */
     318        79920 :     bool runQuery(MetricPtr metric, const std::vector<unsigned>& sources,
     319              :                   const std::vector<unsigned>& targets, const V* const vehicle, double t) {
     320        79920 :         if (metric != myBoundMetric) {
     321         9098 :             myQuery.reset(*metric);
     322         9098 :             myBoundMetric = metric;
     323              :         } else {
     324        70822 :             myQuery.reset();
     325              :         }
     326              :         const double bigEff = (double)(RoutingKit::inf_weight - 1) / 100.0;
     327       159994 :         for (const unsigned s : sources) {
     328        80074 :             const double eff = this->getEffort(myGraph->edgeOf(s), vehicle, t);
     329        80074 :             const unsigned d = eff < bigEff ? (unsigned)llround(eff * 100.0) : RoutingKit::inf_weight - 1;
     330        80074 :             myQuery.add_source(s, d);
     331              :         }
     332       160006 :         for (const unsigned tt : targets) {
     333        80086 :             myQuery.add_target(tt, 0);
     334              :         }
     335        79920 :         myQuery.run();
     336        79920 :         return myQuery.get_distance() < RoutingKit::inf_weight;
     337              :     }
     338              : 
     339              :     /// @brief expand the last query's node path, bracketing it with the TAZ
     340              :     /// connectors where the endpoints are zones
     341        79754 :     void buildPath(const E* from, const E* to, bool fromTaz, bool toTaz,
     342              :                    std::vector<const E*>& path) {
     343        79754 :         const std::vector<unsigned> nodePath = myQuery.get_node_path();
     344        79754 :         if (fromTaz) {
     345          249 :             path.push_back(from);
     346              :         }
     347        79754 :         myGraph->expandNodePath(nodePath, path);
     348        79754 :         if (toTaz) {
     349          264 :             path.push_back(to);
     350              :         }
     351        79754 :     }
     352              : 
     353              :     /** @brief Can the per-class CCH metric already express every edge this
     354              :      * prohibition set forbids for @p vClass? True iff each forbidden edge is
     355              :      * ALSO closed for the class in the live network (getPermissions()) -- the
     356              :      * exact condition fillInputWeights() turns into inf_weight. A prohibited
     357              :      * edge that still permits the class is an arbitrary per-query prohibition
     358              :      * no shared metric can encode, so the caller must use the exact fallback.
     359              :      * (For time-bounded closures A* itself avoids the edge via this same live
     360              :      * permission signal -- SUMOAbstractRouter::isProhibited -- not the
     361              :      * per-query branch, so this is faithful to A*'s own behavior.)
     362              :      */
     363          225 :     bool prohibitionsCoveredByMetric(SUMOVehicleClass vClass) const {
     364          306 :         for (const auto& item : myProhibited) {
     365          231 :             if ((item.second.permissions & vClass) != vClass          // forbids vClass here...
     366          231 :                     && (item.first->getPermissions() & vClass) != 0) {  // ...but edge still permits it live
     367              :                 return false;
     368              :             }
     369              :         }
     370              :         return true;
     371              :     }
     372              : 
     373              : private:
     374              :     const GRAPH* myGraph;                    // shared, immutable, not owned
     375              :     MetricProvider myMetricProvider;
     376              :     RoutingKit::CustomizableContractionHierarchyQuery myQuery;  // per-clone scratch
     377              :     MetricPtr myBoundMetric = nullptr;       // metric myQuery is currently bound to
     378              :     SUMOAbstractRouter<E, V>* myFallback;    // owned
     379              :     PeriodEnd myPeriodEnd;                   // may be nullptr (static weights)
     380              :     ResetHook myOnReset;                     // may be nullptr (nobody to notify)
     381              :     bool myProhibitionActive;
     382              :     Prohibitions myProhibited;               // last set installed via prohibit()
     383              : 
     384              : private:
     385              :     CCHRouter& operator=(const CCHRouter&) = delete;
     386              : };
        

Generated by: LCOV version 2.0-1