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 CCHMetricFamily.h
15 : /// @author Pranav Sateesh
16 : /// @date 2026
17 : ///
18 : // A family of per-vehicle-type CCH metrics over one shared CCHGraph -- the
19 : // MetricProvider machinery behind CCHRouter, shared between the simulation
20 : // and duarouter the same way CCHGraph shares the topology.
21 : //
22 : // Keying by the vehicle TYPE (rather than the class) makes everything the
23 : // effort function reads from the vehicle exact per metric: the type's
24 : // maximum speed, per-class edge speed restrictions, routing preferences,
25 : // the bicycle speed table and one frozen weights.random-factor realization;
26 : // only individual speed-factor draws within one type share a metric (the
27 : // same approximation the CH family makes with its per-class prototype
28 : // vehicles, but at finer granularity).
29 : //
30 : // Two modes:
31 : //
32 : // STATIC -- weights never change while a (type, weight period) pair is in
33 : // use. Metrics are customized lazily on the first query of a pair under a
34 : // mutex and cached (duarouter, marouter and the simulation's free-flow
35 : // routers). When the efforts do change between queries -- a runtime
36 : // permission flip, marouter's travel times after an assignment iteration
37 : // -- the host flags it (flagStale) and every cached metric is refilled
38 : // and re-customized on the next query.
39 : //
40 : // LIVE -- weights track an adaptive speed table (the rerouting device).
41 : // Metrics are double-buffered: worker threads acquire-load the published
42 : // front while the owner refills and customizes the back at its barrier
43 : // and flips, so queries never observe a metric mid-customization. The
44 : // customize is sparse (RoutingKit partial customization over the edges
45 : // whose effort actually moved, behind a configurable deadband) with
46 : // wholesale refills after permission flips. Staleness is bounded to one
47 : // barrier -- CH's weightPeriod semantics: once any metric exists,
48 : // customize(now) must run at every barrier where an effort moved (see
49 : // atBarrier), and an epoch pair guards the bound as an invariant.
50 : //
51 : // The family is intentionally ignorant of WHEN to customize -- only the
52 : // owner knows when no query is in flight (the simulation's adaptation
53 : // barrier, duarouter's mutex) -- and of HOW reference vehicles are built
54 : // (injected factory; V construction is host-specific).
55 : /****************************************************************************/
56 : #pragma once
57 : #include <config.h>
58 :
59 : #include <atomic>
60 : #include <cmath>
61 : #include <limits>
62 : #include <map>
63 : #include <memory>
64 : #include <mutex>
65 : #include <utility>
66 : #include <vector>
67 : #include <utils/common/SUMOTime.h>
68 : #include <utils/common/SUMOVehicleClass.h>
69 : #include <utils/router/CCHGraph.h>
70 :
71 :
72 : // ===========================================================================
73 : // class definitions
74 : // ===========================================================================
75 : /**
76 : * @class CCHMetricFamily
77 : * @brief per-vehicle-type CCH metric store over a shared CCHGraph
78 : *
79 : * @tparam E the edge type (MSEdge / ROEdge)
80 : * @tparam V the vehicle type of the effort callback (SUMOVehicle / ROVehicle)
81 : * @tparam K the vehicle-type key (MSVehicleType / SUMOVTypeParameter); used
82 : * only as a stable map key, never dereferenced
83 : */
84 : template<class E, class V, class K>
85 : class CCHMetricFamily {
86 : public:
87 : typedef CCHGraph<E, V> Graph;
88 : typedef typename Graph::EffortOperation EffortOperation;
89 : typedef const RoutingKit::CustomizableContractionHierarchyMetric* MetricPtr;
90 : /// @brief builds an OWNED effort-reference vehicle for a type: never
91 : /// registered, counted or inserted -- it exists only so every fill of
92 : /// that type's metric evaluates the same reference (deterministic speed
93 : /// factor and frozen random realization are the factory's business).
94 : /// The second argument is the ensemble slot (see the LIVE constructor);
95 : /// slot 0 must reproduce the pre-ensemble reference exactly, further
96 : /// slots give distinct frozen weights.random-factor realizations
97 : /// (typically by varying the reference vehicle's id, which seeds them)
98 : typedef V* (*RefVehicleFactory)(const K*, int);
99 : /// @brief post-fill hook on the input weights (STATIC mode), e.g.
100 : /// duarouter masking the edges that restrict a type to inf_weight
101 : typedef void (*WeightPatch)(const Graph*, const V*, std::vector<unsigned>&);
102 :
103 : /** @brief construct a STATIC family (lazy build per (type, period))
104 : * @param[in] graph the shared topology
105 : * @param[in] fillEffort the effort the weights are computed from
106 : * @param[in] begin begin of the weight-period grid
107 : * @param[in] weightPeriod period length; SUMOTime_MAX = a single period
108 : * @param[in] factory reference-vehicle factory; nullptr = fill with the
109 : * QUERYING vehicle (each pair is customized exactly once, so the first
110 : * vehicle of a type is that type's reference -- duarouter's contract)
111 : * @param[in] patch optional post-fill weight hook */
112 655 : CCHMetricFamily(const Graph* graph, EffortOperation fillEffort,
113 : SUMOTime begin, SUMOTime weightPeriod,
114 : RefVehicleFactory factory, WeightPatch patch) :
115 655 : myGraph(graph), myFillEffort(fillEffort), myGateEffort(nullptr),
116 655 : myFactory(factory), myPatch(patch), myLive(false),
117 655 : myBegin(begin), myWeightPeriod(weightPeriod),
118 4585 : myUpdateFactor(1.), myUpdateConstant(0.) {
119 655 : }
120 :
121 : /** @brief construct a LIVE family (double-buffered, barrier-customized)
122 : * @param[in] graph the shared topology
123 : * @param[in] fillEffort the (possibly per-type) effort the weights are
124 : * computed from, evaluated with the type's reference vehicle
125 : * @param[in] gateEffort the type-INDEPENDENT effort used by the deadband
126 : * (evaluated with a null vehicle: per-type extras are static
127 : * multipliers that cancel out of the moved-or-not decision)
128 : * @param[in] updateFactor / updateConstant the deadband: an edge's arcs
129 : * are only refilled when its gate effort moved by MORE than BOTH the
130 : * relative factor AND the absolute constant (seconds) since this
131 : * buffer last applied it -- the sufficientSaving() analog. The
132 : * moving-average speed filter smears every traffic event into many
133 : * ticks of sub-percent drift; without a deadband that drift marks
134 : * thousands of arcs per barrier and partial re-customization
135 : * degenerates (RoutingKit documents it for the sparse "new traffic
136 : * jam" case). 1/0 = every change propagates. Rejected edges stay
137 : * pending and their drift accumulates against the same applied value,
138 : * so a slow trend eventually passes while filter jitter never does.
139 : * @param[in] factory reference-vehicle factory (required in LIVE mode)
140 : * @param[in] ensembleK how many frozen weights.random-factor
141 : * realizations (= metrics) to keep per type. The exact routers draw a
142 : * fresh perturbation per vehicle; a shared metric freezes ONE
143 : * realization, losing the feature's route diversity. With K > 1 every
144 : * type keeps K metrics -- each filled with its own reference vehicle
145 : * (factory slot k) and therefore its own frozen realization -- and a
146 : * vehicle is assigned its slot by a stable hash of its id, so each
147 : * vehicle persistently routes on one of K perturbed networks:
148 : * K = 1 is exactly the single-realization behavior, growing K
149 : * converges toward the exact routers' per-vehicle diversity at K x
150 : * customization and memory cost. Pointless without
151 : * weights.random-factor (the K references fill identical weights). */
152 231 : CCHMetricFamily(const Graph* graph, EffortOperation fillEffort,
153 : EffortOperation gateEffort, double updateFactor,
154 : double updateConstant, RefVehicleFactory factory,
155 : int ensembleK = 1) :
156 231 : myGraph(graph), myFillEffort(fillEffort), myGateEffort(gateEffort),
157 231 : myFactory(factory), myPatch(nullptr), myLive(true),
158 231 : myBegin(0), myWeightPeriod(SUMOTime_MAX),
159 231 : myUpdateFactor(updateFactor), myUpdateConstant(updateConstant),
160 1617 : myEnsembleK(ensembleK > 1 ? ensembleK : 1) {
161 : const unsigned space = myGraph->edgeIdSpace();
162 693 : for (int i = 0; i < 2; i++) {
163 : // NaN = "never applied": the first pending occurrence always
164 : // passes the deadband and primes the entry
165 462 : myAppliedEffort[i].assign(space, std::numeric_limits<double>::quiet_NaN());
166 462 : myPendingFlag[i].assign(space, 0);
167 : }
168 231 : }
169 :
170 332 : ~CCHMetricFamily() {
171 1793 : for (LiveMetric* c : myLiveMetrics) {
172 1461 : delete c->refVehicle;
173 1461 : delete c;
174 : }
175 436 : for (auto& item : myStaticMetrics) {
176 104 : delete item.second.refVehicle;
177 : }
178 2656 : }
179 :
180 : /// @name STATIC mode
181 : /// @{
182 :
183 : /** @brief the metric for (type key, period of @p time), built on the
184 : * first query of the pair; nullptr before init. Thread-safe: the mutex
185 : * serialises concurrent first queries from parallel routing threads
186 : * (satisfying primeClassMask's synchronization contract); afterwards the
187 : * map is read-only for that key (map references are address-stable).
188 : * A pending flagStale() first refills and re-customizes every cached
189 : * metric from the live efforts and permissions -- in place, so the
190 : * flagging host guarantees that no query is in flight (the simulation's
191 : * main thread, marouter's reset between two iterations). */
192 21273 : MetricPtr get(const K* key, SUMOVehicleClass vClass, SUMOTime time, const V* veh) {
193 21273 : std::lock_guard<std::mutex> lock(myStaticLock);
194 21273 : if (myStale) {
195 102 : for (auto& item : myStaticMetrics) {
196 49 : StaticMetric& sm = item.second;
197 56 : fillStatic(sm, item.first.second, time, sm.refVehicle != nullptr ? sm.refVehicle : veh, veh);
198 49 : sm.metric->customize();
199 : }
200 53 : myStale = false;
201 : }
202 : const int period = periodOf(time);
203 : const std::pair<const K*, int> mapKey(key, period);
204 : auto it = myStaticMetrics.find(mapKey);
205 21273 : if (it == myStaticMetrics.end()) {
206 523 : StaticMetric& sm = myStaticMetrics[mapKey];
207 523 : sm.vClass = vClass;
208 : const V* ref = veh;
209 523 : if (myFactory != nullptr) {
210 104 : sm.refVehicle = myFactory(key, 0);
211 : ref = sm.refVehicle;
212 : }
213 523 : fillStatic(sm, period, time, ref, veh);
214 523 : sm.metric.reset(new RoutingKit::CustomizableContractionHierarchyMetric(
215 523 : myGraph->cch(), sm.weights));
216 523 : sm.metric->customize();
217 523 : return sm.metric.get();
218 : }
219 20750 : return it->second.metric.get();
220 : }
221 :
222 : /// @brief the end of the weight period containing the given time
223 : /// (SUMOTime_MAX when the weights are static); CCHRouter's PeriodEnd hook
224 : SUMOTime periodEnd(SUMOTime time) const {
225 788 : if (myWeightPeriod <= 0 || myWeightPeriod == SUMOTime_MAX) {
226 : return SUMOTime_MAX;
227 : }
228 788 : return myBegin + (periodOf(time) + 1) * myWeightPeriod;
229 : }
230 :
231 : /** @brief the efforts behind the cached metrics changed -- a runtime
232 : * permission change in the simulation (the caller must also invalidate
233 : * the graph's class masks), or marouter's travel times after an
234 : * assignment iteration (CCHRouter::reset): every cached metric is
235 : * refilled and re-customized on the next get(). */
236 : void flagStale() {
237 74 : std::lock_guard<std::mutex> lock(myStaticLock);
238 74 : myStale = true;
239 48 : }
240 :
241 : /// @brief flagStale() under the simulation's name for the permission case
242 : void flagPermissionsStale() {
243 : flagStale();
244 26 : }
245 : /// @}
246 :
247 : /// @name LIVE mode
248 : /// @{
249 :
250 : /// @brief allocate the metric state (all ensemble slots) for one type;
251 : /// call only while no query is in flight (seeding at init, or the
252 : /// customization barrier)
253 1461 : void seedKey(const K* key) {
254 : if (myByKey.count(key) == 0) {
255 1461 : std::vector<LiveMetric*>& slots = myByKey[key];
256 2922 : for (int k = 0; k < myEnsembleK; k++) {
257 1461 : slots.push_back(buildLiveMetric(key, k));
258 : }
259 : }
260 1461 : }
261 :
262 : /// @brief queue an edge whose gate effort changed (both buffers; the
263 : /// deadband applies later, at the customize barrier)
264 1115421 : void markDirty(const E* e) {
265 1115421 : const int id = e->getNumericalID();
266 1115421 : if (id < 0 || id >= (int)myPendingFlag[0].size()) {
267 : return;
268 : }
269 3346263 : for (int i = 0; i < 2; i++) {
270 2230842 : if (!myPendingFlag[i][id]) {
271 1172916 : myPendingFlag[i][id] = 1;
272 1172916 : myPendingList[i].push_back(e);
273 : }
274 : }
275 : }
276 :
277 : /** @brief a runtime permission change (closure / re-opening) hit this
278 : * edge: queue it bypassing the deadband and divert every query to the
279 : * exact fallback until customize() has refilled both buffers wholesale
280 : * (the sparse path cannot repair tail-side arcs of a connection-level
281 : * change). The caller must also invalidate the graph's class masks. */
282 20 : void invalidateEdge(const E* e) {
283 : const int id = e->getNumericalID();
284 20 : if (id < 0 || id >= (int)myPendingFlag[0].size()) {
285 : return;
286 : }
287 20 : markDirty(e);
288 : // NaN sentinel: the deadband always passes, so the flip reaches the
289 : // metric at the next barrier even though the speed table did not move
290 20 : myAppliedEffort[0][id] = std::numeric_limits<double>::quiet_NaN();
291 20 : myAppliedEffort[1][id] = std::numeric_limits<double>::quiet_NaN();
292 20 : myFullFillsPending = 2;
293 : myMetricStale.store(true, std::memory_order_release);
294 : }
295 :
296 : /** @brief the owner's customization barrier: bump the speed epoch when
297 : * an effort moved and re-customize when it matters. Lazy, following the
298 : * CH weightPeriod pattern: metric state is only built for types somebody
299 : * routes on (myLiveMetrics stays empty in a run that never routes, which
300 : * then pays nothing here). Once any metric exists, every barrier where
301 : * an effort moved re-customizes, so a query never sees a metric more
302 : * than one barrier old -- CH's weightPeriod bound. The queried flag only
303 : * bootstraps type creation; a pending permission flip forces the
304 : * customize even without queries. */
305 287851 : void atBarrier(double now, bool effortsMoved) {
306 287851 : if (effortsMoved) {
307 : mySpeedEpoch.fetch_add(1, std::memory_order_release);
308 : }
309 : if (myQueried.exchange(false, std::memory_order_acq_rel)
310 271261 : || myMetricStale.load(std::memory_order_acquire)
311 559104 : || (effortsMoved && !myLiveMetrics.empty())) {
312 211024 : customize(now);
313 : }
314 287851 : }
315 :
316 : /** @brief refill + customize + publish every type's metric from the live
317 : * efforts and permissions. Call only while no query is in flight (the
318 : * publish itself is lock-free: release store here, acquire loads on the
319 : * query hot path). */
320 211255 : void customize(double now) {
321 : // types that queried since the last barrier without a metric
322 : // (streamed route files, TraCI-added types): create their state now
323 : {
324 211255 : std::lock_guard<std::mutex> lock(myWantedLock);
325 211255 : for (const K* key : myWantedKeys) {
326 0 : seedKey(key);
327 : }
328 : myWantedKeys.clear();
329 : }
330 211255 : if (myLiveMetrics.empty()) {
331 0 : return;
332 : }
333 : // buffers flip in lockstep across types (every call processes all)
334 211255 : const int backShared = 1 - myLiveMetrics.front()->frontIndex;
335 : // Permission flip pending: run a FULL refill on this pass's back
336 : // buffer (the other buffer gets its full refill on the next pass --
337 : // myFullFillsPending counts both down). The fills below re-prime the
338 : // graph's connection masks, which the flip's caller invalidated.
339 : bool forceFull = false;
340 211255 : if (myFullFillsPending > 0) {
341 22 : myFullFillsPending--;
342 : forceFull = true;
343 : }
344 : // Deadband pass, once, on the type-shared gate effort: accept edges
345 : // whose effort moved by more than BOTH bounds since this buffer last
346 : // applied them; rejected edges stay pending (see the constructor doc)
347 : std::vector<const E*> accepted;
348 211255 : if (!forceFull && myLiveMetrics.front()->metric[backShared] != nullptr) {
349 : std::vector<const E*> stillPending;
350 3157245 : for (const E* e : myPendingList[backShared]) {
351 : const int id = e->getNumericalID();
352 2946455 : const double effNow = myGateEffort(e, nullptr, now);
353 2946455 : const double effApplied = myAppliedEffort[backShared][id];
354 : bool pass = true;
355 2946455 : if (!std::isnan(effApplied) && effApplied > 0. && effNow > 0.) {
356 : const double hi = MAX2(effNow, effApplied);
357 : const double lo = MIN2(effNow, effApplied);
358 2933724 : pass = (hi / lo > myUpdateFactor) && (hi - lo > myUpdateConstant);
359 : }
360 : if (pass) {
361 1168553 : accepted.push_back(e);
362 1168553 : myAppliedEffort[backShared][id] = effNow;
363 1168553 : myPendingFlag[backShared][id] = 0;
364 : } else {
365 1777902 : stillPending.push_back(e);
366 : }
367 : }
368 : myPendingList[backShared].swap(stillPending);
369 210790 : }
370 1564792 : for (LiveMetric* c : myLiveMetrics) {
371 1353537 : const int back = 1 - c->frontIndex;
372 : // Fill from the live efforts, masking arcs the class is not
373 : // permitted on -- this is where per-class permissions AND active
374 : // closures become inf_weight. The fill reference is the type's
375 : // OWNED refVehicle, so the effort captures the type's extras
376 : // exactly (including each folded via edge inside viaChainEffort)
377 : // and freezes ONE random realization per metric -- the same
378 : // approximation CHRouterWrapper makes per hierarchy; exact
379 : // per-vehicle randomization remains the domain of the exact
380 : // routers.
381 1353537 : if (c->metric[back] == nullptr) {
382 : // first use of this buffer for THIS type only (streamed/late
383 : // vType): no previous state to diff against for this type,
384 : // but every other already-running type's sparse deadband
385 : // state is unaffected, so this branch must NOT trigger the
386 : // shared wipe below (which would reset the type-shared
387 : // myPendingList/myAppliedEffort for everybody just because
388 : // one type is new)
389 2806 : myGraph->fillInputWeights(myFillEffort, c->vClass, c->refVehicle, now, c->weight[back]);
390 2806 : c->metric[back] = std::make_shared<RoutingKit::CustomizableContractionHierarchyMetric>(
391 2806 : myGraph->cch(), c->weight[back]);
392 2806 : c->metric[back]->customize(); // serial; avoids OpenMP oversubscription under FOX
393 1350731 : } else if (forceFull) {
394 : // permission flip: wholesale refill in place from the
395 : // re-primed masks and live permissions (the metric already
396 : // references weight[back]), then a full customize. This is
397 : // the ONLY case that invalidates the type-shared deadband
398 : // state below, since it is the only one that actually
399 : // changes what "applied" means for every type at once.
400 140 : myGraph->fillInputWeights(myFillEffort, c->vClass, c->refVehicle, now, c->weight[back]);
401 140 : c->metric[back]->customize();
402 : } else {
403 : // SPARSE PATH: metric[back] is the customization of the
404 : // current contents of weight[back]. Recompute only the arcs
405 : // of accepted edges (edge->arc reverse image), write the
406 : // ones that moved, and propagate through affected triangles
407 : // only. Cost scales with traffic transitions, not with the
408 : // network.
409 : std::vector<unsigned>& applied = c->weight[back];
410 1350591 : c->partial->reset(myGraph->cch());
411 : unsigned changed = 0;
412 8563503 : for (const E* e : accepted) {
413 24148276 : for (const unsigned a : myGraph->arcsOfEdge(e)) {
414 16935364 : const unsigned newW = myGraph->computeArcWeight(a, myFillEffort, c->vClass, c->refVehicle, now);
415 16935364 : if (newW != applied[a]) {
416 9583256 : applied[a] = newW;
417 9583256 : c->partial->update_arc(a); // takes INPUT arc ids
418 9583256 : ++changed;
419 : }
420 : }
421 : }
422 1350591 : if (changed > 0) {
423 611989 : c->partial->customize(*c->metric[back]);
424 : }
425 : }
426 : // publish lock-free: release store at the barrier, worker
427 : // acquire-loads on the hot path
428 1353537 : c->frontIndex = back;
429 : c->front.store(c->metric[back].get(), std::memory_order_release);
430 : }
431 211255 : if (forceFull) {
432 : // a permission flip's full refill matched every arc to the live
433 : // efforts/masks for this buffer: type-shared deadband entries
434 : // are stale; NaN re-arms the first-change auto-pass. (A plain
435 : // per-type first-fill above does NOT reach here -- it must not
436 : // wipe deadband state that other, already-running types still
437 : // depend on.)
438 97 : for (const E* e : myPendingList[backShared]) {
439 75 : myPendingFlag[backShared][e->getNumericalID()] = 0;
440 : }
441 : myPendingList[backShared].clear();
442 22 : myAppliedEffort[backShared].assign(myAppliedEffort[backShared].size(),
443 22 : std::numeric_limits<double>::quiet_NaN());
444 : }
445 : // every published front now reflects the live permissions (the other
446 : // buffer, if still pending a full refill, is repaired before its
447 : // next publish) and the live efforts as of this barrier (both epoch
448 : // counters only move on the owner's thread at the barrier, so the
449 : // pair cannot tear)
450 : myMetricStale.store(false, std::memory_order_release);
451 : myPublishedEpoch.store(mySpeedEpoch.load(std::memory_order_relaxed),
452 : std::memory_order_release);
453 211255 : }
454 :
455 : /** @brief the published metric for a type key and querying vehicle id
456 : * (the id picks the ensemble slot -- see the LIVE constructor), or
457 : * nullptr if the query must route on the exact fallback. Query hot
458 : * path, any thread. */
459 58844 : MetricPtr published(const K* key, const std::string& vehID) {
460 : const auto it = myByKey.find(key);
461 58844 : if (it == myByKey.end()) {
462 : // streamed-in type without a metric yet: register it for
463 : // creation at the next barrier and fall back
464 : {
465 0 : std::lock_guard<std::mutex> lock(myWantedLock);
466 0 : myWantedKeys.push_back(key);
467 : }
468 : myQueried.store(true, std::memory_order_relaxed);
469 0 : return nullptr;
470 : }
471 : // mark the metric as consumed. Test before set: an unconditional
472 : // store from every query would ping-pong the cache line between
473 : // cores; the read is shared and the store fires once per barrier
474 : // window (relaxed: only gates bootstrap/re-arm, never visibility)
475 58844 : if (!myQueried.load(std::memory_order_relaxed)) {
476 : myQueried.store(true, std::memory_order_relaxed);
477 : }
478 : // A permission flip that is not yet customized in makes every
479 : // published metric unusable: it may still route THROUGH a
480 : // just-closed edge, and CCHRouter's coverage test consults the LIVE
481 : // permissions, which would wrongly bless the query. Route exactly
482 : // until the barrier repairs the metrics.
483 58844 : if (myMetricStale.load(std::memory_order_acquire)) {
484 : return nullptr;
485 : }
486 : // Safety invariant: never serve a metric the efforts have moved away
487 : // from -- the exact routers see live weights and CH is bounded to
488 : // one weightPeriod. atBarrier re-customizes at every moved barrier
489 : // once metrics exist, so this cannot fire in steady state; if a gap
490 : // ever opens, this burst routes on the exact fallback (live weights)
491 : // and the queried flag set above arms the customize that closes it.
492 58808 : if (myPublishedEpoch.load(std::memory_order_acquire)
493 : != mySpeedEpoch.load(std::memory_order_acquire)) {
494 : return nullptr;
495 : }
496 : // stable slot assignment: FNV-1a over the id (std::hash is
497 : // implementation-defined and would break cross-platform test
498 : // reproducibility)
499 58808 : const LiveMetric* c = myEnsembleK == 1 ? it->second.front()
500 0 : : it->second[fnv1a(vehID) % myEnsembleK];
501 58808 : return c->front.load(std::memory_order_acquire);
502 : }
503 :
504 : /// @brief whether any type has live metric state yet
505 : bool empty() const {
506 : return myLiveMetrics.empty();
507 : }
508 : /// @}
509 :
510 : private:
511 : /// @brief per-type LIVE metric state. Heap-owned: the atomic makes it
512 : /// non-movable, so it cannot live in a map by value.
513 4383 : struct LiveMetric {
514 : std::vector<unsigned> weight[2]; // ping-pong input-weight buffers, live whole run
515 : std::shared_ptr<RoutingKit::CustomizableContractionHierarchyMetric> metric[2];
516 : std::atomic<const RoutingKit::CustomizableContractionHierarchyMetric*> front{nullptr};
517 : int frontIndex = 1; // first customize uses back = 0
518 : SUMOVehicleClass vClass = SVC_PASSENGER;
519 : /// @brief OWNED effort-reference vehicle (see RefVehicleFactory)
520 : V* refVehicle = nullptr;
521 : /// @brief partial-customization worker (queue over the shared CCH);
522 : /// one per metric -- its queue is drained by every customize()
523 : std::shared_ptr<RoutingKit::CustomizableContractionHierarchyPartialCustomization> partial;
524 : };
525 :
526 : /// @brief per-(type, period) STATIC metric state. RoutingKit metrics
527 : /// BORROW their input-weight buffer, so buffer and metric live together
528 : /// in the map (std::map nodes are address-stable).
529 523 : struct StaticMetric {
530 : std::vector<unsigned> weights;
531 : std::unique_ptr<RoutingKit::CustomizableContractionHierarchyMetric> metric;
532 : SUMOVehicleClass vClass = SVC_PASSENGER;
533 : /// @brief OWNED reference vehicle (nullptr when filling with the
534 : /// querying vehicle)
535 : V* refVehicle = nullptr;
536 : /// @brief whether the first fill (which runs the patch) is done
537 : bool filled = false;
538 : /// @brief the (arc, weight) pairs the patch wrote on the first fill,
539 : /// re-applied on every refill
540 : std::vector<std::pair<unsigned, unsigned> > patched;
541 : };
542 :
543 : /// @brief the weight period containing @p time: 0 is everything before
544 : /// begin and the whole run when the weights are static
545 : int periodOf(SUMOTime time) const {
546 22061 : if (myWeightPeriod > 0 && myWeightPeriod != SUMOTime_MAX && time > myBegin) {
547 442 : return (int)((time - myBegin) / myWeightPeriod);
548 : }
549 : return 0;
550 : }
551 :
552 : /** @brief (re)fill a STATIC metric's input weights from the live efforts.
553 : * A weight-period grid evaluates at the period's begin, so each pair is
554 : * customized once per fill; a single period evaluates at the query time
555 : * -- free-flow efforts are time-invariant anyway, and marouter's
556 : * per-iteration travel times are exactly what a Dijkstra query at that
557 : * time sees. The patch runs once, on the first fill, and what it wrote is
558 : * re-applied on every refill: the masked set depends only on the
559 : * metric's type, whereas the vehicle triggering a refill may be of
560 : * another type. */
561 572 : void fillStatic(StaticMetric& sm, int period, SUMOTime time, const V* ref, const V* veh) {
562 572 : const double fillTime = myWeightPeriod == SUMOTime_MAX
563 572 : ? STEPS2TIME(time) : STEPS2TIME(myBegin + period * myWeightPeriod);
564 572 : myGraph->fillInputWeights(myFillEffort, sm.vClass, ref, fillTime, sm.weights);
565 572 : if (!sm.filled) {
566 523 : sm.filled = true;
567 523 : if (myPatch != nullptr) {
568 419 : const std::vector<unsigned> unpatched(sm.weights);
569 419 : myPatch(myGraph, veh, sm.weights);
570 8824 : for (unsigned a = 0; a < (unsigned)sm.weights.size(); a++) {
571 8405 : if (sm.weights[a] != unpatched[a]) {
572 3 : sm.patched.emplace_back(a, sm.weights[a]);
573 : }
574 : }
575 419 : }
576 : } else {
577 49 : for (const auto& p : sm.patched) {
578 0 : sm.weights[p.first] = p.second;
579 : }
580 : }
581 572 : }
582 :
583 : /// @brief stable 64-bit FNV-1a for the ensemble slot assignment
584 : static uint64_t fnv1a(const std::string& s) {
585 : uint64_t h = 1469598103934665603ull;
586 0 : for (const char ch : s) {
587 0 : h = (h ^ (unsigned char)ch) * 1099511628211ull;
588 : }
589 : return h;
590 : }
591 :
592 : /// @brief allocate LIVE metric state for one (type, ensemble slot)
593 : /// (owner's thread only)
594 1461 : LiveMetric* buildLiveMetric(const K* key, int slot) {
595 1461 : LiveMetric* c = new LiveMetric();
596 1461 : c->refVehicle = myFactory(key, slot);
597 1461 : c->vClass = c->refVehicle->getVClass();
598 1461 : c->weight[0].resize(myGraph->arcCount());
599 1461 : c->weight[1].resize(myGraph->arcCount());
600 1461 : c->partial = std::make_shared<RoutingKit::CustomizableContractionHierarchyPartialCustomization>(myGraph->cch());
601 1461 : myLiveMetrics.push_back(c);
602 1461 : return c;
603 : }
604 :
605 : /// @brief the immutable shared topology
606 : const Graph* myGraph;
607 : /// @brief the effort the metrics are filled from
608 : EffortOperation myFillEffort;
609 : /// @brief the type-independent effort of the LIVE deadband
610 : EffortOperation myGateEffort;
611 : /// @brief reference-vehicle factory (required LIVE, optional STATIC)
612 : RefVehicleFactory myFactory;
613 : /// @brief post-fill weight hook (STATIC)
614 : WeightPatch myPatch;
615 : /// @brief LIVE or STATIC (fixed at construction)
616 : const bool myLive;
617 :
618 : /// @name STATIC state
619 : /// @{
620 : SUMOTime myBegin;
621 : SUMOTime myWeightPeriod;
622 : std::map<std::pair<const K*, int>, StaticMetric> myStaticMetrics;
623 : std::mutex myStaticLock;
624 : /// @brief the efforts changed behind the cached metrics: refill and
625 : /// re-customize them all on the next get() (see flagStale)
626 : bool myStale = false;
627 : /// @}
628 :
629 : /// @name LIVE state
630 : /// @{
631 : /// @brief the deadband bounds (see the LIVE constructor)
632 : double myUpdateFactor;
633 : double myUpdateConstant;
634 : /// @brief frozen random-factor realizations per type (see the LIVE ctor)
635 : int myEnsembleK = 1;
636 : /// @brief every type's state, in creation order (barrier iteration) and
637 : /// by key (query lookup; only mutated while no query is in flight)
638 : std::vector<LiveMetric*> myLiveMetrics;
639 : std::map<const K*, std::vector<LiveMetric*> > myByKey;
640 : /// @brief types that queried but have no metric yet (demand streams, so
641 : /// types can appear after seeding); queries register them under the lock
642 : /// and route via the fallback until the next barrier builds their state
643 : std::vector<const K*> myWantedKeys;
644 : std::mutex myWantedLock;
645 : /// @brief whether a query arrived since the last barrier (bootstraps
646 : /// type creation and re-arms after resets -- see atBarrier)
647 : std::atomic<bool> myQueried{false};
648 : /// @brief a permission flip has not yet been customized into the
649 : /// published metrics (queries divert to the exact fallback while set)
650 : std::atomic<bool> myMetricStale{false};
651 : /// @brief barrier counter of the tracked efforts and the counter value
652 : /// the published metrics were customized from: the staleness bound's
653 : /// safety invariant (see atBarrier / published). Both only change on the
654 : /// owner's thread at the barrier -- no query is in flight there -- the
655 : /// atomics just publish the values to the query threads.
656 : std::atomic<uint64_t> mySpeedEpoch{0};
657 : std::atomic<uint64_t> myPublishedEpoch{0};
658 : /// @brief how many customize passes must run a FULL refill after a
659 : /// permission flip: 2 = both ping-pong buffers (the sparse path cannot
660 : /// repair connection-level mask changes: arcsOfEdge deliberately omits
661 : /// the tail side)
662 : int myFullFillsPending = 0;
663 : /// @brief per-buffer edge state for the sparse path: the gate effort
664 : /// each edge's arcs were last filled from (NaN = never applied /
665 : /// forced), the pending-dirty flag, and the pending list
666 : std::vector<double> myAppliedEffort[2];
667 : std::vector<char> myPendingFlag[2];
668 : std::vector<const E*> myPendingList[2];
669 : /// @}
670 :
671 : private:
672 : CCHMetricFamily(const CCHMetricFamily&) = delete;
673 : CCHMetricFamily& operator=(const CCHMetricFamily&) = delete;
674 : };
|