diff --git a/include/nigiri/routing/pareto_set.h b/include/nigiri/routing/pareto_set.h index 13190b710..93c3eff00 100644 --- a/include/nigiri/routing/pareto_set.h +++ b/include/nigiri/routing/pareto_set.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/include/nigiri/routing/tb/tb_a_star/a_star.h b/include/nigiri/routing/tb/tb_a_star/a_star.h new file mode 100644 index 000000000..c070a995c --- /dev/null +++ b/include/nigiri/routing/tb/tb_a_star/a_star.h @@ -0,0 +1,124 @@ +#pragma once +#include "nigiri/common/dial.h" +#include "nigiri/routing/pareto_set.h" +#include "nigiri/routing/tb/tb_data.h" +#include "nigiri/rt/rt_timetable.h" + +namespace nigiri::routing::tb::a_star { + +inline unsigned int transfer_factor = 5; +unixtime_t get_time(segment_idx_t const& idx, + timetable const& tt, + event_type const& event, + tb_data const& tbd, + day_idx_t const& day_idx); +day_idx_t get_day(unixtime_t const& before, + segment_idx_t const& s_idx, + timetable const& tt, + tb_data const& tbd); +std::pair, bool> get_neighbours( + segment_idx_t const& s_idx, tb_data const& tbd, day_idx_t const& day); + +struct cost_func_t { + explicit cost_func_t() = default; + std::size_t operator()( + std::pair const& element) const { + if (element.second.count() < 0) + throw std::runtime_error("Got negative costs"); + return static_cast(element.second.count()); + } +}; + +struct a_star_stats { + std::map to_map() const { + return {{"max_transfers_reached_", max_transfers_reached_}, + {"queued_segments_", queued_segments_}, + {"segments_taken_out_", segments_taken_out_}, + {"segments_ignored_", segments_ignored_}, + {"ever_requeued_", ever_requeued_}}; + } + + bool max_transfers_reached_ = false; + int queued_segments_ = 0; + int segments_taken_out_ = 0; + int segments_ignored_ = 0; + bool ever_requeued_ = false; +}; + +struct a_star_state { + explicit a_star_state(tb_data const& tbd) : tbd_{tbd} { + end_reachable_.resize(tbd_.segment_transfers_.size()); + } + tb_data tbd_; + bitvec_map end_reachable_; + hash_map dist_to_dest_; +}; + +struct tb_a_star { + + using algo_state_t = a_star_state; + using algo_stats_t = a_star_stats; + static constexpr bool kUseLowerBounds = true; + static constexpr auto kUnreachable = + std::numeric_limits::max(); + + explicit tb_a_star(unsigned int const& init_cap) + : state_(tb_data()), + queue_(1440 + (init_cap - 1) * transfer_factor, cost_func_t()) { + pred_ = vector_map(init_cap, + segment_idx_t::invalid()); + day_idx_ = vector_map(init_cap); + transfers_ = vector_map( + init_cap, std::numeric_limits::max()); + is_start_segment_ = bitvec_map(init_cap); + is_start_segment_.zero_out(); + } + + tb_a_star(timetable const& tt, + rt_timetable const*, + a_star_state& state, + bitvec const& is_dest, + std::array const&, + std::vector const& dist_to_dest, + hash_map> const&, + std::vector const& lb, + std::vector const&, + day_idx_t base, + clasz_mask_t, + bool, + bool, + bool, + transfer_time_settings); + + algo_stats_t get_stats() const { return algo_stats_; } + void reset_arrivals() {} + void next_start_time() { + queue_.clear(); + is_start_segment_.zero_out(); + utl::fill(pred_, segment_idx_t::invalid()); + utl::fill(transfers_, std::numeric_limits::max()); + } + void add_start(location_idx_t const l, unixtime_t const t); + segment_idx_t get_departure_segment(segment_idx_t const& s); + void execute(unixtime_t const start_time, + std::uint8_t const max_transfers, + unixtime_t const worst_time_at_dest, + profile_idx_t const profile_idx, + pareto_set& results); + void reconstruct(query const& q, journey& j); + + duration_t heuristic(segment_idx_t const& s); + + vector_map pred_; + vector_map day_idx_; + timetable tt_; + a_star_state state_; + dial, cost_func_t> queue_; + segment_idx_t end_segment_ = segment_idx_t::invalid(); + vector_map transfers_; + std::vector travel_time_lower_bound_; + bitvec_map is_start_segment_; + algo_stats_t algo_stats_; +}; + +} // namespace nigiri::routing::tb::a_star \ No newline at end of file diff --git a/include/nigiri/string_store.h b/include/nigiri/string_store.h index fa79c40c0..63b6921b3 100644 --- a/include/nigiri/string_store.h +++ b/include/nigiri/string_store.h @@ -53,6 +53,7 @@ struct string_store { cache_ = o.cache_; resolve(); } + return *this; } string_store& operator=(string_store&& o) { if (&o != this) { @@ -60,6 +61,7 @@ struct string_store { cache_ = std::move(o.cache_); resolve(); } + return *this; } template diff --git a/src/routing/tb/tb_a_star/a_star.cc b/src/routing/tb/tb_a_star/a_star.cc new file mode 100644 index 000000000..503c30413 --- /dev/null +++ b/src/routing/tb/tb_a_star/a_star.cc @@ -0,0 +1,419 @@ +#include "nigiri/routing/tb/tb_a_star/a_star.h" + +#include +#include + +#include "nigiri/routing/get_earliest_transport.h" +#include "nigiri/timetable.h" + +#include "nigiri/for_each_meta.h" + +namespace nigiri::routing::tb::a_star { + +tb_a_star::tb_a_star(timetable const& tt, + rt_timetable const*, + a_star_state& state, + bitvec const& is_dest, + std::array const&, + std::vector const& dist_to_dest, + hash_map> const&, + std::vector const& lb, + std::vector const&, + day_idx_t, + clasz_mask_t, + bool, + bool, + bool, + transfer_time_settings) + : pred_(state.tbd_.segment_transfers_.size(), segment_idx_t::invalid()), + day_idx_(state.tbd_.segment_transfers_.size()), + tt_{tt}, + state_{state}, + queue_{ + 1440 + (state.tbd_.segment_transfers_.size() - 1) * transfer_factor, + cost_func_t()}, + transfers_(state.tbd_.segment_transfers_.size(), + std::numeric_limits::max()), + travel_time_lower_bound_{lb}, + is_start_segment_(state.tbd_.segment_transfers_.size()) { + state_.end_reachable_.zero_out(); + is_start_segment_.zero_out(); + auto const mark_dest_segments = [&](location_idx_t const l, + duration_t const d) { + for (auto const r : tt_.location_routes_[l]) { + auto const stop_seq = tt_.route_location_seq_[r]; + for (auto i = stop_idx_t{1U}; i != stop_seq.size(); ++i) { + if (auto const stp = stop{stop_seq[i]}; + stp.location_idx() != l || !stp.out_allowed()) { + continue; + } + + for (auto const t : tt_.route_transport_ranges_[r]) { + auto const segment = state_.tbd_.transport_first_segment_[t] + i - 1; + state_.end_reachable_.set(segment, true); + + auto const it = state_.dist_to_dest_.find(segment); + if (it == end(state_.dist_to_dest_)) { + state_.dist_to_dest_.emplace_hint(it, segment, d); + } else { + it->second = std::min(it->second, d); + } + } + } + } + }; + + if (dist_to_dest.empty()) /* Destination is stop. */ { + is_dest.for_each_set_bit([&](std::size_t const i) { + auto const l = location_idx_t{i}; + mark_dest_segments(l, duration_t{0U}); + for (auto const fp : + tt_.locations_.footpaths_in_[state_.tbd_.prf_idx_][l]) { + mark_dest_segments(fp.target(), fp.duration()); + } + }); + } else /* Destination is coordinate. */ { + for (auto const [l_idx, dist] : utl::enumerate(dist_to_dest)) { + if (dist != kUnreachable) { + mark_dest_segments(location_idx_t{l_idx}, duration_t{dist}); + } + } + } +} + +std::pair, bool> get_neighbours( + segment_idx_t const& s_idx, tb_data const& tbd, day_idx_t const& day) { + std::vector neighbours; + for (auto const transfer : tbd.segment_transfers_[s_idx]) { + if (tbd.bitfields_[transfer.traffic_days_].test(day.v_)) + neighbours.push_back(transfer.to_segment_); + } + auto next_exists = + s_idx + 1 < tbd.segment_transports_.size() && + tbd.segment_transports_[s_idx] == tbd.segment_transports_[s_idx + 1]; + if (next_exists) neighbours.push_back(s_idx + 1); + return {neighbours, next_exists}; +} + +unixtime_t get_time(segment_idx_t const& idx, + timetable const& tt, + event_type const& event, + tb_data const& tbd, + day_idx_t const& day_idx) { + auto const transport = tbd.segment_transports_[idx]; + auto const stop_idx = + static_cast((idx - tbd.transport_first_segment_[transport] + + (event == event_type::kArr ? 1 : 0)) + .v_); + return tt.event_time({transport, day_idx}, stop_idx, event); +} + +day_idx_t get_day(unixtime_t const& before, + segment_idx_t const& s_idx, + timetable const& tt, + tb_data const& tbd) { + auto size = (tt.date_range_.to_ - tt.date_range_.from_).count(); + auto const starting_day_idx = tt.day_idx_mam(tt.date_range_.from_).first; + for (unsigned short i = 0; i < size; ++i) { + auto day_idx = starting_day_idx + i; + auto dep_time = get_time(s_idx, tt, event_type::kDep, tbd, day_idx); + if (dep_time >= before) return day_idx; + } + return day_idx_t::invalid(); +} + +location_idx_t get_location(segment_idx_t const& s, + timetable const& tt, + tb_data const& tbd, + event_type const& event) { + auto transport = tbd.segment_transports_[s]; + auto stop_idx = s - tbd.transport_first_segment_[transport] + + (event == event_type::kArr ? 1 : 0); + auto route_idx = tt.transport_route_[transport]; + auto stop_value = tt.route_location_seq_[route_idx][stop_idx.v_]; + stop stop_place(stop_value); + return stop_place.location_idx(); +} + +duration_t tb_a_star::heuristic(segment_idx_t const& s) { + return duration_t( + travel_time_lower_bound_ + [get_location(s, tt_, state_.tbd_, event_type::kArr).v_]); +} + +template +bool is_invalid(T v) { + auto invalid_value = T::invalid(); + return v == invalid_value; +} + +void tb_a_star::execute(unixtime_t const start_time, + std::uint8_t const max_transfers, + unixtime_t const worst_time_at_dest, + profile_idx_t const, + pareto_set& results) { + vector_map seen(state_.tbd_.segment_transfers_.size(), + false); + duration_t best_cost_at_dest = duration_t::max(); + unixtime_t arrival_time_limit = + std::min(worst_time_at_dest, start_time + duration_t(1441)); + is_start_segment_.for_each_set_bit([&](segment_idx_t const& s) { + day_idx_[s] = get_day(start_time, s, tt_, state_.tbd_); + if (is_invalid(day_idx_[s])) return; + auto time = get_time(s, tt_, event_type::kArr, state_.tbd_, day_idx_[s]); + if (time >= arrival_time_limit) return; + queue_.push(std::pair(s, time - start_time + heuristic(s))); + ++algo_stats_.queued_segments_; + }); + + while (true) { + if (queue_.empty()) { + if (best_cost_at_dest != duration_t::max()) break; + return; + } + auto [p, costs] = queue_.top(); + queue_.pop(); + ++algo_stats_.segments_taken_out_; + if (costs >= best_cost_at_dest) break; + if (seen[p]) continue; + seen[p] = true; + if (state_.end_reachable_.test(p)) { + auto costs_at_dest = costs + state_.dist_to_dest_[p] - heuristic(p); + if (costs_at_dest < best_cost_at_dest) { + best_cost_at_dest = costs_at_dest; + end_segment_ = p; + } + } + auto arrival = get_time(p, tt_, event_type::kArr, state_.tbd_, day_idx_[p]); + auto day_of_arrival = tt_.day_idx_mam(arrival).first; + auto [neighbours, next_exists] = + get_neighbours(p, state_.tbd_, day_of_arrival); + for (std::size_t i = 0; i < neighbours.size(); ++i) { + auto neighbour = neighbours[i]; + auto is_next = next_exists && i == neighbours.size() - 1; + uint8_t transfer_count = transfers_[p] + (is_next ? 0 : 1); + day_idx_[neighbour] = + is_next ? day_idx_[p] : get_day(arrival, neighbour, tt_, state_.tbd_); + if (is_invalid(day_idx_[neighbour])) continue; + auto neighbour_arrival = get_time(neighbour, tt_, event_type::kArr, + state_.tbd_, day_idx_[neighbour]); + if (transfer_count < transfers_[neighbour] && + neighbour_arrival < arrival_time_limit) { + transfers_[neighbour] = transfer_count; + pred_[neighbour] = p; + duration_t new_costs = neighbour_arrival - start_time + + duration_t(transfer_factor * transfer_count) + + heuristic(neighbour); + queue_.push(std::pair(neighbour, new_costs)); + if (seen[neighbour]) algo_stats_.ever_requeued_ = true; + seen[neighbour] = false; + ++algo_stats_.queued_segments_; + } else { + ++algo_stats_.segments_ignored_; + } + } + } + algo_stats_.max_transfers_reached_ = + transfers_[end_segment_] >= max_transfers; + results.add({.legs_ = {}, + .start_time_ = start_time, + .dest_time_ = get_time(end_segment_, tt_, event_type::kArr, + state_.tbd_, day_idx_[end_segment_]) + + state_.dist_to_dest_[end_segment_], + .dest_ = location_idx_t::invalid(), + .transfers_ = transfers_[end_segment_]}); +} + +void tb_a_star::add_start(location_idx_t const l, unixtime_t const t) { + auto const [day, mam] = tt_.day_idx_mam(t); + for (auto const r : tt_.location_routes_[l]) { + // iterate stop sequence of route, skip last stop + auto const stop_seq = tt_.route_location_seq_[r]; + for (auto i = stop_idx_t{0U}; i < stop_seq.size() - 1; ++i) { + auto const stp = stop{stop_seq[i]}; + if (!stp.in_allowed() || stp.location_idx() != l) { + continue; + } + + auto const et = get_earliest_transport( + tt_, tt_, 0U, r, i, day, mam, stp.location_idx(), + [](day_idx_t, std::int16_t) { return false; }); + if (!et.is_valid()) { + continue; + } + + auto const transport_first_segment = + state_.tbd_.transport_first_segment_[et.t_idx_]; + is_start_segment_.set(transport_first_segment + i); + transfers_[transport_first_segment + i] = 0; + } + } +} + +segment_idx_t tb_a_star::get_departure_segment(segment_idx_t const& s) { + auto p = pred_[s]; + auto prev = s; + auto invalid_segment = segment_idx_t::invalid(); + while (p != invalid_segment && state_.tbd_.segment_transports_[p] == + state_.tbd_.segment_transports_[prev]) { + prev = p; + p = pred_[p]; + } + return prev; +} + +void tb_a_star::reconstruct(query const& q, journey& j) { + if (is_invalid(end_segment_)) + throw std::runtime_error("reconstruction demanded with no route found"); + auto const offset = state_.dist_to_dest_.at(end_segment_); + auto arrival_location = + get_location(end_segment_, tt_, state_.tbd_, event_type::kArr); + segment_idx_t departure_segment = get_departure_segment(end_segment_); + + auto const has_offset = [&](std::vector const& offsets, + location_match_mode const match_mode, + location_idx_t const l) { + return utl::any_of(offsets, [&](routing::offset const& o) { + return matches(tt_, match_mode, o.target(), l); + }); + }; + + auto const get_fp = [&](location_idx_t const from, location_idx_t const to) { + if (from == to) { + return footpath{to, tt_.locations_.transfer_time_[from]}; + } + auto const from_fps = + tt_.locations_.footpaths_out_[state_.tbd_.prf_idx_][from]; + auto const it = utl::find_if( + from_fps, [&](footpath const& fp) { return fp.target() == to; }); + utl::verify(it != end(from_fps), + "tb reconstruct: footpath from {} to {} not found", + loc{tt_, from}, loc{tt_, to}); + return *it; + }; + + auto const get_transport_info = [&](segment_idx_t const s, + event_type const ev_type) + -> std::tuple { + auto const d = day_idx_[s]; + auto const t = state_.tbd_.segment_transports_[s]; + auto const i = static_cast( + to_idx(s - state_.tbd_.transport_first_segment_[t] + + (ev_type == event_type::kArr ? 1 : 0))); + auto const loc_seq = tt_.route_location_seq_[tt_.transport_route_[t]]; + return {{t, d}, + i, + stop{loc_seq[i]}.location_idx(), + tt_.event_time({t, d}, i, ev_type)}; + }; + + auto const get_run_leg = + [&](segment_idx_t const arrival_segment) -> journey::leg { + auto const [transport, arr_stop_idx, arr_l, arr_time] = + get_transport_info(arrival_segment, event_type::kArr); + auto const [_, dep_stop_idx, dep_l, dep_time] = + get_transport_info(departure_segment, event_type::kDep); + return { + direction::kForward, + dep_l, + arr_l, + dep_time, + arr_time, + journey::run_enter_exit{ + rt::run{ + .t_ = transport, + .stop_range_ = {static_cast(0U), + static_cast( + tt_.route_location_seq_ + [tt_.transport_route_[transport.t_idx_]] + .size())}}, + dep_stop_idx, arr_stop_idx}}; + }; + + // Last leg + if (q.dest_match_mode_ == location_match_mode::kIntermodal) { + auto const offset_it = + utl::find_if(q.destination_, [&](routing::offset const& o) { + return o.target() == arrival_location && o.duration() == offset; + }); + utl::verify(offset_it != q.destination_.end(), + "Reconstruction failed because end_segment is invalid"); + j.legs_.emplace_back(direction::kForward, arrival_location, + get_special_station(special_station::kEnd), + get_time(end_segment_, tt_, event_type::kArr, + state_.tbd_, day_idx_[end_segment_]), + j.arrival_time(), *offset_it); + j.legs_.push_back(get_run_leg(end_segment_)); + } else { + auto arr_time = get_time(end_segment_, tt_, event_type::kArr, state_.tbd_, + day_idx_[end_segment_]); + auto const handle_fp = [&](footpath const& fp) { + if (arr_time + fp.duration() != j.arrival_time() || + !has_offset(q.destination_, q.dest_match_mode_, fp.target())) { + return false; + } + j.legs_.emplace_back(direction::kForward, arrival_location, fp.target(), + arr_time, j.arrival_time(), fp); + j.legs_.push_back(get_run_leg(end_segment_)); + return true; + }; + bool found_path = handle_fp(footpath{arrival_location, duration_t{0}}); + if (!found_path) { + for (auto const fp : tt_.locations_.footpaths_out_[state_.tbd_.prf_idx_] + [arrival_location]) { + if (handle_fp(fp)) { + found_path = true; + break; + } + } + } + utl::verify(found_path, "Reconstruction found no footpath to destination"); + } + j.dest_ = j.legs_.back().to_; + + // intermediary legs + segment_idx_t p = pred_[departure_segment]; + while (!is_invalid(p)) { + auto const [transport, arr_stop_idx, arr_l, arr_time] = + get_transport_info(p, event_type::kArr); + auto const fp = get_fp(arr_l, j.legs_.back().from_); + j.legs_.emplace_back(direction::kForward, arr_l, j.legs_.back().from_, + arr_time, arr_time + fp.duration(), fp); + departure_segment = get_departure_segment(p); + j.legs_.push_back(get_run_leg(p)); + p = pred_[departure_segment]; + } + + // First leg + auto const start_time = j.start_time_; + auto const first_dep_l = j.legs_.back().from_; + auto const first_dep_time = j.legs_.back().dep_time_; + if (q.start_match_mode_ == location_match_mode::kIntermodal) { + auto const offset_it = + utl::find_if(q.start_, [&](routing::offset const& o) { + return o.target() == first_dep_l && + start_time + o.duration() <= first_dep_time; + }); + utl::verify(offset_it != end(q.start_), + "Reconstruction failed because no start offset was found"); + j.legs_.emplace_back(direction::kForward, + get_special_station(special_station::kStart), + first_dep_l, first_dep_time - offset_it->duration(), + first_dep_time, *offset_it); + } else { + for (auto const fp : + tt_.locations_.footpaths_in_[state_.tbd_.prf_idx_][first_dep_l]) { + if (start_time + fp.duration() <= first_dep_time && + has_offset(q.start_, q.start_match_mode_, fp.target())) { + j.legs_.emplace_back(direction::kForward, fp.target(), first_dep_l, + first_dep_time - fp.duration(), first_dep_time, + fp); + break; + } + } + } + + std::ranges::reverse(j.legs_); +} + +} // namespace nigiri::routing::tb::a_star diff --git a/test/routing/tb_a_star_pareto_verification.cc b/test/routing/tb_a_star_pareto_verification.cc new file mode 100644 index 000000000..71a7f7bd9 --- /dev/null +++ b/test/routing/tb_a_star_pareto_verification.cc @@ -0,0 +1,557 @@ +#include +#include +#include +#include "nigiri/routing/tb/preprocess.h" +#include "nigiri/routing/tb/tb_a_star/a_star.h" + +#include "nigiri/loader/gtfs/load_timetable.h" +#include "nigiri/loader/hrd/load_timetable.h" +#include "nigiri/loader/init_finish.h" +#include "../loader/hrd/hrd_timetable.h" +#include "gtest/gtest.h" +namespace nigiri::routing::tb { + +using namespace date; +using namespace nigiri; +using namespace nigiri::routing; +using namespace nigiri::loader; +using namespace std::chrono_literals; +using namespace nigiri::test_data::hrd_timetable; + +timetable load_gtfs(auto const& files) { + timetable tt; + tt.date_range_ = interval{sys_days{2021_y / March / 1}, + sys_days{2021_y / March / 8}}; + loader::register_special_stations(tt); + loader::gtfs::load_timetable({}, source_idx_t{0}, files(), tt); + loader::finalize(tt); + return tt; +} + +timetable load_hrd(auto const& files) { + timetable tt; + tt.date_range_ = full_period(); + register_special_stations(tt); + hrd::load_timetable(source_idx_t{0U}, loader::hrd::hrd_5_20_26, files(), tt); + finalize(tt); + return tt; +} +constexpr double EPSILON = 0.00001; +unsigned int calculate_transfer_factor(std::vector const& journeys, + std::size_t index) { + double higher_bound = std::numeric_limits::max(); + double lower_bound = std::numeric_limits::min(); + for (std::size_t j = 0; j < journeys.size(); ++j) { + if (j == index) continue; + utl::verify(journeys[index].transfers_ != journeys[j].transfers_, + "Two travels with same amount of transfers"); + auto factor = static_cast((journeys[j].arrival_time() - + journeys[index].arrival_time()) + .count()) / + (journeys[index].transfers_ - journeys[j].transfers_); + if (journeys[index].transfers_ < journeys[j].transfers_ && + factor > lower_bound) + lower_bound = factor; + if (journeys[index].transfers_ > journeys[j].transfers_ && + factor < higher_bound) + higher_bound = factor; + } + if (std::numeric_limits::max() - higher_bound < EPSILON) + return std::abs(lower_bound - + static_cast( + lower_bound))(lower_bound)> - + 2 + ? static_cast(lower_bound + 1) + : (std::ceil(lower_bound) > 0 + ? static_cast(std::ceil(lower_bound)) + : 0); + int higher_bound_floor = static_cast(higher_bound); + auto result = (higher_bound - higher_bound_floor < EPSILON) + ? higher_bound - 1 + : higher_bound_floor; + utl::verify(result > lower_bound, "No Integer exists between bounds"); + if (result < 0) throw std::runtime_error("Negative factor"); + return static_cast(result); +} + +void verify_pareto_set(auto const& files, + std::string_view const& start, + std::string_view const& stop, + unixtime_t const& start_time, + int expectedNumberOfJourneys) { + auto const tt = load_gtfs(files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + + query q{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {start, source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({stop, source_idx_t{0}}), + 0_minutes, 0U}}}; + + static auto search_state = routing::search_state{}; + auto algo_state = query_state{tt, tbd}; + + search> tb_searcher{ + tt, nullptr, search_state, algo_state, q}; + + auto results = tb_searcher.execute().journeys_->els_; + + EXPECT_EQ(results.size(), expectedNumberOfJourneys); + for (std::size_t i = 0; i < results.size(); ++i) { + if (results.size() > 1) + a_star::transfer_factor = calculate_transfer_factor(results, i); + static auto a_star_search_state = routing::search_state{}; + auto a_star_algo_state = a_star::a_star_state{tbd}; + search a_star_searcher{ + tt, nullptr, a_star_search_state, a_star_algo_state, q}; + + auto a_star = a_star_searcher.execute().journeys_->els_.front(); + EXPECT_EQ(results[i], a_star_searcher.execute().journeys_->els_.front()); + } + + search_state = routing::search_state{}; + auto raptor_algo_state = raptor_state{}; + + search> + raptor_searcher{tt, nullptr, search_state, raptor_algo_state, + q, std::nullopt}; + + results = raptor_searcher.execute().journeys_->els_; + + EXPECT_EQ(results.size(), expectedNumberOfJourneys); + for (std::size_t i = 0; i < results.size(); ++i) { + if (results.size() > 1) + a_star::transfer_factor = calculate_transfer_factor(results, i); + static auto a_star_search_state = routing::search_state{}; + auto a_star_algo_state = a_star::a_star_state{tbd}; + + search a_star_searcher{ + tt, nullptr, a_star_search_state, a_star_algo_state, q}; + + auto found_journey = a_star_searcher.execute().journeys_->els_.front(); + found_journey.legs_.pop_back(); + EXPECT_EQ(results[i], found_journey); + } +} + +loader::mem_dir simple_pareto_verification_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S3",2 +R1,DTA,R1,R1,"S0 -> S4",2 +R2,DTA,R2,R2,"S2 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 +R2,MON,R2_MON,R2_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R0_MON,03:00:00,03:00:00,S3,3,0,0 +R1_MON,00:00:00,00:00:00,S0,0,0,0 +R1_MON,01:00:00,01:00:00,S2,1,0,0 +R1_MON,02:00:00,02:00:00,S4,2,0,0 +R2_MON,01:10:00,01:10:00,S2,0,0,0 +R2_MON,01:30:00,01:30:00,S3,1,0,0 +)"); +} +TEST(tb_a_star, pareto_verification) { + verify_pareto_set(simple_pareto_verification_files, "S0", "S3", + sys_days{year{2021} / March / day{1}}, 2); +} + +TEST(tb_a_star, pareto_verification_with_early_start_time) { + verify_pareto_set(simple_pareto_verification_files, "S0", "S3", + sys_days{year{2021} / February / day{28}} + 23h, 2); +} + +loader::mem_dir later_pareto_verification_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S3",2 +R1,DTA,R1,R1,"S0 -> S4",2 +R2,DTA,R2,R2,"S2 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 +R2,MON,R2_MON,R2_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,01:00:00,01:00:00,S0,0,0,0 +R0_MON,02:00:00,02:00:00,S1,1,0,0 +R0_MON,04:00:00,04:00:00,S3,3,0,0 +R1_MON,01:00:00,01:00:00,S0,0,0,0 +R1_MON,02:00:00,02:00:00,S2,1,0,0 +R1_MON,03:00:00,03:00:00,S4,2,0,0 +R2_MON,02:10:00,02:10:00,S2,0,0,0 +R2_MON,02:30:00,02:30:00,S3,1,0,0 +)"); +} +TEST(tb_a_star, pareto_verification_early_start_time_same_day) { + verify_pareto_set(simple_pareto_verification_files, "S0", "S3", + sys_days{year{2021} / March / day{1}}, 2); +} +loader::mem_dir overnight_pareto_verification_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S3",2 +R1,DTA,R1,R1,"S0 -> S4",2 +R2,DTA,R2,R2,"S2 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 +R2,MON,R2_MON,R2_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,10:00:00,10:00:00,S0,0,0,0 +R0_MON,11:00:00,11:00:00,S1,1,0,0 +R0_MON,26:00:00,26:00:00,S3,3,0,0 +R1_MON,10:00:00,10:00:00,S0,0,0,0 +R1_MON,11:00:00,11:00:00,S2,1,0,0 +R1_MON,12:00:00,12:00:00,S4,2,0,0 +R2_MON,11:10:00,11:10:00,S2,0,0,0 +R2_MON,24:30:00,24:30:00,S3,1,0,0 +)"); +} +TEST(tb_a_star, overnight_pareto_verification) { + verify_pareto_set(overnight_pareto_verification_files, "S0", "S3", + sys_days{year{2021} / March / day{1}} + 10h, 2); +} + +loader::mem_dir three_journeys_pareto_verification_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, +S5,S5,,,,,, +S6,S6,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S3",2 +R1,DTA,R1,R1,"S0 -> S4",2 +R2,DTA,R2,R2,"S2 -> S3",2 +R3,DTA,R2,R2,"S2 -> S3",2 +R4,DTA,R2,R2,"S2 -> S3",2 +R5,DTA,R2,R2,"S2 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 +R2,MON,R2_MON,R2_MON,1 +R3,MON,R3_MON,R3_MON,1 +R4,MON,R4_MON,R4_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R0_MON,03:00:00,03:00:00,S3,3,0,0 +R1_MON,00:00:00,00:00:00,S0,0,0,0 +R1_MON,01:00:00,01:00:00,S2,1,0,0 +R1_MON,02:00:00,02:00:00,S4,2,0,0 +R2_MON,01:10:00,01:10:00,S2,0,0,0 +R2_MON,02:30:00,02:30:00,S3,1,0,0 +R3_MON,01:10:00,01:10:00,S2,0,0,0 +R3_MON,01:30:00,01:30:00,S5,1,0,0 +R3_MON,02:00:00,02:00:00,S6,2,0,0 +R4_MON,01:40:00,01:40:00,S5,0,0,0 +R4_MON,02:10:00,02:10:00,S3,0,0,0 +)"); +} +TEST(tb_a_star, three_journeys_pareto_verification) { + verify_pareto_set(three_journeys_pareto_verification_files, "S0", "S3", + sys_days{year{2021} / March / day{1}}, 3); +} + +TEST(tb_a_star, abc_pareto_verification) { + auto const tt = load_hrd(files_abc); + auto const tbd = tb::preprocess(tt, profile_idx_t{0}); + + query q{.start_time_ = interval{unixtime_t{sys_days{March / 30 / 2020}} + 5h, + unixtime_t{sys_days{March / 30 / 2020}} + 6h}, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"0000001", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"0000003", source_idx_t{0}}), + 0_minutes, 0U}}}; + + static auto search_state = routing::search_state{}; + auto algo_state = query_state{tt, tbd}; + + search> tb_searcher{ + tt, nullptr, search_state, algo_state, q}; + + auto results = tb_searcher.execute().journeys_->els_; + + static auto a_star_search_state = routing::search_state{}; + auto a_star_algo_state = a_star::a_star_state{tbd}; + + search a_star_searcher{ + tt, nullptr, a_star_search_state, a_star_algo_state, q}; + + auto result_a_star = a_star_searcher.execute().journeys_->els_; + EXPECT_TRUE(result_a_star.front() == results.front() || + result_a_star.front() == results.back()); + EXPECT_TRUE(result_a_star.back() == results.front() || + result_a_star.back() == results.back()); + EXPECT_NE(result_a_star.back(), result_a_star.front()); + + search_state = routing::search_state{}; + auto raptor_algo_state = raptor_state{}; + + search> + raptor_searcher{tt, nullptr, search_state, raptor_algo_state, + q, std::nullopt}; + + results = raptor_searcher.execute().journeys_->els_; + result_a_star.front().legs_.pop_back(); + result_a_star.back().legs_.pop_back(); + EXPECT_TRUE(result_a_star.front() == results.front() || + result_a_star.front() == results.back()); + EXPECT_TRUE(result_a_star.back() == results.front() || + result_a_star.back() == results.back()); + EXPECT_NE(result_a_star.back(), result_a_star.front()); +} + +mem_dir earlier_stop_transfer_files() { + return mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S4",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON0,R0_MON0,0 +R0,MON,R0_MON1,R0_MON1,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON0,00:00:00,00:00:00,S0,0,0,0 +R0_MON0,01:00:00,01:00:00,S1,1,0,0 +R0_MON0,02:00:00,02:00:00,S2,2,0,0 +R0_MON0,03:00:00,03:00:00,S3,3,0,0 +R0_MON0,04:00:00,04:00:00,S1,4,0,0 +R0_MON0,05:00:00,05:00:00,S4,5,0,0 +R0_MON1,04:00:00,04:00:00,S0,0,0,0 +R0_MON1,05:00:00,05:00:00,S1,1,0,0 +R0_MON1,06:00:00,06:00:00,S2,2,0,0 +R0_MON1,07:00:00,07:00:00,S3,3,0,0 +R0_MON1,08:00:00,08:00:00,S1,4,0,0 +R0_MON1,09:00:00,09:00:00,S4,5,0,0 +)"); +} +TEST(tb_a_star, earlier_stop_transfer_verification) { + verify_pareto_set(earlier_stop_transfer_files, "S3", "S2", + sys_days{year{2021} / March / day{1}}, 1); +} + +mem_dir another_longer_files() { + return mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, +S5,S5,,,,,, +S6,S6,,,,,, +S7,S7,,,,,, +S8,S8,,,,,, +S9,S9,,,,,, +S10,S10,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S1 -> S3",2 +R1,DTA,R1,R1,"S3 -> S7",2 +R2,DTA,R2,R2,"S7 -> S4",2 +R3,DTA,R3,R3,"S4 -> S10",2 +R4,DTA,R4,R4,"S1 -> S7",2 +R5,DTA,R5,R5,"S7 -> S9",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,0 +R1,MON,R1_MON,R1_MON,0 +R2,MON,R2_MON,R2_MON,0 +R3,MON,R3_MON,R3_MON,0 +R4,MON,R4_MON,R4_MON,0 +R5,MON,R5_MON,R5_MON,0 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S1,0,0,0 +R0_MON,01:00:00,01:00:00,S2,1,0,0 +R0_MON,02:00:00,02:00:00,S3,2,0,0 +R1_MON,02:10:00,02:10:00,S3,0,0,0 +R1_MON,03:00:00,03:00:00,S7,1,0,0 +R2_MON,03:10:00,03:10:00,S7,0,0,0 +R2_MON,04:00:00,04:00:00,S8,1,0,0 +R2_MON,05:00:00,05:00:00,S5,2,0,0 +R2_MON,06:00:00,06:00:00,S4,3,0,0 +R3_MON,06:10:00,06:10:00,S4,0,0,0 +R3_MON,07:00:00,07:00:00,S10,1,0,0 +R4_MON,00:00:00,00:00:00,S1,0,0,0 +R4_MON,01:00:00,01:00:00,S6,1,0,0 +R4_MON,03:00:00,03:00:00,S7,2,0,0 +R5_MON,03:10:00,03:10:00,S7,0,0,0 +R5_MON,04:00:00,04:00:00,S9,1,0,0 +)"); +} +TEST(tb_a_star, another_longer_gtfs) { + verify_pareto_set(another_longer_files, "S1", "S10", + sys_days{year{2021} / March / day{1}}, 1); +} + +mem_dir overnight_files() { + return mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 +TUE,0,1,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S2",2 +R1,DTA,R1,R1,"S1 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,0 +R1,TUE,R1_TUE,R1_TUE,0 + + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,23:30:00,23:30:00,S0,0,0,0 +R0_MON,23:55:00,23:55:00,S1,1,0,0 +R0_MON,25:30:00,25:30:00,S2,2,0,0 +R1_TUE,00:05:00,00:05:00,S1,0,0,0 +R1_TUE,01:00:00,01:00:00,S2,1,0,0 +)"); +} +TEST(tb_a_star, overnight_pareto_verification_files) { + verify_pareto_set(overnight_files, "S0", "S2", + sys_days{year{2021} / March / day{1}} + 23h + 30min, 2); +} +} // namespace nigiri::routing::tb diff --git a/test/routing/tb_a_star_test.cc b/test/routing/tb_a_star_test.cc new file mode 100644 index 000000000..7f8ef71ed --- /dev/null +++ b/test/routing/tb_a_star_test.cc @@ -0,0 +1,1590 @@ +#include "nigiri/routing/tb/tb_a_star/a_star.h" + +#include "nigiri/routing/tb/preprocess.h" + +#include +#include + +#include "gtest/gtest.h" + +#include "nigiri/loader/gtfs/load_timetable.h" +#include "nigiri/loader/init_finish.h" + +namespace nigiri::routing::tb::a_star { + +using namespace date; +using namespace std::chrono_literals; + +timetable load_gtfs(auto const& files) { + timetable tt; + tt.date_range_ = {sys_days{2021_y / March / 1}, sys_days{2021_y / March / 8}}; + loader::register_special_stations(tt); + loader::gtfs::load_timetable({}, source_idx_t{0}, files(), tt); + loader::finalize(tt); + return tt; +} + +void check_neighbours(tb_data const& tbd) { + for (segment_idx_t i(0); i < tbd.segment_transports_.size(); ++i) { + auto [neighbours, next_exists] = get_neighbours(i, tbd, day_idx_t{0}); + auto end = next_exists ? neighbours.size() - 1 : neighbours.size(); + std::vector seen(tbd.segment_transports_.size(), false); + for (std::size_t j = 0; j < end; ++j) { + auto neighbour = neighbours[j]; + EXPECT_TRUE(utl::any_of(tbd.segment_transfers_[i], + [neighbour](transfer const& t) { + return t.to_segment_ == neighbour; + })); + EXPECT_FALSE(seen[neighbour.v_]); + seen[neighbour.v_] = true; + } + if (next_exists) { + EXPECT_EQ(i + 1, neighbours.back()); + EXPECT_EQ(tbd.segment_transports_[i], tbd.segment_transports_[i + 1]); + } + } +} + +std::vector get_segments(tb_a_star const& a_star_exec) { + std::vector result; + auto p = a_star_exec.end_segment_; + auto invalid_segment = segment_idx_t::invalid(); + while (p != invalid_segment) { + result.push_back(p); + p = a_star_exec.pred_[p]; + } + std::ranges::reverse(result); + return result; +} + +loader::mem_dir no_transfer_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, +S5,S5,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S4",2 +R1,DTA,R1,R1,"S2 -> S5 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R0_MON,02:00:00,02:00:00,S2,2,0,0 +R0_MON,03:00:00,03:00:00,S3,3,0,0 +R0_MON,04:00:00,04:00:00,S4,4,0,0 +R1_MON,02:00:10,02:10:00,S2,0,0,0 +R1_MON,02:30:00,02:30:00,S5,1,0,0 +R1_MON,02:50:00,02:50:00,S3,2,0,0 +)"); +} + +TEST(tb_a_star, a_star_no_transfer) { + auto const tt = load_gtfs(no_transfer_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + auto start_segment = tbd.transport_first_segment_[transport_idx_t(0)]; + auto end_segment = tbd.transport_first_segment_[transport_idx_t(1)] - 1; + auto day_idx = tt.day_idx(year_month_day{year{2021}, March, day{1}}); + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + a_star_exec.travel_time_lower_bound_ = + std::vector(tt.n_locations(), 0); + a_star_exec.queue_.push(std::pair(start_segment, duration_t(0))); + a_star_exec.state_.end_reachable_.resize(tbd.segment_transports_.size()); + a_star_exec.state_.end_reachable_.set(end_segment); + a_star_exec.transfers_[start_segment] = 0; + a_star_exec.day_idx_[start_segment] = + day_idx; // get_arrival_day(start_time,start_segment,tt,tbd); + a_star_exec.state_.dist_to_dest_ = + hash_map(tbd.segment_transports_.size()); + + auto const it = a_star_exec.state_.dist_to_dest_.find(end_segment); + if (it == end(a_star_exec.state_.dist_to_dest_)) { + a_star_exec.state_.dist_to_dest_.emplace_hint(it, end_segment, + duration_t::zero()); + } else { + it->second = duration_t::zero(); + } + + pareto_set journeys; + a_star_exec.execute(start_time, std::numeric_limits::max(), + unixtime_t::max(), profile_idx_t{0}, journeys); + auto result = get_segments(a_star_exec); + std::vector expected; + expected.emplace_back(0); + expected.emplace_back(1); + expected.emplace_back(2); + expected.emplace_back(3); + EXPECT_EQ(result, expected); + EXPECT_EQ(a_star_exec.end_segment_, end_segment); + + for (auto const& segment : result) { + EXPECT_EQ(a_star_exec.day_idx_[segment], day_idx); + EXPECT_EQ(a_star_exec.transfers_[segment], 0); + } + + EXPECT_EQ(journeys.els_.size(), 1); + journey journey = journeys.els_[0]; + EXPECT_TRUE(journey.legs_.empty()); + EXPECT_EQ(journey.start_time_, start_time); + EXPECT_EQ(journey.transfers_, 0); + EXPECT_EQ(journey.dest_, location_idx_t::invalid()); + EXPECT_EQ(journey.dest_time_, start_time + 4h); + + a_star_exec.reconstruct( + query{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"S0", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"S4", source_idx_t{0}}), + 0_minutes, 0U}}}, + journey); + EXPECT_EQ(journey.legs_.size(), 2); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + EXPECT_EQ(journey.transfers_, 0); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 4h); + EXPECT_EQ(journey.legs_.back().dep_time_, start_time + 4h); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 4h); + + check_neighbours(tbd); +} + +loader::mem_dir with_transfer_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S2",2 +R1,DTA,R1,R1,"S1 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R0_MON,02:00:00,02:00:00,S2,2,0,0 +R1_MON,01:10:00,01:10:00,S1,0,0,0 +R1_MON,02:00:00,02:00:00,S3,1,0,0 +)"); +} + +TEST(tb_a_star, a_star_with_transfer) { + auto const tt = load_gtfs(with_transfer_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + auto start_segment = tbd.transport_first_segment_[transport_idx_t(0)]; + auto end_segment = segment_idx_t(tbd.segment_transports_.size() - 1); + auto day_idx = tt.day_idx(year_month_day{year{2021}, March, day{1}}); + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + a_star_exec.travel_time_lower_bound_ = + std::vector(tt.n_locations(), 0); + a_star_exec.queue_.push(std::pair(start_segment, duration_t(0))); + a_star_exec.state_.end_reachable_.resize(tbd.segment_transports_.size()); + a_star_exec.state_.end_reachable_.set(end_segment); + a_star_exec.transfers_[start_segment] = 0; + a_star_exec.day_idx_[start_segment] = + day_idx; // get_arrival_day(start_time,start_segment,tt,tbd); + a_star_exec.state_.dist_to_dest_ = + hash_map(tbd.segment_transports_.size()); + + auto const it = a_star_exec.state_.dist_to_dest_.find(end_segment); + if (it == end(a_star_exec.state_.dist_to_dest_)) { + a_star_exec.state_.dist_to_dest_.emplace_hint(it, end_segment, + duration_t::zero()); + } else { + it->second = duration_t::zero(); + } + + pareto_set journeys; + a_star_exec.execute(start_time, std::numeric_limits::max(), + unixtime_t::max(), profile_idx_t{0}, journeys); + auto result = get_segments(a_star_exec); + + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], start_segment); + EXPECT_EQ(result[1], end_segment); + + for (auto const& segment : result) { + EXPECT_EQ(a_star_exec.day_idx_[segment], day_idx); + } + EXPECT_EQ(a_star_exec.transfers_[start_segment], 0); + EXPECT_EQ(a_star_exec.transfers_[end_segment], 1); + EXPECT_EQ(a_star_exec.end_segment_, end_segment); + + EXPECT_EQ(journeys.els_.size(), 1); + journey journey = journeys.els_[0]; + EXPECT_TRUE(journey.legs_.empty()); + EXPECT_EQ(journey.start_time_, start_time); + EXPECT_EQ(journey.transfers_, 1); + EXPECT_EQ(journey.dest_, location_idx_t::invalid()); + EXPECT_EQ(journey.dest_time_, start_time + 2h); + + a_star_exec.reconstruct( + query{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"S0", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"S3", source_idx_t{0}}), + 0_minutes, 0U}}}, + journey); + EXPECT_EQ(journey.legs_.size(), 4); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + EXPECT_EQ(journey.transfers_, 1); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 1h); + EXPECT_EQ(journey.legs_[1].dep_time_, start_time + 1h); + EXPECT_EQ(journey.legs_[1].arr_time_, start_time + 1h + 2min); + EXPECT_EQ(journey.legs_[2].dep_time_, start_time + 1h + 10min); + EXPECT_EQ(journey.legs_[2].arr_time_, start_time + 2h); + EXPECT_EQ(journey.legs_[3].dep_time_, start_time + 2h); + EXPECT_EQ(journey.legs_[3].arr_time_, start_time + 2h); + + check_neighbours(tbd); +} + +loader::mem_dir longer_trips_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, +S4,S4,,,,,, +S5,S5,,,,,, +S6,S6,,,,,, +S7,S7,,,,,, +S8,S8,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S4",2 +R1,DTA,R1,R1,"S3 -> S6 -> S8",2 +R2,DTA,R2,R2,"S6 -> S7",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 +R2,MON,R2_MON,R2_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R0_MON,02:00:00,02:00:00,S2,2,0,0 +R0_MON,03:00:00,03:00:00,S3,3,0,0 +R0_MON,04:00:00,04:00:00,S4,4,0,0 +R1_MON,03:10:00,03:10:00,S3,0,0,0 +R1_MON,04:00:00,04:00:00,S5,1,0,0 +R1_MON,05:00:00,05:00:00,S6,2,0,0 +R1_MON,06:00:00,06:00:00,S8,3,0,0 +R2_MON,05:10:00,05:10:00,S6,0,0,0 +R2_MON,06:00:00,06:00:00,S7,1,0,0 +)"); +} + +TEST(tb_a_star, a_star_longer_routes) { + auto const tt = load_gtfs(longer_trips_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + auto start_segment = tbd.transport_first_segment_[transport_idx_t(0)]; + auto end_segment = segment_idx_t(tbd.segment_transports_.size() - 1); + auto day_idx = tt.day_idx(year_month_day{year{2021}, March, day{1}}); + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + a_star_exec.travel_time_lower_bound_ = + std::vector(tt.n_locations(), 0); + a_star_exec.queue_.push(std::pair(start_segment, duration_t(0))); + a_star_exec.state_.end_reachable_.resize(tbd.segment_transports_.size()); + a_star_exec.state_.end_reachable_.set(end_segment); + a_star_exec.transfers_[start_segment] = 0; + a_star_exec.day_idx_[start_segment] = + day_idx; // get_arrival_day(start_time,start_segment,tt,tbd); + a_star_exec.state_.dist_to_dest_ = + hash_map(tbd.segment_transports_.size()); + + auto const it = a_star_exec.state_.dist_to_dest_.find(end_segment); + if (it == end(a_star_exec.state_.dist_to_dest_)) { + a_star_exec.state_.dist_to_dest_.emplace_hint(it, end_segment, + duration_t::zero()); + } else { + it->second = duration_t::zero(); + } + + pareto_set journeys; + a_star_exec.execute(start_time, std::numeric_limits::max(), + unixtime_t::max(), profile_idx_t{0}, journeys); + auto result = get_segments(a_star_exec); + std::vector expected(6); + expected[0] = start_segment; // S0 to S1 + expected[1] = start_segment + 1; // S1 to S2 + expected[2] = start_segment + 2; // S2 to S3 + expected[3] = tbd.transport_first_segment_[transport_idx_t( + 1)]; // S3 to S5 in the next transport + expected[4] = expected[3] + 1; // S5 to S6 + expected[5] = end_segment; // S6 to S7 + EXPECT_EQ(expected, result); + EXPECT_EQ(a_star_exec.end_segment_, end_segment); + + for (auto const& segment : result) { + EXPECT_EQ(a_star_exec.day_idx_[segment], day_idx); + } + EXPECT_EQ(a_star_exec.transfers_[start_segment], 0); + EXPECT_EQ(a_star_exec.transfers_[start_segment + 1], 0); + EXPECT_EQ(a_star_exec.transfers_[start_segment + 2], 0); + EXPECT_EQ(a_star_exec.transfers_[expected[3]], 1); + EXPECT_EQ(a_star_exec.transfers_[expected[4]], 1); + EXPECT_EQ(a_star_exec.transfers_[end_segment], 2); + + EXPECT_EQ(journeys.els_.size(), 1); + journey journey = journeys.els_[0]; + EXPECT_TRUE(journey.legs_.empty()); + EXPECT_EQ(journey.start_time_, start_time); + EXPECT_EQ(journey.transfers_, 2); + EXPECT_EQ(journey.dest_, location_idx_t::invalid()); + EXPECT_EQ(journey.dest_time_, start_time + 6h); + + a_star_exec.reconstruct( + query{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"S0", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"S7", source_idx_t{0}}), + 0_minutes, 0U}}}, + journey); + EXPECT_EQ(journey.legs_.size(), 6); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{2}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + EXPECT_EQ(journey.transfers_, 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 3h); + EXPECT_EQ(journey.legs_[1].dep_time_, start_time + 3h); + EXPECT_EQ(journey.legs_[1].arr_time_, start_time + 3h + 2min); + EXPECT_EQ(journey.legs_[2].dep_time_, start_time + 3h + 10min); + EXPECT_EQ(journey.legs_[2].arr_time_, start_time + 5h); + EXPECT_EQ(journey.legs_[3].dep_time_, start_time + 5h); + EXPECT_EQ(journey.legs_[3].arr_time_, start_time + 5h + 2min); + EXPECT_EQ(journey.legs_[4].dep_time_, start_time + 5h + 10min); + EXPECT_EQ(journey.legs_[4].arr_time_, start_time + 6h); + EXPECT_EQ(journey.legs_[5].dep_time_, start_time + 6h); + EXPECT_EQ(journey.legs_[5].arr_time_, start_time + 6h); + + check_neighbours(tbd); +} + +loader::mem_dir overnight_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 +TUE,0,1,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S1",2 +R1,DTA,R1,R1,"S1 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,TUE,R1_TUE,R1_TUE,2 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,10:00:00,10:00:00,S0,0,0,0 +R0_MON,12:00:00,12:00:00,S1,1,0,0 +R1_TUE,00:00:00,00:00:00,S1,0,0,0 +R1_TUE,08:00:00,08:00:00,S2,1,0,0 +)"); +} + +TEST(tb_a_star, overnight_travel) { + auto const tt = load_gtfs(overnight_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + auto start_segment = tbd.transport_first_segment_[transport_idx_t(0)]; + auto end_segment = segment_idx_t(tbd.segment_transports_.size() - 1); + auto day_idx = tt.day_idx(year_month_day{year{2021}, March, day{1}}); + unixtime_t start_time = sys_days{year{2021} / March / day{1}} + 10h; + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + a_star_exec.travel_time_lower_bound_ = + std::vector(tt.n_locations(), 0); + a_star_exec.queue_.push(std::pair(start_segment, duration_t(0))); + a_star_exec.state_.end_reachable_.resize(tbd.segment_transports_.size()); + a_star_exec.state_.end_reachable_.set(end_segment); + a_star_exec.transfers_[start_segment] = 0; + a_star_exec.day_idx_[start_segment] = day_idx; + a_star_exec.state_.dist_to_dest_ = + hash_map(tbd.segment_transports_.size()); + + auto const it = a_star_exec.state_.dist_to_dest_.find(end_segment); + if (it == end(a_star_exec.state_.dist_to_dest_)) { + a_star_exec.state_.dist_to_dest_.emplace_hint(it, end_segment, + duration_t::zero()); + } else { + it->second = duration_t::zero(); + } + + pareto_set journeys; + a_star_exec.execute(start_time, std::numeric_limits::max(), + unixtime_t::max(), profile_idx_t{0}, journeys); + auto result = get_segments(a_star_exec); + + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], start_segment); + EXPECT_EQ(result[1], end_segment); + + EXPECT_EQ(a_star_exec.day_idx_[start_segment], day_idx); + EXPECT_EQ(a_star_exec.day_idx_[end_segment], day_idx + 1); + + EXPECT_EQ(a_star_exec.transfers_[start_segment], 0); + EXPECT_EQ(a_star_exec.transfers_[end_segment], 1); + EXPECT_EQ(a_star_exec.end_segment_, end_segment); + + EXPECT_EQ(journeys.els_.size(), 1); + journey journey = journeys.els_[0]; + EXPECT_TRUE(journey.legs_.empty()); + EXPECT_EQ(journey.start_time_, start_time); + EXPECT_EQ(journey.transfers_, 1); + EXPECT_EQ(journey.dest_, location_idx_t::invalid()); + EXPECT_EQ(journey.dest_time_, start_time + 22h); + + a_star_exec.reconstruct( + query{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"S0", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"S2", source_idx_t{0}}), + 0_minutes, 0U}}}, + journey); + EXPECT_EQ(journey.legs_.size(), 4); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + EXPECT_EQ(journey.transfers_, 1); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 2h); + EXPECT_EQ(journey.legs_[1].dep_time_, start_time + 2h); + EXPECT_EQ(journey.legs_[1].arr_time_, start_time + 2h + 2min); + EXPECT_EQ(journey.legs_[2].dep_time_, start_time + 14h); + EXPECT_EQ(journey.legs_[2].arr_time_, start_time + 22h); + EXPECT_EQ(journey.legs_[3].dep_time_, start_time + 22h); + EXPECT_EQ(journey.legs_[3].arr_time_, start_time + 22h); + + check_neighbours(tbd); +} + +loader::mem_dir overnight_files_without_transfer() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,10:00:00,10:00:00,S0,0,0,0 +R0_MON,12:00:00,12:00:00,S1,1,0,0 +R0_MON,24:00:00,24:00:00,S2,2,0,0 +)"); +} +TEST(tb_a_star, overnight_travel_without_transfer) { + auto const tt = load_gtfs(overnight_files_without_transfer); + auto const tbd = preprocess(tt, profile_idx_t{0}); + auto start_segment = tbd.transport_first_segment_[transport_idx_t(0)]; + auto end_segment = segment_idx_t(tbd.segment_transports_.size() - 1); + auto day_idx = tt.day_idx(year_month_day{year{2021}, March, day{1}}); + unixtime_t start_time = sys_days{year{2021} / March / day{1}} + 10h; + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + a_star_exec.travel_time_lower_bound_ = + std::vector(tt.n_locations(), 0); + a_star_exec.queue_.push(std::pair(start_segment, duration_t(0))); + a_star_exec.state_.end_reachable_.resize(tbd.segment_transports_.size()); + a_star_exec.state_.end_reachable_.set(end_segment); + a_star_exec.transfers_[start_segment] = 0; + a_star_exec.day_idx_[start_segment] = day_idx; + a_star_exec.state_.dist_to_dest_ = + hash_map(tbd.segment_transports_.size()); + + auto const it = a_star_exec.state_.dist_to_dest_.find(end_segment); + if (it == end(a_star_exec.state_.dist_to_dest_)) { + a_star_exec.state_.dist_to_dest_.emplace_hint(it, end_segment, + duration_t::zero()); + } else { + it->second = duration_t::zero(); + } + + pareto_set journeys; + a_star_exec.execute(start_time, std::numeric_limits::max(), + unixtime_t::max(), profile_idx_t{0}, journeys); + auto result = get_segments(a_star_exec); + + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], start_segment); + EXPECT_EQ(result[1], end_segment); + + EXPECT_EQ(a_star_exec.day_idx_[start_segment], day_idx); + EXPECT_EQ(a_star_exec.day_idx_[end_segment], day_idx); + + EXPECT_EQ(a_star_exec.transfers_[start_segment], 0); + EXPECT_EQ(a_star_exec.transfers_[end_segment], 0); + EXPECT_EQ(a_star_exec.end_segment_, end_segment); + + EXPECT_EQ(journeys.els_.size(), 1); + journey journey = journeys.els_[0]; + EXPECT_TRUE(journey.legs_.empty()); + EXPECT_EQ(journey.start_time_, start_time); + EXPECT_EQ(journey.transfers_, 0); + EXPECT_EQ(journey.dest_, location_idx_t::invalid()); + EXPECT_EQ(journey.dest_time_, start_time + 14h); + + a_star_exec.reconstruct( + query{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"S0", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"S2", source_idx_t{0}}), + 0_minutes, 0U}}}, + journey); + EXPECT_EQ(journey.legs_.size(), 2); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + EXPECT_EQ(journey.transfers_, 0); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 14h); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 14h); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 14h); + + check_neighbours(tbd); +} + +TEST(tb_a_star, add_starts_one_segment) { + auto const tt = load_gtfs(overnight_files); + auto const tbd = tb::preprocess(tt, profile_idx_t{0}); + auto start_segment = tbd.transport_first_segment_[transport_idx_t(0)]; + unixtime_t start_time = + date::sys_days{date::year{2021} / date::March / date::day{1}} + 10h; + auto stop_value = tt.route_location_seq_[route_idx_t(0)][0]; + stop start_stop(stop_value); + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + + a_star_exec.add_start(start_stop.location_idx(), start_time); + EXPECT_TRUE(a_star_exec.is_start_segment_.test(start_segment)); + for (std::size_t i = 0; i < tbd.segment_transfers_.size(); ++i) { + segment_idx_t segment{i}; + if (segment != start_segment) { + EXPECT_FALSE(a_star_exec.is_start_segment_.test(segment)); + } + } + EXPECT_EQ(a_star_exec.transfers_[segment_idx_t(0)], 0); +} + +loader::mem_dir two_start_segment_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S1",2 +R1,DTA,R1,R1,"S0 -> S3",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,2 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,10:00:00,10:00:00,S0,0,0,0 +R0_MON,12:00:00,12:00:00,S1,1,0,0 +R1_MON,11:00:00,11:00:00,S0,0,0,0 +R1_MON,12:00:00,12:00:00,S2,1,0,0 +R1_MON,13:00:00,13:00:00,S3,2,0,0 +)"); +} + +TEST(tb_a_star, add_starts_two_segment) { + auto const tt = load_gtfs(two_start_segment_files); + auto const tbd = tb::preprocess(tt, profile_idx_t{0}); + unixtime_t start_time = + date::sys_days{date::year{2021} / date::March / date::day{1}} + 10h; + auto stop_value = tt.route_location_seq_[route_idx_t(0)][0]; + stop start_stop(stop_value); + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + + a_star_exec.add_start(start_stop.location_idx(), start_time); + + EXPECT_TRUE(a_star_exec.is_start_segment_.test(segment_idx_t{0})); + EXPECT_TRUE(a_star_exec.is_start_segment_.test(segment_idx_t{1})); + EXPECT_FALSE(a_star_exec.is_start_segment_.test(segment_idx_t{2})); + + EXPECT_EQ(a_star_exec.transfers_[segment_idx_t{0}], 0); + EXPECT_EQ(a_star_exec.transfers_[segment_idx_t{1}], 0); +} + +TEST(tb_a_star, a_star_two_start_segments) { + auto const tt = load_gtfs(two_start_segment_files); + auto const tbd = tb::preprocess(tt, profile_idx_t{0}); + auto end_segment = segment_idx_t(0); + unixtime_t start_time = + date::sys_days{date::year{2021} / date::March / date::day{1}} + 10h; + auto day_idx = tt.day_idx( + date::year_month_day{date::year{2021}, date::month{3}, date::day{1}}); + tb_a_star a_star_exec(tbd.segment_transfers_.size()); + a_star_exec.state_.tbd_ = tbd; + a_star_exec.tt_ = tt; + a_star_exec.travel_time_lower_bound_ = + std::vector(tt.n_locations(), 0); + a_star_exec.state_.end_reachable_.resize(tbd.segment_transports_.size()); + a_star_exec.state_.end_reachable_.set(end_segment); + a_star_exec.state_.end_reachable_.set(end_segment + 1); + a_star_exec.state_.dist_to_dest_ = + hash_map(tbd.segment_transports_.size()); + a_star_exec.state_.dist_to_dest_[segment_idx_t(1)] = duration_t::zero(); + a_star_exec.state_.dist_to_dest_[segment_idx_t(0)] = duration_t(1); + auto stop_value = tt.route_location_seq_[route_idx_t(0)][0]; + stop start_stop(stop_value); + a_star_exec.add_start(start_stop.location_idx(), start_time); + pareto_set journeys; + a_star_exec.execute(start_time, std::numeric_limits::max(), + unixtime_t::max(), profile_idx_t{0}, journeys); + auto result = get_segments(a_star_exec); + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result.front(), segment_idx_t(1)); + EXPECT_EQ(a_star_exec.day_idx_[segment_idx_t(1)], day_idx); + EXPECT_EQ(a_star_exec.day_idx_[segment_idx_t(0)], day_idx); + for (std::size_t i = 0; i < tbd.segment_transfers_.size(); ++i) { + EXPECT_EQ(a_star_exec.transfers_[segment_idx_t{i}], 0); + } + EXPECT_EQ(a_star_exec.end_segment_, segment_idx_t(1)); + + EXPECT_EQ(journeys.els_.size(), 1); + journey journey = journeys.els_[0]; + EXPECT_TRUE(journey.legs_.empty()); + EXPECT_EQ(journey.start_time_, start_time); + EXPECT_EQ(journey.transfers_, 0); + EXPECT_EQ(journey.dest_, location_idx_t::invalid()); + EXPECT_EQ(journey.dest_time_, start_time + 2h); + + a_star_exec.reconstruct( + query{.start_time_ = start_time, + .start_match_mode_ = location_match_mode::kEquivalent, + .dest_match_mode_ = location_match_mode::kEquivalent, + .start_ = {{tt.locations_.location_id_to_idx_.at( + {"S0", source_idx_t{0}}), + 0_minutes, 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at( + {"S2", source_idx_t{0}}), + 0_minutes, 0U}}}, + journey); + EXPECT_EQ(journey.legs_.size(), 2); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]][1]} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + EXPECT_EQ(journey.transfers_, 0); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time + 1h); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 2h); + EXPECT_EQ(journey.legs_.back().dep_time_, start_time + 2h); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 2h); + + check_neighbours(tbd); +} + +TEST(tb_a_star, search_execute) { + auto const tt = load_gtfs(longer_trips_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({"S7", src}), 0_minutes, 0U}}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.els_.size(), 1); + journey journey = result.els_[0]; + + EXPECT_EQ(journey.legs_.size(), 6); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{2}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + + auto s3 = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]][3]} + .location_idx(); + auto s6 = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]][2]} + .location_idx(); + EXPECT_EQ(journey.legs_.front().to_, s3); + EXPECT_EQ(journey.legs_[1].to_, s3); + EXPECT_EQ(journey.legs_[2].to_, s6); + EXPECT_EQ(journey.legs_[3].to_, s6); + + EXPECT_EQ(journey.transfers_, 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 3h); + EXPECT_EQ(journey.legs_[1].dep_time_, start_time + 3h); + EXPECT_EQ(journey.legs_[1].arr_time_, start_time + 3h + 2min); + EXPECT_EQ(journey.legs_[2].dep_time_, start_time + 3h + 10min); + EXPECT_EQ(journey.legs_[2].arr_time_, start_time + 5h); + EXPECT_EQ(journey.legs_[3].dep_time_, start_time + 5h); + EXPECT_EQ(journey.legs_[3].arr_time_, start_time + 5h + 2min); + EXPECT_EQ(journey.legs_[4].dep_time_, start_time + 5h + 10min); + EXPECT_EQ(journey.legs_[4].arr_time_, start_time + 6h); + EXPECT_EQ(journey.legs_[5].dep_time_, start_time + 6h); + EXPECT_EQ(journey.legs_[5].arr_time_, start_time + 6h); +} + +TEST(tb_a_star, search_overnight) { + auto const tt = load_gtfs(overnight_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}} + 10h; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({"S2", src}), 0_minutes, 0U}}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.els_.size(), 1); + journey journey = result.els_[0]; + + EXPECT_EQ(journey.legs_.size(), 4); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, journey.dest_); + auto s1 = stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.legs_.front().to_, s1); + EXPECT_EQ(journey.legs_[1].to_, s1); + + EXPECT_EQ(journey.transfers_, 1); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 2h); + EXPECT_EQ(journey.legs_[1].dep_time_, start_time + 2h); + EXPECT_EQ(journey.legs_[1].arr_time_, start_time + 2h + 2min); + EXPECT_EQ(journey.legs_[2].dep_time_, start_time + 14h); + EXPECT_EQ(journey.legs_[2].arr_time_, start_time + 22h); + EXPECT_EQ(journey.legs_[3].dep_time_, start_time + 22h); + EXPECT_EQ(journey.legs_[3].arr_time_, start_time + 22h); +} +TEST(tb_a_star, search_intermodal) { + auto const tt = load_gtfs(longer_trips_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_match_mode_ = location_match_mode::kIntermodal, + .dest_match_mode_ = location_match_mode::kIntermodal, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({"S7", src}), 0_minutes, 0U}}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.els_.size(), 1); + journey journey = result.els_[0]; + + EXPECT_EQ(journey.legs_.size(), 7); + EXPECT_FALSE(journey.error_); + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{2}]] + .back()} + .location_idx(); + EXPECT_EQ(journey.dest_, dest); + auto start = get_special_station(special_station::kStart); + EXPECT_EQ(journey.legs_.front().from_, start); + for (std::size_t i = 0; i < journey.legs_.size() - 1; ++i) { + EXPECT_EQ(journey.legs_[i].to_, journey.legs_[i + 1].from_); + } + EXPECT_EQ(journey.legs_.back().to_, + get_special_station(special_station::kEnd)); + auto s0 = stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + auto s3 = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]][3]} + .location_idx(); + auto s6 = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]][2]} + .location_idx(); + EXPECT_EQ(journey.legs_.front().to_, s0); + EXPECT_EQ(journey.legs_[1].to_, s3); + EXPECT_EQ(journey.legs_[2].to_, s3); + EXPECT_EQ(journey.legs_[3].to_, s6); + EXPECT_EQ(journey.legs_[4].to_, s6); + EXPECT_EQ(journey.legs_[5].to_, dest); + + EXPECT_EQ(journey.transfers_, 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time); + EXPECT_EQ(journey.legs_[1].dep_time_, start_time); + EXPECT_EQ(journey.legs_[1].arr_time_, start_time + 3h); + EXPECT_EQ(journey.legs_[2].dep_time_, start_time + 3h); + EXPECT_EQ(journey.legs_[2].arr_time_, start_time + 3h + 2min); + EXPECT_EQ(journey.legs_[3].dep_time_, start_time + 3h + 10min); + EXPECT_EQ(journey.legs_[3].arr_time_, start_time + 5h); + EXPECT_EQ(journey.legs_[4].dep_time_, start_time + 5h); + EXPECT_EQ(journey.legs_[4].arr_time_, start_time + 5h + 2min); + EXPECT_EQ(journey.legs_[5].dep_time_, start_time + 5h + 10min); + EXPECT_EQ(journey.legs_[5].arr_time_, start_time + 6h); + EXPECT_EQ(journey.legs_[6].dep_time_, start_time + 6h); + EXPECT_EQ(journey.legs_[6].arr_time_, start_time + 6h); +} + +loader::mem_dir no_journey_segment_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S1",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +)"); +} +TEST(tb_a_star, search_no_solution) { + auto const tt = load_gtfs(no_journey_segment_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({"S2", src}), 0_minutes, 0U}}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_TRUE(result.els_.empty()); +} + +loader::mem_dir different_termination_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S1",2 +R1,DTA,R1,R1,"S0 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R1_MON,00:00:00,00:00:00,S0,0,0,0 +R1_MON,02:00:00,02:00:00,S2,1,0,0 +)"); +} +TEST(tb_a_star, search_different_termination) { + auto const tt = load_gtfs(different_termination_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({"S1", src}), 0_minutes, 0U}}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.els_.size(), 1); + EXPECT_EQ(result.els_.front().legs_.size(), 2); + + EXPECT_EQ(result.els_.front().legs_.front().dep_time_, start_time); + EXPECT_EQ(result.els_.front().legs_.front().arr_time_, start_time + 1h); + EXPECT_EQ(result.els_.front().legs_.back().dep_time_, start_time + 1h); + EXPECT_EQ(result.els_.front().legs_.back().arr_time_, start_time + 1h); + + EXPECT_EQ(result.els_.front().transfers_, 0); + EXPECT_FALSE(result.els_.front().error_); + EXPECT_EQ(result.els_.front().legs_.front().to_, + result.els_.front().legs_.back().from_); + + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .back()} + .location_idx(); + EXPECT_EQ(result.els_.front().dest_, dest); + EXPECT_EQ(result.els_.front().legs_.back().to_, dest); + EXPECT_EQ(result.els_.front().legs_.back().from_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().from_, start); + EXPECT_EQ(result.els_.front().legs_.front().to_, dest); +} + +loader::mem_dir max_cost_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, +S3,S3,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S3",2 +R1,DTA,R1,R1,"S1 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,01:00:00,01:00:00,S1,1,0,0 +R0_MON,02:00:00,02:00:00,S3,2,0,0 +R1_MON,01:10:00,01:10:00,S1,0,0,0 +R1_MON,24:00:00,24:00:00,S2,1,0,0 +)"); +} +TEST(tb_a_star, search_max_cost) { + auto const tt = load_gtfs(max_cost_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = { + {tt.locations_.location_id_to_idx_.at({"S2", src}), 0_minutes, 0U}}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.els_.size(), 1); + EXPECT_EQ(result.els_.front().legs_.size(), 4); + + EXPECT_EQ(result.els_.front().legs_.front().dep_time_, start_time); + EXPECT_EQ(result.els_.front().legs_.front().arr_time_, start_time + 1h); + EXPECT_EQ(result.els_.front().legs_[1].dep_time_, start_time + 1h); + EXPECT_EQ(result.els_.front().legs_[1].arr_time_, start_time + 1h + 2min); + EXPECT_EQ(result.els_.front().legs_[2].dep_time_, start_time + 1h + 10min); + EXPECT_EQ(result.els_.front().legs_[2].arr_time_, start_time + 24h); + EXPECT_EQ(result.els_.front().legs_.back().dep_time_, start_time + 24h); + EXPECT_EQ(result.els_.front().legs_.back().arr_time_, start_time + 24h); + + EXPECT_EQ(result.els_.front().transfers_, 1); + EXPECT_FALSE(result.els_.front().error_); + for (unsigned long i = 0; i < 3; ++i) { + EXPECT_EQ(result.els_.front().legs_[i].to_, + result.els_.front().legs_[i + 1].from_); + } + + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]] + .back()} + .location_idx(); + EXPECT_EQ(result.els_.front().dest_, dest); + EXPECT_EQ(result.els_.front().legs_.back().to_, dest); + EXPECT_EQ(result.els_.front().legs_.back().from_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().from_, start); + auto s1 = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]][1]} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().to_, s1); + EXPECT_EQ(result.els_.front().legs_[1].to_, s1); +} + +loader::mem_dir over_24h_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S1",2 +R1,DTA,R1,R1,"S0 -> S2 -> S1",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 +R1,MON,R1_MON,R1_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,00:00:00,00:00:00,S0,0,0,0 +R0_MON,25:00:00,25:00:00,S1,1,0,0 +R1_MON,00:00:00,00:00:00,S0,0,0,0 +R1_MON,00:10:00,00:10:00,S2,1,0,0 +R1_MON,00:20:00,00:20:00,S1,2,0,0 +)"); +} + +TEST(tb_a_star, longer_trip_then_24h) { + auto const tt = load_gtfs(over_24h_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at({"S1", src}), + 0_minutes, 0U}}, + }; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.size(), 1); + auto journey = result.els_.front(); + EXPECT_EQ(journey.legs_.size(), 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 20min); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 20min); + EXPECT_EQ(journey.legs_.back().dep_time_, start_time + 20min); + + EXPECT_EQ(journey.legs_.front().to_, journey.legs_.back().from_); + + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]] + .back()} + .location_idx(); + EXPECT_EQ(result.els_.front().dest_, dest); + EXPECT_EQ(result.els_.front().legs_.back().to_, dest); + EXPECT_EQ(result.els_.front().legs_.back().from_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().from_, start); + EXPECT_EQ(result.els_.front().legs_.front().to_, + result.els_.front().legs_.back().from_); +} + +TEST(tb_a_star, start_time_different_date_to_first_segment) { + auto const tt = load_gtfs(over_24h_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / February / day{28}} + 23h; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at({"S1", src}), + 0_minutes, 0U}}, + }; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.size(), 1); + auto journey = result.els_.front(); + EXPECT_EQ(journey.legs_.size(), 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, + sys_days{year{2021} / March / day{1}}); + EXPECT_EQ(journey.legs_.front().arr_time_, + sys_days{year{2021} / March / day{1}} + 20min); + EXPECT_EQ(journey.legs_.back().arr_time_, + sys_days{year{2021} / March / day{1}} + 20min); + EXPECT_EQ(journey.legs_.back().dep_time_, + sys_days{year{2021} / March / day{1}} + 20min); + + EXPECT_EQ(journey.legs_.front().to_, journey.legs_.back().from_); + + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{1}]] + .back()} + .location_idx(); + EXPECT_EQ(result.els_.front().dest_, dest); + EXPECT_EQ(result.els_.front().legs_.back().to_, dest); + EXPECT_EQ(result.els_.front().legs_.back().from_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().from_, start); + EXPECT_EQ(result.els_.front().legs_.front().to_, + result.els_.front().legs_.back().from_); +} + +loader::mem_dir segment_passing_midnight_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,23:30:00,23:30:00,S0,0,0,0 +R0_MON,24:30:00,24:30:00,S1,1,0,0 +R0_MON,25:00:00,25:00:00,S2,2,0,0 +)"); +} +TEST(tb_a_star, segment_passing_midnight) { + auto const tt = load_gtfs(segment_passing_midnight_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{1}} + 23h + 30min; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S0", src}), 0_minutes, + 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at({"S2", src}), + 0_minutes, 0U}}, + .max_travel_time_ = duration_t{24h}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.size(), 1); + auto journey = result.els_.front(); + EXPECT_EQ(journey.legs_.size(), 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 1h + 30min); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 1h + 30min); + EXPECT_EQ(journey.legs_.back().dep_time_, start_time + 1h + 30min); + + EXPECT_EQ(journey.legs_.front().to_, journey.legs_.back().from_); + + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .back()} + .location_idx(); + EXPECT_EQ(result.els_.front().dest_, dest); + EXPECT_EQ(result.els_.front().legs_.back().to_, dest); + EXPECT_EQ(result.els_.front().legs_.back().from_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .front()} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().from_, start); + EXPECT_EQ(result.els_.front().legs_.front().to_, + result.els_.front().legs_.back().from_); +} + +loader::mem_dir transport_starts_before_start_time_files() { + return loader::mem_dir::read(R"( +# agency.txt +agency_id,agency_name,agency_url,agency_timezone +DTA,Demo Transit Authority,,Europe/London + +# stops.txt +stop_id,stop_name,stop_desc,stop_lat,stop_lon,stop_url,location_type,parent_station +S0,S0,,,,,, +S1,S1,,,,,, +S2,S2,,,,,, + +# calendar.txt +service_id,monday,tuesday,wednesday,thursday,friday,saturday,sunday,start_date,end_date +MON,1,0,0,0,0,0,0,20210301,20210307 + +# routes.txt +route_id,agency_id,route_short_name,route_long_name,route_desc,route_type +R0,DTA,R0,R0,"S0 -> S2",2 + +# trips.txt +route_id,service_id,trip_id,trip_headsign,block_id +R0,MON,R0_MON,R0_MON,1 + +# stop_times.txt +trip_id,arrival_time,departure_time,stop_id,stop_sequence,pickup_type,drop_off_type +R0_MON,23:30:00,23:30:00,S0,0,0,0 +R0_MON,24:30:00,24:30:00,S1,1,0,0 +R0_MON,25:00:00,25:00:00,S2,2,0,0 +)"); +} +TEST(tb_a_star, tranpsport_starts_before_start_time) { + auto const tt = load_gtfs(transport_starts_before_start_time_files); + auto const tbd = preprocess(tt, profile_idx_t{0}); + static auto search_state = routing::search_state{}; + auto algo_state = a_star_state{tbd}; + unixtime_t start_time = sys_days{year{2021} / March / day{2}}; + + auto constexpr src = source_idx_t{0}; + auto q = routing::query{ + .start_time_ = start_time, + .start_ = {{tt.locations_.location_id_to_idx_.at({"S1", src}), 0_minutes, + 0U}}, + .destination_ = {{tt.locations_.location_id_to_idx_.at({"S2", src}), + 0_minutes, 0U}}, + .max_travel_time_ = duration_t{24h}}; + + routing::search searcher{ + tt, nullptr, search_state, algo_state, std::move(q)}; + auto result = *searcher.execute().journeys_; + EXPECT_EQ(result.size(), 1); + auto journey = result.els_.front(); + EXPECT_EQ(journey.legs_.size(), 2); + + EXPECT_EQ(journey.legs_.front().dep_time_, start_time + 30min); + EXPECT_EQ(journey.legs_.front().arr_time_, start_time + 1h); + EXPECT_EQ(journey.legs_.back().arr_time_, start_time + 1h); + EXPECT_EQ(journey.legs_.back().dep_time_, start_time + 1h); + + EXPECT_EQ(journey.legs_.front().to_, journey.legs_.back().from_); + + auto dest = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]] + .back()} + .location_idx(); + EXPECT_EQ(result.els_.front().dest_, dest); + EXPECT_EQ(result.els_.front().legs_.back().to_, dest); + EXPECT_EQ(result.els_.front().legs_.back().from_, dest); + auto start = + stop{tt.route_location_seq_[tt.transport_route_[transport_idx_t{0}]][1]} + .location_idx(); + EXPECT_EQ(result.els_.front().legs_.front().from_, start); + EXPECT_EQ(result.els_.front().legs_.front().to_, + result.els_.front().legs_.back().from_); +} +} // namespace nigiri::routing::tb::a_star