diff --git a/include/nigiri/routing/gpu/raptor.h b/include/nigiri/routing/gpu/raptor.h index 549271c59..9f6511b2b 100644 --- a/include/nigiri/routing/gpu/raptor.h +++ b/include/nigiri/routing/gpu/raptor.h @@ -7,6 +7,7 @@ #include "nigiri/common/delta_t.h" #include "nigiri/routing/clasz_mask.h" #include "nigiri/routing/journey.h" +#include "nigiri/routing/lb/lb_transit_legs.h" #include "nigiri/routing/limits.h" #include "nigiri/routing/pareto_set.h" #include "nigiri/routing/query.h" @@ -70,6 +71,7 @@ struct gpu_raptor { std::vector const& dist_to_dest, hash_map> const& td_dist_to_dest, std::vector const& lb, + lb_transit_legs& lb_rounds, std::vector const& via_stops, day_idx_t const base, clasz_mask_t const allowed_claszes, diff --git a/include/nigiri/routing/lb/lb_transit_legs.h b/include/nigiri/routing/lb/lb_transit_legs.h new file mode 100644 index 000000000..fa9f230b3 --- /dev/null +++ b/include/nigiri/routing/lb/lb_transit_legs.h @@ -0,0 +1,308 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "utl/erase_duplicates.h" + +#include "nigiri/for_each_meta.h" +#include "nigiri/routing/query.h" +#include "nigiri/rt/rt_timetable.h" +#include "nigiri/timetable.h" + +namespace nigiri::routing { + +struct lb_components { + static lb_components const& get_cached(timetable const& tt, + profile_idx_t const prf_idx) { + struct key { + timetable const* tt_; + profile_idx_t prf_idx_; + std::uint32_t n_locations_, n_routes_; + void const* fp_data_; + void const* route_data_; + auto operator<=>(key const&) const = default; + }; + static auto mutex = std::mutex{}; + static auto cache = std::map>{}; + + auto const k = key{&tt, + prf_idx, + tt.n_locations(), + tt.n_routes(), + tt.locations_.footpaths_out_[prf_idx].data_.data(), + tt.route_location_seq_.data_.data()}; + auto const lock = std::scoped_lock{mutex}; + auto& entry = cache[k]; + if (entry == nullptr) { + entry = std::make_unique(tt, prf_idx); + } + return *entry; + } + + lb_components(timetable const& tt, profile_idx_t const prf_idx) { + auto const n = tt.n_locations(); + + auto parent = std::vector(n); + std::iota(begin(parent), end(parent), 0U); + auto const find = [&](std::uint32_t x) { + while (parent[x] != x) { + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + }; + auto const unite = [&](std::uint32_t const a, std::uint32_t const b) { + auto const ra = find(a); + auto const rb = find(b); + if (ra != rb) { + parent[rb] = ra; + } + }; + + for (auto l = std::uint32_t{0U}; l != n; ++l) { + auto const li = location_idx_t{l}; + for (auto const fp : tt.locations_.footpaths_out_[prf_idx][li]) { + unite(l, to_idx(fp.target())); + } + for (auto const fp : tt.locations_.footpaths_in_[prf_idx][li]) { + unite(l, to_idx(fp.target())); + } + } + + constexpr auto kNoId = std::numeric_limits::max(); + comp_.resize(n); + utl::fill(comp_, kNoId); + n_components_ = 0U; + for (auto l = std::uint32_t{0U}; l != n; ++l) { + auto const r = find(l); + if (comp_[r] == kNoId) { + comp_[r] = n_components_++; + } + comp_[l] = comp_[r]; + } + + comp_loc_offsets_.resize(n_components_ + 1U); + utl::fill(comp_loc_offsets_, 0U); + for (auto l = std::uint32_t{0U}; l != n; ++l) { + ++comp_loc_offsets_[comp_[l] + 1U]; + } + std::partial_sum(begin(comp_loc_offsets_), end(comp_loc_offsets_), + begin(comp_loc_offsets_)); + comp_locations_.resize(n); + auto pos = comp_loc_offsets_; + for (auto l = std::uint32_t{0U}; l != n; ++l) { + comp_locations_[pos[comp_[l]]++] = location_idx_t{l}; + } + + comp_route_offsets_.resize(n_components_ + 1U); + comp_route_offsets_[0U] = 0U; + auto routes = std::vector{}; + for (auto c = std::uint32_t{0U}; c != n_components_; ++c) { + routes.clear(); + for (auto i = comp_loc_offsets_[c]; i != comp_loc_offsets_[c + 1U]; ++i) { + for (auto const r : tt.location_routes_[comp_locations_[i]]) { + routes.push_back(r); + } + } + utl::erase_duplicates(routes); + comp_routes_.insert(end(comp_routes_), begin(routes), end(routes)); + comp_route_offsets_[c + 1U] = + static_cast(comp_routes_.size()); + } + } + + std::uint32_t n_components_{0U}; + std::vector comp_; // location -> component id + std::vector comp_loc_offsets_; + std::vector comp_locations_; + std::vector comp_route_offsets_; + std::vector comp_routes_; +}; + +// SearchDir refers to the direction of the main routing query +// fwd: finds the minimum number of transit legs backward from the destination +// bwd: finds the minimum number of transit legs forward from the destination +// +// Locations connected by footpaths are collapsed into components +template +struct lb_transit_legs { + static constexpr auto kUnreachable = std::numeric_limits::max(); + static constexpr auto kUnknown = kUnreachable - 1U; + + lb_transit_legs(timetable const& tt, + query const& q, + rt_timetable const* rtt = nullptr, + bool const disabled = false) + : tt_{tt}, + rtt_{rtt != nullptr && rtt->n_rt_transports() > 0U ? rtt : nullptr}, + q_{q}, + comps_{disabled ? nullptr : &lb_components::get_cached(tt, q.prf_idx_)}, + k_{0U}, + end_k_{static_cast( + std::min(q.max_transfers_, kMaxTransfers) + 2U)}, + any_marked_{false}, + total_time_{0U} { + if (comps_ == nullptr) { + return; + } + auto const start_time = std::chrono::steady_clock::now(); + + lb_.resize(comps_->n_components_); + utl::fill(lb_, kUnknown); + comp_mark_.resize(comps_->n_components_); + utl::fill(comp_mark_.blocks_, 0U); + route_mark_.resize(tt_.n_routes()); + if (rtt_ != nullptr) { + rt_transport_mark_.resize(rtt_->n_rt_transports()); + } + + auto const set_terminal = [&](location_idx_t const i) { + auto const c = comps_->comp_[to_idx(i)]; + lb_[c] = 0U; + comp_mark_.set(c, true); + }; + + for (auto const& o : q.destination_) { + for_each_meta(tt_, q.dest_match_mode_, o.target(), + [&](location_idx_t const meta) { set_terminal(meta); }); + } + + for (auto const& [l, tds] : q.td_dest_) { + for (auto const& td : tds) { + if (td.duration() != footpath::kMaxDuration && + td.duration() < q.max_travel_time_) { + for_each_meta(tt_, q.dest_match_mode_, l, + [&](location_idx_t const meta) { set_terminal(meta); }); + } + } + } + + total_time_ += std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time); + + k_ = 1U; + for (auto const& s : q.start_) { + get(s.target()); + } + for (auto const& td : q.td_start_) { + get(td.first); + } + } + + void run_round() { + constexpr auto kFwd = SearchDir == direction::kForward; + auto const start_time = std::chrono::steady_clock::now(); + + utl::fill(route_mark_.blocks_, 0U); + if (rtt_ != nullptr) { + utl::fill(rt_transport_mark_.blocks_, 0U); + } + + any_marked_ = false; + comp_mark_.for_each_set_bit([&](std::uint64_t const c) { + for (auto i = comps_->comp_route_offsets_[c]; + i != comps_->comp_route_offsets_[c + 1U]; ++i) { + any_marked_ = true; + route_mark_.set(to_idx(comps_->comp_routes_[i]), true); + } + if (rtt_ != nullptr) { + for (auto i = comps_->comp_loc_offsets_[c]; + i != comps_->comp_loc_offsets_[c + 1U]; ++i) { + auto const l = comps_->comp_locations_[i]; + for (auto const rt_t : rtt_->location_rt_transports_[l]) { + any_marked_ = true; + rt_transport_mark_.set(to_idx(rt_t), true); + } + } + } + }); + if (!any_marked_) { + total_time_ += std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time); + return; + } + + utl::fill(comp_mark_.blocks_, 0U); + + any_marked_ = false; + auto const prev_k = static_cast(k_ - 1U); + auto const relax_seq = [&](auto const& seq) { + auto prop = false; + for (auto x = 0U; x != seq.size(); ++x) { + auto const pos = kFwd ? seq.size() - x - 1U : x; + auto const c = comps_->comp_[to_idx(stop{seq[pos]}.location_idx())]; + auto const lb = lb_[c]; + if (lb == prev_k) { + prop = true; + } else if (prop) { + if (k_ < lb) { + lb_[c] = k_; + comp_mark_.set(c, true); + any_marked_ = true; + } + } + } + }; + + route_mark_.for_each_set_bit([&](std::uint64_t const i) { + relax_seq(tt_.route_location_seq_[route_idx_t{i}]); + }); + if (rtt_ != nullptr) { + rt_transport_mark_.for_each_set_bit([&](std::uint64_t const i) { + relax_seq(rtt_->rt_transport_location_seq_[rt_transport_idx_t{i}]); + }); + } + + total_time_ += std::chrono::duration_cast( + std::chrono::steady_clock::now() - start_time); + } + + void set_end_k(std::uint8_t const end_k) { end_k_ = end_k; } + + bool enabled() const { return comps_ != nullptr; } + + void advance() { + run_round(); + ++k_; + if (!any_marked_) { + for (auto& lb : lb_) { + if (lb == kUnknown) { + lb = kUnreachable; + } + } + } + } + + std::uint8_t get(location_idx_t const l) { + if (comps_ == nullptr) { + return 0U; + } + auto const c = comps_->comp_[to_idx(l)]; + while (lb_[c] == kUnknown) { + if (k_ >= end_k_) { + return kUnreachable; + } + advance(); + } + return lb_[c]; + } + + timetable const& tt_; + rt_timetable const* rtt_; + query const& q_; + lb_components const* comps_; + std::uint8_t k_; + std::uint8_t end_k_; + bool any_marked_; + std::chrono::microseconds total_time_; + bitvec comp_mark_; + bitvec route_mark_; + bitvec rt_transport_mark_; + std::vector lb_; // component id -> lower bound +}; + +} // namespace nigiri::routing diff --git a/include/nigiri/routing/raptor/raptor.h b/include/nigiri/routing/raptor/raptor.h index c48e0a077..e5ab0a3d2 100644 --- a/include/nigiri/routing/raptor/raptor.h +++ b/include/nigiri/routing/raptor/raptor.h @@ -5,6 +5,7 @@ #include "nigiri/common/delta_t.h" #include "nigiri/common/linear_lower_bound.h" #include "nigiri/routing/journey.h" +#include "nigiri/routing/lb/lb_transit_legs.h" #include "nigiri/routing/limits.h" #include "nigiri/routing/pareto_set.h" #include "nigiri/routing/raptor/debug.h" @@ -17,6 +18,13 @@ #include "nigiri/timetable.h" #include "nigiri/types.h" +// #define NIGIRI_LB_TRACING +#ifdef NIGIRI_LB_TRACING +#define trace_lb(...) fmt::print(__VA_ARGS__) +#else +#define trace_lb(...) +#endif + namespace nigiri::routing { enum class search_mode { kOneToOne, kOneToAll }; @@ -60,7 +68,8 @@ struct raptor { std::array& is_via, std::vector& dist_to_dest, hash_map> const& td_dist_to_dest, - std::vector& lb, + std::vector& lb_time, + lb_transit_legs& lb_rounds, std::vector const& via_stops, day_idx_t const base, clasz_mask_t const allowed_claszes, @@ -82,7 +91,8 @@ struct raptor { is_via_{is_via}, dist_to_end_{dist_to_dest}, td_dist_to_end_{td_dist_to_dest}, - lb_{lb}, + lb_time_{lb_time}, + lb_rounds_{lb_rounds}, via_stops_{via_stops}, base_{base}, allowed_claszes_{allowed_claszes}, @@ -92,6 +102,14 @@ struct raptor { transfer_time_settings_{tts} { assert(Vias == via_stops_.size()); reset_arrivals(); + use_suffix_ = lb_rounds_.enabled(); + if (use_suffix_) { + auto const n_route_stops = tt_.route_location_seq_.data_.size(); + state_.route_suffix_time_.resize(n_route_stops); + state_.route_suffix_legs_.resize(n_route_stops); + state_.route_suffix_built_.resize(n_routes_); + utl::fill(state_.route_suffix_built_.blocks_, 0U); + } if (!dist_to_end_.empty()) { // only used for intermodal queries (dist_to_dest != empty) end_reachable_.resize(n_locations_); @@ -144,7 +162,8 @@ struct raptor { unixtime_t const worst_time_at_dest, profile_idx_t const prf_idx, pareto_set& results) { - auto const end_k = std::min(max_transfers, kMaxTransfers) + 2U; + end_k_ = std::min(max_transfers, kMaxTransfers) + 2U; + lb_rounds_.set_end_k(static_cast(end_k_)); auto const d_worst_at_dest = unix_to_delta(base(), worst_time_at_dest); for (auto& time_at_dest : time_at_dest_) { @@ -153,7 +172,7 @@ struct raptor { trace_print_init_state(); - for (auto k = 1U; k != end_k; ++k) { + for (auto k = 1U; k != end_k_; ++k) { for (auto i = 0U; i != n_locations_; ++i) { for (auto v = 0U; v != Vias + 1; ++v) { best_[i][v] = get_best(round_times_[k][i][v], best_[i][v]); @@ -165,6 +184,16 @@ struct raptor { auto any_marked = false; state_.station_mark_.for_each_set_bit([&](std::uint64_t const i) { + if (lb_time_[i] == kUnreachable) { + return; + } + auto b = round_times_[k - 1][i][0]; + for (auto v = 1U; v != Vias + 1; ++v) { + b = get_best(b, round_times_[k - 1][i][v]); + } + if (!is_better(b + dir(lb_time_[i]), time_at_dest_[k])) { + return; + } for (auto const& r : tt_.location_routes_[location_idx_t{i}]) { any_marked = true; state_.route_mark_.set(to_idx(r), true); @@ -263,7 +292,7 @@ struct raptor { } is_dest_.for_each_set_bit([&](auto const i) { - for (auto k = 1U; k != end_k; ++k) { + for (auto k = 1U; k != end_k_; ++k) { auto const dest_time = round_times_[k][i][Vias]; if (dest_time != kInvalid) { trace("ADDING JOURNEY: start={}, dest={} @ {}, transfers={}\n", @@ -502,10 +531,20 @@ struct raptor { if (is_better(fp_target_time, best_[i][target_v]) && is_better(fp_target_time, time_at_dest_[k])) { - if (lb_[i] == kUnreachable || - !is_better(fp_target_time + dir(lb_[i]), time_at_dest_[k])) { + if (lb_time_[i] == kUnreachable || + !is_better(fp_target_time + dir(lb_time_[i]), time_at_dest_[k])) { ++stats_.fp_update_prevented_by_lower_bound_; - return; + trace_lb( + "┊ ├k={} *** LB NO TRANSFER UPD: (from={}, tmp={}) --{}--> " + "(to={}, " + "best={}) --> update => {}, LB={}, LB_AT_DEST={}, " + "DEST[{}]={}\n", + k, loc{tt_, location_idx_t{i}}, to_unix(tmp_[i][v]), + transfer_time, loc{tt_, location_idx_t{i}}, best_[i][target_v], + to_unix(fp_target_time), lb_time_[i], + to_unix(clamp(fp_target_time + dir(lb_time_[i]))), k, + to_unix(time_at_dest_[k])); + continue; } ++stats_.n_earliest_arrival_updated_by_footpath_; @@ -531,6 +570,19 @@ struct raptor { } } + if (lb_time_[i] == kUnreachable) { + return; + } + auto src = tmp_[i][0]; + for (auto v = 1U; v != Vias + 1; ++v) { + src = get_best(src, tmp_[i][v]); + } + if (!is_better(src + dir(lb_time_[i]), time_at_dest_[k])) { + trace_lb("┊ ├k={} *** LB NO FP SOURCE: at={}, tmp={}, lb_time={}\n", k, + loc{tt_, l_idx}, to_unix(src), lb_time_[i]); + return; + } + auto const& fps = kFwd ? tt_.locations_.footpaths_out_[prf_idx][l_idx] : tt_.locations_.footpaths_in_[prf_idx][l_idx]; @@ -567,20 +619,20 @@ struct raptor { if (is_better(fp_target_time, best_[target][target_v]) && is_better(fp_target_time, time_at_dest_[k])) { - auto const lower_bound = lb_[target]; - if (lower_bound == kUnreachable || - !is_better(fp_target_time + dir(lower_bound), + if (lb_time_[target] == kUnreachable || + !is_better(fp_target_time + dir(lb_time_[target]), time_at_dest_[k])) { ++stats_.fp_update_prevented_by_lower_bound_; - trace_upd( - "┊ ├k={} *** LB NO UPD: (from={}, tmp={}) --{}--> (to={}, " - "best={}) --> update => {}, LB={}, LB_AT_DEST={}, DEST={}\n", + trace_lb( + "┊ ├k={} *** LB NO FP UPD: (from={}, tmp={}) --{}--> (to={}, " + "best={}) --> update => {}, LB={}, LB_AT_DEST={}, " + "DEST[{}]={}\n", k, loc{tt_, l_idx}, to_unix(tmp_[to_idx(l_idx)][v]), adjusted_transfer_time(transfer_time_settings_, fp.duration()), loc{tt_, fp.target()}, best_[target][target_v], - to_unix(fp_target_time), lower_bound, - to_unix(clamp(fp_target_time + dir(lower_bound))), + to_unix(fp_target_time), lb_time_[target], + to_unix(clamp(fp_target_time + dir(lb_time_[target]))), k, to_unix(time_at_dest_[k])); continue; } @@ -631,6 +683,19 @@ struct raptor { return; } + if (lb_time_[i] == kUnreachable) { + return; + } + auto src = tmp_[i][0]; + for (auto v = 1U; v != Vias + 1; ++v) { + src = get_best(src, tmp_[i][v]); + } + if (!is_better(src + dir(lb_time_[i]), time_at_dest_[k])) { + trace_lb("┊ ├k={} *** LB NO TD FP SOURCE: at={}, tmp={}, lb_time={}\n", + k, loc{tt_, l_idx}, to_unix(src), lb_time_[i]); + return; + } + auto const& fps = kFwd ? rtt_->td_footpaths_out_[prf_idx][l_idx] : rtt_->td_footpaths_in_[prf_idx][l_idx]; @@ -665,19 +730,21 @@ struct raptor { if (is_better(fp_target_time, best_[target][target_v]) && is_better(fp_target_time, time_at_dest_[k])) { - auto const lower_bound = lb_[target]; - if (lower_bound == kUnreachable || - !is_better(fp_target_time + dir(lower_bound), + if (lb_time_[target] == kUnreachable || + !is_better(fp_target_time + dir(lb_time_[target]), time_at_dest_[k])) { ++stats_.fp_update_prevented_by_lower_bound_; - trace_upd( + trace_lb( "┊ ├k={} *** LB NO TD FP UPD: (from={}, tmp={}) --{}--> " - "(to={}, best={}) --> update => {}, LB={}, LB_AT_DEST={}, " - "DEST={}\n", + "(to={}, " + "best={}) --> update => {}, LB={}, LB_AT_DEST={}, " + "DEST[{}]={}\n", k, loc{tt_, l_idx}, to_unix(tmp_[to_idx(l_idx)][v]), - fp.duration(), loc{tt_, fp.target()}, best_[target][target_v], - fp_target_time, lower_bound, - to_unix(clamp(fp_target_time + dir(lower_bound))), + adjusted_transfer_time(transfer_time_settings_, + fp.duration()), + loc{tt_, fp.target()}, best_[target][target_v], + to_unix(fp_target_time), lb_time_[target], + to_unix(clamp(fp_target_time + dir(lb_time_[target]))), k, to_unix(time_at_dest_[k])); return utl::cflow::kContinue; } @@ -885,10 +952,10 @@ struct raptor { auto current_best = get_best(round_times_[k - 1][l_idx][target_v], tmp_[l_idx][target_v], best_[l_idx][target_v]); - if (is_better(by_transport, time_at_dest_[k]) && - lb_[l_idx] != kUnreachable && - is_better(by_transport + dir(lb_[l_idx]), time_at_dest_[k])) { + lb_time_[l_idx] != kUnreachable && + is_better(by_transport + dir(lb_time_[l_idx]), + time_at_dest_[k])) { trace_upd( "┊ │k={} RT | name={}, dbg={}, time_by_transport={}, " "BETTER THAN current_best={} => update, {} marking station " @@ -912,7 +979,7 @@ struct raptor { } } - if (lb_[l_idx] == kUnreachable) { + if (lb_time_[l_idx] == kUnreachable) { break; } @@ -935,11 +1002,32 @@ struct raptor { return any_marked; } + void build_route_suffix(route_idx_t const r) { + auto const seq = tt_.route_location_seq_[r]; + auto const off = tt_.route_location_seq_.bucket_starts_[to_idx(r)]; + auto run_time = std::numeric_limits::max(); + auto run_legs = lb_transit_legs::kUnreachable; + for (auto x = 0U; x != seq.size(); ++x) { + auto const pos = kFwd ? seq.size() - x - 1U : x; + state_.route_suffix_time_[off + pos] = run_time; + state_.route_suffix_legs_[off + pos] = run_legs; + auto const l = stop{seq[pos]}.location_idx(); + run_time = std::min(run_time, lb_time_[to_idx(l)]); + run_legs = std::min(run_legs, lb_rounds_.get(l)); + } + state_.route_suffix_built_.set(to_idx(r), true); + } + template bool update_route(unsigned const k, route_idx_t const r) { auto const stop_seq = tt_.route_location_seq_[r]; + auto const suffix_off = + use_suffix_ ? tt_.route_location_seq_.bucket_starts_[to_idx(r)] : 0U; + if (use_suffix_ && !state_.route_suffix_built_.test(to_idx(r))) { + build_route_suffix(r); + } bool any_marked = false; auto et = std::array{}; @@ -1034,8 +1122,9 @@ struct raptor { assert(by_transport != std::numeric_limits::min() && by_transport != std::numeric_limits::max()); if (is_better(by_transport, time_at_dest_[k]) && - lb_[l_idx] != kUnreachable && - is_better(by_transport + dir(lb_[l_idx]), time_at_dest_[k])) { + lb_time_[l_idx] != kUnreachable && + is_better(by_transport + dir(lb_time_[l_idx]), + time_at_dest_[k])) { trace_upd( "┊ │k={} v={}->{} name={}, dbg={}, time_by_transport={}, " "BETTER THAN current_best={} => update, {} marking station " @@ -1045,7 +1134,6 @@ struct raptor { to_unix(current_best[v]), !is_better(by_transport, current_best[v]) ? "NOT" : "", loc{tt_, stp.location_idx()}); - ++stats_.n_earliest_arrival_updated_by_route_; tmp_[l_idx][target_v] = get_best(by_transport, tmp_[l_idx][target_v]); @@ -1055,11 +1143,11 @@ struct raptor { } any_marked = true; } else { - trace( - "┊ │k={} v={}->{} *** NO UPD: at={}, name={}, dbg={}, " + trace_lb( + "┊ │k={} v={}->{} *** NO ROUTE UPD: at={}, name={}, dbg={}, " "time_by_transport={}, current_best=min({}, {}, {})={} => {} " "- " - "LB={}, LB_AT_DEST={}, TIME_AT_DEST={}, " + "LB={}, LB_AT_DEST={}, TIME_AT_DEST[{}]={}, " "(is_better(by_transport={}={}, current_best={}={})={}, " "is_better(by_transport={}={}, time_at_dest_={}={})={}, " "reachable={}, " @@ -1070,19 +1158,20 @@ struct raptor { to_unix(round_times_[k - 1][l_idx][target_v]), to_unix(best_[l_idx][target_v]), to_unix(tmp_[l_idx][target_v]), to_unix(current_best[v]), loc{tt_, location_idx_t{l_idx}}, - lb_[l_idx], to_unix(time_at_dest_[k]), - to_unix(clamp(by_transport + dir(lb_[l_idx]))), by_transport, - to_unix(by_transport), current_best[v], - to_unix(current_best[v]), + lb_time_[l_idx], + to_unix(clamp(by_transport + dir(lb_time_[l_idx]))), k, + to_unix(time_at_dest_[k]), by_transport, to_unix(by_transport), + current_best[v], to_unix(current_best[v]), is_better(by_transport, current_best[v]), by_transport, to_unix(by_transport), time_at_dest_[k], to_unix(time_at_dest_[k]), is_better(by_transport, time_at_dest_[k]), - lb_[l_idx] != kUnreachable, by_transport + dir(lb_[l_idx]), - to_unix(clamp(by_transport + dir(lb_[l_idx]))), + lb_time_[l_idx] != kUnreachable, + by_transport + dir(lb_time_[l_idx]), + to_unix(clamp(by_transport + dir(lb_time_[l_idx]))), time_at_dest_[k], to_unix(time_at_dest_[k]), to_unix(time_at_dest_[k]), - is_better(clamp(by_transport + dir(lb_[l_idx])), + is_better(clamp(by_transport + dir(lb_time_[l_idx])), time_at_dest_[k])); } } else { @@ -1099,10 +1188,25 @@ struct raptor { continue; } - if (lb_[l_idx] == kUnreachable) { + if (lb_time_[l_idx] == kUnreachable) { break; } + if (use_suffix_) { + auto b = round_times_[k - 1][l_idx][0]; + for (auto v = 1U; v != Vias + 1; ++v) { + b = get_best(b, round_times_[k - 1][l_idx][v]); + } + auto const sfx_dest_k = + k + state_.route_suffix_legs_[suffix_off + stop_idx]; + if (sfx_dest_k >= end_k_ || + !is_better( + b + dir(state_.route_suffix_time_[suffix_off + stop_idx]), + time_at_dest_[sfx_dest_k])) { + continue; + } + } + for (auto v = 0U; v != Vias + 1; ++v) { if (!et[v].is_valid() && !state_.prev_station_mark_[l_idx]) { continue; @@ -1186,15 +1290,17 @@ struct raptor { auto const ev_mam = ev.mam(); if (is_better_or_eq(time_at_dest_[k], - to_delta(day, ev_mam) + dir(lb_[to_idx(l)]))) { - trace( + to_delta(day, ev_mam) + dir(lb_time_[to_idx(l)]))) { + trace_lb( "┊ │k={} => name={}, dbg={}, day={}={}, best_mam={}, " - "transport_mam={}, transport_time={} => TIME AT DEST {} IS " + "transport_mam={}, transport_time={}, lb_time={} => " + "TIME_AT_DEST[{}] {} IS " "BETTER!\n", k, tt_.transport_name(tt_.route_transport_ranges_[r][t_offset]), tt_.dbg(tt_.route_transport_ranges_[r][t_offset]), day, tt_.to_unixtime(day, 0_minutes), mam_at_stop, ev_mam, tt_.to_unixtime(day, duration_t{ev_mam}), + to_delta(day, ev_mam) + dir(lb_time_[to_idx(l)]), k, to_unix(time_at_dest_[k])); return {transport_idx_t::invalid(), day_idx_t::invalid()}; } @@ -1312,7 +1418,10 @@ struct raptor { std::array const& is_via_; std::vector const& dist_to_end_; hash_map> const& td_dist_to_end_; - std::vector const& lb_; + std::vector const& lb_time_; + lb_transit_legs& lb_rounds_; + unsigned end_k_; + bool use_suffix_{false}; std::vector const& via_stops_; std::array time_at_dest_; day_idx_t base_; diff --git a/include/nigiri/routing/raptor/raptor_state.h b/include/nigiri/routing/raptor/raptor_state.h index 278fcce4d..94510d07c 100644 --- a/include/nigiri/routing/raptor/raptor_state.h +++ b/include/nigiri/routing/raptor/raptor_state.h @@ -89,6 +89,10 @@ struct raptor_state { bitvec prev_station_mark_; bitvec route_mark_; bitvec rt_transport_mark_; + + std::vector route_suffix_time_; + std::vector route_suffix_legs_; + bitvec route_suffix_built_; }; } // namespace nigiri::routing diff --git a/include/nigiri/routing/search.h b/include/nigiri/routing/search.h index 1cd3651d8..2255380e9 100644 --- a/include/nigiri/routing/search.h +++ b/include/nigiri/routing/search.h @@ -13,8 +13,10 @@ #include "nigiri/routing/dijkstra.h" #include "nigiri/routing/direct.h" #include "nigiri/routing/get_fastest_direct.h" +#include "nigiri/routing/gpu/raptor.h" #include "nigiri/routing/interval_estimate.h" #include "nigiri/routing/journey.h" +#include "nigiri/routing/lb/lb_transit_legs.h" #include "nigiri/routing/limits.h" #include "nigiri/routing/pareto_set.h" #include "nigiri/routing/query.h" @@ -80,6 +82,8 @@ struct search { using algo_state_t = typename Algo::algo_state_t; static constexpr auto const kFwd = (SearchDir == direction::kForward); static constexpr auto const kBwd = (SearchDir == direction::kBackward); + static constexpr auto const kGpu = + std::is_same_v>; Algo init(clasz_mask_t const allowed_claszes, bool const require_bikes_allowed, @@ -153,6 +157,7 @@ struct search { state_.dist_to_dest_, q_.td_dest_, state_.travel_time_lower_bound_, + lb_rounds_, q_.via_stops_, day_idx_t{ std::chrono::duration_cast( @@ -188,6 +193,7 @@ struct search { }}, q_.start_time_)}, fastest_direct_{get_fastest_direct(tt_, q_, SearchDir)}, + lb_rounds_{tt, q_, rtt, kGpu}, algo_{init(q_.allowed_claszes_, q_.require_bike_transport_, q_.require_car_transport_, @@ -498,6 +504,7 @@ struct search { interval search_interval_; search_stats stats_; duration_t fastest_direct_; + lb_transit_legs lb_rounds_; Algo algo_; std::optional timeout_; }; diff --git a/include/nigiri/routing/tb/query_engine.h b/include/nigiri/routing/tb/query_engine.h index aff791410..fb4ffa914 100644 --- a/include/nigiri/routing/tb/query_engine.h +++ b/include/nigiri/routing/tb/query_engine.h @@ -1,6 +1,7 @@ #pragma once #include "nigiri/routing/journey.h" +#include "nigiri/routing/lb/lb_transit_legs.h" #include "nigiri/routing/pareto_set.h" #include "nigiri/routing/query.h" #include "nigiri/routing/tb/queue.h" @@ -87,7 +88,8 @@ struct query_engine { std::array const&, std::vector const& dist_to_dest, hash_map> const&, - std::vector const& lb, + std::vector const& lb_time, + lb_transit_legs& lb_rounds, std::vector const&, day_idx_t base, clasz_mask_t, @@ -130,7 +132,8 @@ struct query_engine { query_state& state_; bitvec const& is_dest_; std::vector const& dist_to_dest_; - std::vector const& lb_; + std::vector const& lb_time_; + lb_transit_legs& lb_rounds_; day_idx_t base_; query_stats stats_; }; diff --git a/src/routing/gpu/raptor.cu b/src/routing/gpu/raptor.cu index ab333d564..a1fc54833 100644 --- a/src/routing/gpu/raptor.cu +++ b/src/routing/gpu/raptor.cu @@ -505,6 +505,7 @@ gpu_raptor::gpu_raptor( std::vector const& dist_to_dest, hash_map> const& td_dist_to_dest, std::vector const& /* lb (GPU: no lower bounds) */, + lb_transit_legs& /* lb_rounds (GPU: no lower bounds) */, std::vector const& via_stops, day_idx_t const base, clasz_mask_t const allowed_claszes, diff --git a/src/routing/one_to_all.cc b/src/routing/one_to_all.cc index dda00c2ad..6314e2592 100644 --- a/src/routing/one_to_all.cc +++ b/src/routing/one_to_all.cc @@ -65,7 +65,8 @@ raptor_state one_to_all(timetable const& tt, auto is_dest = bitvec{tt.n_locations()}; // Keep footpath time for each stop auto is_via = std::array{}; auto dist_to_dest = std::vector{}; - auto lb = std::vector(tt.n_locations(), 0U); + auto lb_time = std::vector(tt.n_locations(), 0U); + auto lb_rounds = lb_transit_legs(tt, q, nullptr, true); auto const base = make_base(tt, start_time); auto const is_wheelchair = q.prf_idx_ == kWheelchairProfile; @@ -77,7 +78,8 @@ raptor_state one_to_all(timetable const& tt, is_via, dist_to_dest, q.td_dest_, - lb, + lb_time, + lb_rounds, q.via_stops_, base, q.allowed_claszes_, diff --git a/src/routing/raptor/pong.cc b/src/routing/raptor/pong.cc index e5f304f9c..22fdce099 100644 --- a/src/routing/raptor/pong.cc +++ b/src/routing/raptor/pong.cc @@ -11,6 +11,7 @@ #include "nigiri/routing/direct.h" #include "nigiri/routing/get_earliest_transport.h" #include "nigiri/routing/gpu/raptor.h" +#include "nigiri/routing/lb/lb_transit_legs.h" #include "nigiri/routing/leg_alternatives.h" #include "nigiri/routing/transfer_time_settings.h" #include "nigiri/rt/frun.h" @@ -117,6 +118,8 @@ routing_result pong(timetable const& tt, ping_lb); } lb_time += std::chrono::steady_clock::now() - ping_lb_start; + auto ping_lb_rounds = + lb_transit_legs(tt, q, rtt, /* disabled= */ true); auto ping = ping_algo_t{tt, rtt, @@ -126,6 +129,7 @@ routing_result pong(timetable const& tt, ping_dist_to_dest, q.td_dest_, ping_lb, + ping_lb_rounds, q.via_stops_, base_day, q.allowed_claszes_, @@ -165,6 +169,7 @@ routing_result pong(timetable const& tt, pong_lb); } lb_time += std::chrono::steady_clock::now() - pong_lb_start; + auto pong_lb_rounds = lb_transit_legs(tt, q, rtt, kGpu); auto pong = pong_algo_t{tt, rtt, @@ -174,6 +179,7 @@ routing_result pong(timetable const& tt, pong_dist_to_dest, q.td_dest_, pong_lb, + pong_lb_rounds, q.via_stops_, base_day, q.allowed_claszes_, @@ -384,6 +390,10 @@ routing_result pong(timetable const& tt, fmt::join(s_state.results_.els_ | std::views::transform(to_tuple), "\n\t")); + fmt::println("lb_rounds total time: {}", + std::chrono::duration_cast( + ping_lb_rounds.total_time_ + pong_lb_rounds.total_time_)); + if constexpr (!kFwd) { return result; } diff --git a/src/routing/tb/query_engine.cc b/src/routing/tb/query_engine.cc index 6c95fb1e6..ce4eadfc7 100644 --- a/src/routing/tb/query_engine.cc +++ b/src/routing/tb/query_engine.cc @@ -31,7 +31,8 @@ query_engine::query_engine( std::array const&, std::vector const& dist_to_dest, hash_map> const&, - std::vector const& lb, + std::vector const& lb_time, + lb_transit_legs& lb_rounds, std::vector const&, day_idx_t const base, clasz_mask_t, @@ -43,7 +44,8 @@ query_engine::query_engine( state_{state}, is_dest_{is_dest}, dist_to_dest_{dist_to_dest}, - lb_{lb}, + lb_time_{lb_time}, + lb_rounds_{lb_rounds}, base_{base - QUERY_DAY_SHIFT} { state_.q_n_.base_ = base_; stats_.lower_bound_pruning_ = UseLowerBounds; @@ -185,7 +187,7 @@ void query_engine::seg_prune(std::uint8_t const k, if constexpr (UseLowerBounds) { auto const l = stop{tt_.route_location_seq_[tt_.transport_route_[t]][i]} .location_idx(); - arr_time += duration_t{lb_[to_idx(l)]}; + arr_time += duration_t{lb_time_[to_idx(l)]}; } if (arr_time > state_.t_min_[k + 1]) { tb_debug("PRUNING {}", seg(segment, qe)); diff --git a/test/routing/lb_transit_legs_test.cc b/test/routing/lb_transit_legs_test.cc new file mode 100644 index 000000000..1a530c872 --- /dev/null +++ b/test/routing/lb_transit_legs_test.cc @@ -0,0 +1,286 @@ +#include "gtest/gtest.h" + +#include "nigiri/loader/gtfs/load_timetable.h" +#include "nigiri/loader/init_finish.h" +#include "nigiri/routing/lb/lb_transit_legs.h" +#include "nigiri/rt/create_rt_timetable.h" +#include "nigiri/rt/rt_timetable.h" +#include "nigiri/timetable.h" + +using namespace date; +using namespace nigiri; +using namespace nigiri::loader; +using namespace nigiri::routing; + +// routes: +// X -> Y -> P | P -> S | S -> T +// | S -> B1 | B1 -> T +// | S -> C1 | C1 -> C2 | C2 -> T +// | S -> D1 | D1 -> D2 | D2 -> D3 | D3 -> T +// +// footpaths: +// F -> S +mem_dir lb_test_tt() { + return mem_dir::read(R"__( +"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +MTA,MOTIS Transit Authority,https://motis-project.de/,Europe/Berlin + +# calendar_dates.txt +service_id,date,exception_type +SID,20260227,1 + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +O,O,,,,,, +X,X,,,,,, +Y,Y,,,,,, +P,P,,,,,, +F,F,,,,,, +S,S,,,,,, +B1,B1,,,,,, +C1,C1,,,,,, +C2,C2,,,,,, +D1,D1,,,,,, +D2,D2,,,,,, +D3,D3,,,,,, +T,T,,,,,, + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +XYP,MTA,XYP,XYP,X -> Y -> P,0 +PS,MTA,PS,PS,P -> S,0 +A,MTA,A,A,S -> T,0 +B1,MTA,B1,B1,S -> B1,0 +B2,MTA,B2,B2,B1 -> T,0 +C1,MTA,C1,C1,S -> C1,0 +C2,MTA,C2,C2,C1 -> C2,0 +C3,MTA,C3,C3,C2 -> T,0 +D1,MTA,D1,D1,S -> D1,0 +D2,MTA,D2,D2,D1 -> D2,0 +D3,MTA,D3,D3,D2 -> D3,0 +D4,MTA,D4,D4,D3 -> T,0 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +XYP,SID,XYP,XYP,0 +PS,SID,PS,PS,1 +A,SID,A,A,2 +B1,SID,B1,B1,3 +B2,SID,B2,B2,4 +C1,SID,C1,C1,5 +C2,SID,C2,C2,6 +C3,SID,C3,C3,7 +D1,SID,D1,D1,8 +D2,SID,D2,D2,9 +D3,SID,D3,D3,10 +D4,SID,D4,D4,11 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +XYP,12:00,12:15,X,0,0,0 +XYP,13:00,14:15,Y,1,0,0 +XYP,15:00,15:15,P,2,0,0 +PS,03:00,03:00,P,0,0,0 +PS,04:00,04:00,S,1,0,0 +A,08:00,08:00,S,0,0,0 +A,18:00,18:00,T,1,0,0 +B1,09:00,09:00,S,0,0,0 +B1,13:00,13:00,B1,1,0,0 +B2,14:00,14:00,B1,0,0,0 +B2,17:00,17:00,T,1,0,0 +C1,10:00,10:00,S,0,0,0 +C1,11:00,11:00,C1,1,0,0 +C2,12:00,12:00,C1,0,0,0 +C2,13:00,13:00,C2,1,0,0 +C3,14:00,14:00,C2,0,0,0 +C3,15:00,15:00,T,1,0,0 +D1,11:00,11:00,S,0,0,0 +D1,11:15,11:15,D1,1,0,0 +D2,11:30,11:30,D1,0,0,0 +D2,11:45,11:45,D2,1,0,0 +D3,12:00,12:00,D2,0,0,0 +D3,12:15,12:15,D3,1,0,0 +D4,12:30,12:30,D3,0,0,0 +D4,13:00,13:00,T,0,0,0 + +# transfers.txt +from_stop_id,to_stop_id,transfer_type,min_transfer_time +F,S,2,600 +)__"); +} + +constexpr auto kGtfsDateRange = interval{sys_days{2026_y / February / 27}, + sys_days{2026_y / February / 28}}; + +timetable load_gtfs(auto const& files, interval const date_range) { + using namespace date; + timetable tt; + tt.date_range_ = date_range; + register_special_stations(tt); + gtfs::load_timetable({}, source_idx_t{0U}, files(), tt); + finalize(tt); + return tt; +} + +TEST(routing, lb_transit_legs) { + auto const tt = load_gtfs(lb_test_tt, kGtfsDateRange); + auto rtt = rt::create_rt_timetable(tt, sys_days{2026_y / February / 27}); + auto q = query{ + .start_time_ = unixtime_t{sys_days{February / 27 / 2026}}, + .start_ = {{tt.locations_.location_id_to_idx_.at({"P", source_idx_t{0U}}), + 3_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"T", source_idx_t{0U}}), + 13_minutes, 0U}}, + .td_dest_{{tt.find(location_id{"T", source_idx_t{}}).value(), + {{.valid_from_ = sys_days{2026_y / January / 27}, + .duration_ = 7_minutes, + .transport_mode_id_ = 5}, + {.valid_from_ = sys_days{2026_y / January / 28}, + .duration_ = footpath::kMaxDuration, + .transport_mode_id_ = 5}}}}}; + + auto lb_fwd = lb_transit_legs(tt, q); + + auto const get_lb_fwd = [&](auto&& id) { + return lb_fwd.lb_[lb_fwd.comps_->comp_[to_idx( + tt.locations_.location_id_to_idx_.at({id, source_idx_t{0U}}))]]; + }; + + ASSERT_EQ(lb_fwd.comps_->comp_.size(), tt.n_locations()); + EXPECT_EQ(get_lb_fwd("T"), 0U); + EXPECT_EQ(get_lb_fwd("D3"), 1U); + EXPECT_EQ(get_lb_fwd("C2"), 1U); + EXPECT_EQ(get_lb_fwd("B1"), 1U); + EXPECT_EQ(get_lb_fwd("S"), 1U); + EXPECT_EQ(get_lb_fwd("F"), 1U); + EXPECT_EQ(get_lb_fwd("P"), 2U); + EXPECT_EQ(get_lb_fwd("D2"), 2U); + EXPECT_EQ(get_lb_fwd("C1"), 2U); + EXPECT_EQ(get_lb_fwd("D1"), lb_fwd.kUnknown); + EXPECT_EQ(get_lb_fwd("X"), lb_fwd.kUnknown); + EXPECT_EQ(get_lb_fwd("Y"), lb_fwd.kUnknown); + + EXPECT_EQ(lb_fwd.get( + tt.locations_.location_id_to_idx_.at({"D1", source_idx_t{0U}})), + 3U); + EXPECT_EQ(get_lb_fwd("X"), 3U); + EXPECT_EQ(get_lb_fwd("Y"), 3U); + + q.flip_dir(); + auto lb_bwd = lb_transit_legs(tt, q); + + auto const get_lb_bwd = [&](auto&& id) { + return lb_bwd.lb_[lb_bwd.comps_->comp_[to_idx( + tt.locations_.location_id_to_idx_.at({id, source_idx_t{0U}}))]]; + }; + + ASSERT_EQ(lb_bwd.comps_->comp_.size(), tt.n_locations()); + EXPECT_EQ(get_lb_bwd("T"), 2U); + EXPECT_EQ(get_lb_bwd("D3"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("C2"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("B1"), 2U); + EXPECT_EQ(get_lb_bwd("S"), 1U); + EXPECT_EQ(get_lb_bwd("F"), 1U); + EXPECT_EQ(get_lb_bwd("P"), 0U); + EXPECT_EQ(get_lb_bwd("D2"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("C1"), 2U); + EXPECT_EQ(get_lb_bwd("D1"), 2U); + EXPECT_EQ(get_lb_bwd("X"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("Y"), lb_bwd.kUnknown); + + EXPECT_EQ(lb_bwd.get( + tt.locations_.location_id_to_idx_.at({"C2", source_idx_t{0U}})), + 3U); + EXPECT_EQ(get_lb_bwd("D3"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("D2"), 3U); + EXPECT_EQ(get_lb_bwd("X"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("Y"), lb_bwd.kUnknown); + + EXPECT_EQ(lb_bwd.get( + tt.locations_.location_id_to_idx_.at({"D3", source_idx_t{0U}})), + 4U); + EXPECT_EQ(get_lb_bwd("X"), lb_bwd.kUnknown); + EXPECT_EQ(get_lb_bwd("Y"), lb_bwd.kUnknown); + + EXPECT_EQ( + lb_bwd.get(tt.locations_.location_id_to_idx_.at({"X", source_idx_t{0U}})), + lb_bwd.kUnreachable); + EXPECT_EQ(get_lb_bwd("Y"), lb_bwd.kUnreachable); +} + +mem_dir lb_fp_after_ride_tt() { + return mem_dir::read(R"__( +"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +MTA,MOTIS Transit Authority,https://motis-project.de/,Europe/Berlin + +# calendar_dates.txt +service_id,date,exception_type +SID,20260227,1 + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +T,T,,,,,, +P,P,,,,,, +M,M,,,,,, +C,C,,,,,, +W,W,,,,,, + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +PT,MTA,PT,PT,P -> T,0 +MC,MTA,MC,MC,M -> C,0 +CT,MTA,CT,CT,C -> T,0 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +PT,SID,PT,PT,0 +MC,SID,MC,MC,1 +CT,SID,CT,CT,2 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +PT,10:00,10:00,P,0,0,0 +PT,11:00,11:00,T,1,0,0 +MC,08:00,08:00,M,0,0,0 +MC,09:00,09:00,C,1,0,0 +CT,09:30,09:30,C,0,0,0 +CT,10:30,10:30,T,1,0,0 + +# transfers.txt +from_stop_id,to_stop_id,transfer_type,min_transfer_time +M,P,2,540 +W,M,2,540 +)__"); +} + +TEST(routing, lb_transit_legs_footpath_component) { + auto tt_mut = timetable{}; + tt_mut.date_range_ = kGtfsDateRange; + register_special_stations(tt_mut); + gtfs::load_timetable({}, source_idx_t{0U}, lb_fp_after_ride_tt(), tt_mut); + finalize(tt_mut, false, false, false, 10U); + auto const& tt = tt_mut; + auto const l = [&](auto&& id) { + return tt.locations_.location_id_to_idx_.at({id, source_idx_t{0U}}); + }; + auto q = query{.start_time_ = unixtime_t{sys_days{February / 27 / 2026}}, + .start_ = {{l("W"), 0_minutes, 0U}}, + .destination_ = {{l("T"), 0_minutes, 0U}}}; + + auto lb = lb_transit_legs(tt, q); + + EXPECT_EQ(lb.comps_->comp_[to_idx(l("W"))], lb.comps_->comp_[to_idx(l("M"))]); + EXPECT_EQ(lb.comps_->comp_[to_idx(l("M"))], lb.comps_->comp_[to_idx(l("P"))]); + EXPECT_NE(lb.comps_->comp_[to_idx(l("C"))], lb.comps_->comp_[to_idx(l("P"))]); + EXPECT_NE(lb.comps_->comp_[to_idx(l("T"))], lb.comps_->comp_[to_idx(l("C"))]); + + EXPECT_EQ(lb.get(l("P")), 1U); + EXPECT_EQ(lb.get(l("C")), 1U); + EXPECT_EQ(lb.get(l("M")), 1U); + EXPECT_EQ(lb.get(l("W")), 1U); +}