From d4f9c44970ddae230dd4ef702d38c26f7c5399f1 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 01:22:52 -0700 Subject: [PATCH 01/26] Read VROOM summary duration --- .../api/forecast_optimizer.hpp | 3 +++ app/api/src/forecast_optimizer.cpp | 14 ++++++++++++++ .../weather_forecast_optimizer_test.cpp | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index b75ce315d..350622415 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -4,6 +4,7 @@ #include #include +#include #include namespace deliveryoptimizer::api { @@ -50,6 +51,8 @@ FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coord EstimateRouteWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, int baseline_duration_seconds); +[[nodiscard]] std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_output); + [[nodiscard]] Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& impact); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index f354278de..43621a58f 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -296,6 +296,20 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o return impact; } +std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_output) { + const Json::Value& duration = vroom_output["summary"]["duration"]; + if (!duration.isNumeric()) { + return std::nullopt; + } + + const double raw_duration = duration.asDouble(); + if (raw_duration < 0.0 || raw_duration > static_cast(std::numeric_limits::max())) { + return std::nullopt; + } + + return static_cast(std::ceil(raw_duration)); +} + Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& impact) { Json::Value payload = BuildVroomInput(input); diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 008fb481f..bd75def5e 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -106,3 +106,21 @@ TEST(WeatherForecastOptimizerTest, AboveThresholdWeatherAddsServiceTime) { EXPECT_EQ(forecast["weather_delay_seconds"].asInt(), 400); EXPECT_TRUE(forecast["reoptimization"]["applied"].asBool()); } + +TEST(WeatherForecastOptimizerTest, ReadsVroomSummaryDuration) { + Json::Value output{Json::objectValue}; + output["summary"] = Json::Value{Json::objectValue}; + output["summary"]["duration"] = 124.2; + + const std::optional duration = + deliveryoptimizer::api::ReadVroomSummaryDurationSeconds(output); + + ASSERT_TRUE(duration.has_value()); + EXPECT_EQ(*duration, 125); +} + +TEST(WeatherForecastOptimizerTest, IgnoresMissingVroomSummaryDuration) { + const Json::Value output{Json::objectValue}; + + EXPECT_FALSE(deliveryoptimizer::api::ReadVroomSummaryDurationSeconds(output).has_value()); +} From c94cd0ab721cd8c96cafce9bfa1cabf7b0622a9c Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 01:29:30 -0700 Subject: [PATCH 02/26] read planned route start time --- .../api/forecast_optimizer.hpp | 3 ++ app/api/src/forecast_optimizer.cpp | 15 +++++++++ .../weather_forecast_optimizer_test.cpp | 31 +++++++++++++++++++ 3 files changed, 49 insertions(+) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 350622415..6225f7970 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -51,6 +51,9 @@ FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coord EstimateRouteWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, int baseline_duration_seconds); +[[nodiscard]] std::optional +ReadPlannedRouteStartTime(const OptimizeRequestInput& input); + [[nodiscard]] std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_output); [[nodiscard]] Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 43621a58f..dacbdfe2f 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -296,6 +296,21 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o return impact; } +std::optional +ReadPlannedRouteStartTime(const OptimizeRequestInput& input) { + std::optional planned_start; + for (const VehicleInput& vehicle : input.vehicles) { + if (!vehicle.time_window.has_value()) { + continue; + } + if (!planned_start.has_value() || vehicle.time_window->start < *planned_start) { + planned_start = vehicle.time_window->start; + } + } + + return planned_start; +} + std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_output) { const Json::Value& duration = vroom_output["summary"]["duration"]; if (!duration.isNumeric()) { diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index bd75def5e..dd6c00a6f 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -124,3 +124,34 @@ TEST(WeatherForecastOptimizerTest, IgnoresMissingVroomSummaryDuration) { EXPECT_FALSE(deliveryoptimizer::api::ReadVroomSummaryDurationSeconds(output).has_value()); } + +TEST(WeatherForecastOptimizerTest, ReadsEarliestVehicleStartTime) { + auto input = BuildInput(); + input.vehicles.push_back(deliveryoptimizer::api::VehicleInput{ + .external_id = "driver-2", + .capacity = 8, + .start = std::nullopt, + .end = std::nullopt, + .time_window = + deliveryoptimizer::api::TimeWindow{ + .start = std::chrono::sys_seconds{std::chrono::seconds{1800}}, + .end = std::chrono::sys_seconds{std::chrono::seconds{7200}}, + }, + }); + input.vehicles[0].time_window = deliveryoptimizer::api::TimeWindow{ + .start = std::chrono::sys_seconds{std::chrono::seconds{900}}, + .end = std::chrono::sys_seconds{std::chrono::seconds{3600}}, + }; + + const std::optional planned_start = + deliveryoptimizer::api::ReadPlannedRouteStartTime(input); + + ASSERT_TRUE(planned_start.has_value()); + EXPECT_EQ(planned_start->time_since_epoch(), std::chrono::seconds{900}); +} + +TEST(WeatherForecastOptimizerTest, MissingVehicleTimeWindowHasNoPlannedStart) { + const auto input = BuildInput(); + + EXPECT_FALSE(deliveryoptimizer::api::ReadPlannedRouteStartTime(input).has_value()); +} From 7351d5c2244ede1e4c9eea539735d1e3335773da Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 01:39:26 -0700 Subject: [PATCH 03/26] use VROOM duration for weather timing --- .../api/forecast_optimizer.hpp | 9 +++-- .../deliveries_optimize_endpoint.cpp | 4 ++- app/api/src/forecast_optimizer.cpp | 27 ++++++++++++-- app/api/src/optimization_job_runtime.cpp | 13 +++++-- .../weather_forecast_optimizer_test.cpp | 35 ++++++++++++++++--- 5 files changed, 74 insertions(+), 14 deletions(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 6225f7970..d967b8328 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -52,9 +52,14 @@ EstimateRouteWeatherImpact(const WeatherForecastOptions& options, const Optimize int baseline_duration_seconds); [[nodiscard]] std::optional -ReadPlannedRouteStartTime(const OptimizeRequestInput& input); +ReadRouteStartTime(const OptimizeRequestInput& input); -[[nodiscard]] std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_output); +[[nodiscard]] std::optional ReadVroomDuration(const Json::Value& vroom_output); + +[[nodiscard]] WeatherImpactEstimate +RecalculateWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, + const WeatherImpactEstimate& planned_impact, + const Json::Value& vroom_output); [[nodiscard]] Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& impact); diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index d9a77fd40..338cc218b 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -189,9 +189,11 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, respond_with_completion](const CoordinatedSolveResult& result) mutable { std::optional forecast; if (result.output.has_value()) { - const WeatherImpactEstimate impact = weather_impact->value_or( + const WeatherImpactEstimate planned_impact = weather_impact->value_or( EstimateWeatherImpact(weather_options, optimize_request_ptr->jobs.size(), EstimateServiceSeconds(*optimize_request_ptr))); + const WeatherImpactEstimate impact = RecalculateWeatherImpact( + weather_options, *optimize_request_ptr, planned_impact, *result.output); forecast = BuildWeatherForecastAnnotation(weather_options, impact); } respond_with_completion(BuildSolveExecutionResponse( diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index dacbdfe2f..f2e8f49cd 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -296,8 +296,7 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o return impact; } -std::optional -ReadPlannedRouteStartTime(const OptimizeRequestInput& input) { +std::optional ReadRouteStartTime(const OptimizeRequestInput& input) { std::optional planned_start; for (const VehicleInput& vehicle : input.vehicles) { if (!vehicle.time_window.has_value()) { @@ -311,7 +310,7 @@ ReadPlannedRouteStartTime(const OptimizeRequestInput& input) { return planned_start; } -std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_output) { +std::optional ReadVroomDuration(const Json::Value& vroom_output) { const Json::Value& duration = vroom_output["summary"]["duration"]; if (!duration.isNumeric()) { return std::nullopt; @@ -325,6 +324,28 @@ std::optional ReadVroomSummaryDurationSeconds(const Json::Value& vroom_outp return static_cast(std::ceil(raw_duration)); } +WeatherImpactEstimate RecalculateWeatherImpact(const WeatherForecastOptions& options, + const OptimizeRequestInput& input, + const WeatherImpactEstimate& planned_impact, + const Json::Value& vroom_output) { + const std::optional summary_duration = ReadVroomDuration(vroom_output); + if (!summary_duration.has_value()) { + return planned_impact; + } + + const int weather_delay_already_in_route = + planned_impact.should_reoptimize ? planned_impact.weather_delay_seconds : 0; + const int baseline_route_seconds = + std::max(*summary_duration - weather_delay_already_in_route, 0); + + WeatherForecastOptions effective_options = options; + effective_options.weather_delay_seconds_per_stop = planned_impact.delay_seconds_per_stop; + WeatherImpactEstimate impact = + EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_route_seconds); + impact.source = planned_impact.source; + return impact; +} + Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& impact) { Json::Value payload = BuildVroomInput(input); diff --git a/app/api/src/optimization_job_runtime.cpp b/app/api/src/optimization_job_runtime.cpp index fcd08f51d..3c9f40775 100644 --- a/app/api/src/optimization_job_runtime.cpp +++ b/app/api/src/optimization_job_runtime.cpp @@ -158,9 +158,16 @@ void OptimizationJobRuntime::WorkerLoop(const std::stop_token stop_token, const WeatherImpactEstimate impact = EstimateRouteWeatherImpact(weather_options_, parsed_request->input, baseline_seconds); const Json::Value vroom_input = BuildWeatherAdjustedVroomInput(parsed_request->input, impact); - const auto solve_result = BuildSolveExecutionResult( - parsed_request->input, ToCoordinatedSolveResult(runner_->Run(vroom_input)), - BuildWeatherForecastAnnotation(weather_options_, impact)); + const CoordinatedSolveResult coordinated_result = + ToCoordinatedSolveResult(runner_->Run(vroom_input)); + std::optional forecast; + if (coordinated_result.output.has_value()) { + forecast = BuildWeatherForecastAnnotation( + weather_options_, RecalculateWeatherImpact(weather_options_, parsed_request->input, + impact, *coordinated_result.output)); + } + const auto solve_result = + BuildSolveExecutionResult(parsed_request->input, coordinated_result, forecast); if (solve_result.response_body.has_value()) { if (store_->CompleteJobSuccess(claimed_job->record.job_id, claimed_job->worker_id, *solve_result.response_body, solve_result.outcome, diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index dd6c00a6f..a96d8ecf3 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -112,8 +112,7 @@ TEST(WeatherForecastOptimizerTest, ReadsVroomSummaryDuration) { output["summary"] = Json::Value{Json::objectValue}; output["summary"]["duration"] = 124.2; - const std::optional duration = - deliveryoptimizer::api::ReadVroomSummaryDurationSeconds(output); + const std::optional duration = deliveryoptimizer::api::ReadVroomDuration(output); ASSERT_TRUE(duration.has_value()); EXPECT_EQ(*duration, 125); @@ -122,7 +121,7 @@ TEST(WeatherForecastOptimizerTest, ReadsVroomSummaryDuration) { TEST(WeatherForecastOptimizerTest, IgnoresMissingVroomSummaryDuration) { const Json::Value output{Json::objectValue}; - EXPECT_FALSE(deliveryoptimizer::api::ReadVroomSummaryDurationSeconds(output).has_value()); + EXPECT_FALSE(deliveryoptimizer::api::ReadVroomDuration(output).has_value()); } TEST(WeatherForecastOptimizerTest, ReadsEarliestVehicleStartTime) { @@ -144,7 +143,7 @@ TEST(WeatherForecastOptimizerTest, ReadsEarliestVehicleStartTime) { }; const std::optional planned_start = - deliveryoptimizer::api::ReadPlannedRouteStartTime(input); + deliveryoptimizer::api::ReadRouteStartTime(input); ASSERT_TRUE(planned_start.has_value()); EXPECT_EQ(planned_start->time_since_epoch(), std::chrono::seconds{900}); @@ -153,5 +152,31 @@ TEST(WeatherForecastOptimizerTest, ReadsEarliestVehicleStartTime) { TEST(WeatherForecastOptimizerTest, MissingVehicleTimeWindowHasNoPlannedStart) { const auto input = BuildInput(); - EXPECT_FALSE(deliveryoptimizer::api::ReadPlannedRouteStartTime(input).has_value()); + EXPECT_FALSE(deliveryoptimizer::api::ReadRouteStartTime(input).has_value()); +} + +TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { + const auto input = BuildInput(); + const deliveryoptimizer::api::WeatherForecastOptions options{ + .enabled = true, + .weather_delay_seconds_per_stop = 200, + .reoptimize_threshold_seconds = 100, + .reoptimize_threshold_percent = 0.0, + .openweather_api_key = "", + .openweather_base_url = "", + }; + const deliveryoptimizer::api::WeatherImpactEstimate planned_impact = + deliveryoptimizer::api::EstimateWeatherImpact(options, input.jobs.size(), 300); + Json::Value output{Json::objectValue}; + output["summary"] = Json::Value{Json::objectValue}; + output["summary"]["duration"] = 960; + + const deliveryoptimizer::api::WeatherImpactEstimate impact = + deliveryoptimizer::api::RecalculateWeatherImpact(options, input, planned_impact, output); + const Json::Value forecast = + deliveryoptimizer::api::BuildWeatherForecastAnnotation(options, impact); + + EXPECT_EQ(forecast["baseline_duration_seconds"].asInt(), 560); + EXPECT_EQ(forecast["weather_delay_seconds"].asInt(), 400); + EXPECT_EQ(forecast["predicted_duration_seconds"].asInt(), 960); } From 25a35ab48b9b1170513b9deb5d932df338bca0d0 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 01:54:45 -0700 Subject: [PATCH 04/26] added weather timing fields --- .../api/forecast_optimizer.hpp | 5 ++++ app/api/src/forecast_optimizer.cpp | 29 +++++++++++++++++++ .../weather_forecast_optimizer_test.cpp | 10 ++++++- 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index d967b8328..c83a3bdcf 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -2,6 +2,7 @@ #include "deliveryoptimizer/api/optimize_request.hpp" +#include #include #include #include @@ -27,11 +28,15 @@ struct OpenWeatherDelayEstimate { struct WeatherImpactEstimate { int stop_count{0}; int baseline_duration_seconds{0}; + int baseline_route_duration_seconds{0}; int delay_seconds_per_stop{0}; int weather_delay_seconds{0}; + int weather_adjusted_duration_seconds{0}; int reoptimize_threshold_seconds{300}; bool should_reoptimize{false}; std::string source; + std::optional planned_start_time; + std::optional estimated_finish_time; }; [[nodiscard]] WeatherForecastOptions ResolveWeatherForecastOptionsFromEnv(); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index f2e8f49cd..a05cf1eba 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -155,6 +155,18 @@ constexpr double kDefaultWeatherThresholdPercent = 5.0; return delay_seconds; } +void SetRouteTimes(const deliveryoptimizer::api::OptimizeRequestInput& input, + deliveryoptimizer::api::WeatherImpactEstimate& impact) { + impact.planned_start_time = deliveryoptimizer::api::ReadRouteStartTime(input); + if (!impact.planned_start_time.has_value()) { + impact.estimated_finish_time = std::nullopt; + return; + } + + impact.estimated_finish_time = + *impact.planned_start_time + std::chrono::seconds{impact.weather_adjusted_duration_seconds}; +} + } // namespace namespace deliveryoptimizer::api { @@ -271,11 +283,15 @@ WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& option return WeatherImpactEstimate{ .stop_count = normalized_stop_count, .baseline_duration_seconds = normalized_baseline_seconds, + .baseline_route_duration_seconds = normalized_baseline_seconds, .delay_seconds_per_stop = configured_delay_per_stop, .weather_delay_seconds = weather_delay_seconds, + .weather_adjusted_duration_seconds = normalized_baseline_seconds + weather_delay_seconds, .reoptimize_threshold_seconds = threshold_seconds, .should_reoptimize = weather_delay_seconds > 0 && weather_delay_seconds >= threshold_seconds, .source = options.enabled ? "fixed_delay" : "disabled", + .planned_start_time = std::nullopt, + .estimated_finish_time = std::nullopt, }; } @@ -285,12 +301,14 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o WeatherForecastOptions effective_options = options; WeatherImpactEstimate impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); + SetRouteTimes(input, impact); const OpenWeatherDelayEstimate openweather = FetchOpenWeatherDelayEstimate( options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}); if (openweather.available) { effective_options.weather_delay_seconds_per_stop = openweather.delay_seconds_per_stop; impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); impact.source = openweather.source; + SetRouteTimes(input, impact); } return impact; @@ -343,6 +361,7 @@ WeatherImpactEstimate RecalculateWeatherImpact(const WeatherForecastOptions& opt WeatherImpactEstimate impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_route_seconds); impact.source = planned_impact.source; + SetRouteTimes(input, impact); return impact; } @@ -370,10 +389,20 @@ Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options forecast["provider"] = impact.source; forecast["stop_count"] = impact.stop_count; forecast["baseline_duration_seconds"] = impact.baseline_duration_seconds; + forecast["baseline_route_duration_seconds"] = impact.baseline_route_duration_seconds; forecast["weather_delay_seconds"] = impact.weather_delay_seconds; + forecast["weather_adjusted_duration_seconds"] = impact.weather_adjusted_duration_seconds; forecast["predicted_duration_seconds"] = impact.baseline_duration_seconds + impact.weather_delay_seconds; forecast["reoptimize_threshold_seconds"] = impact.reoptimize_threshold_seconds; + if (impact.planned_start_time.has_value()) { + forecast["planned_start_time"] = + static_cast(impact.planned_start_time->time_since_epoch().count()); + } + if (impact.estimated_finish_time.has_value()) { + forecast["estimated_finish_time"] = + static_cast(impact.estimated_finish_time->time_since_epoch().count()); + } Json::Value reoptimization{Json::objectValue}; reoptimization["applied"] = impact.should_reoptimize; diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index a96d8ecf3..792b9de9d 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -156,7 +156,11 @@ TEST(WeatherForecastOptimizerTest, MissingVehicleTimeWindowHasNoPlannedStart) { } TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { - const auto input = BuildInput(); + auto input = BuildInput(); + input.vehicles[0].time_window = deliveryoptimizer::api::TimeWindow{ + .start = std::chrono::sys_seconds{std::chrono::seconds{600}}, + .end = std::chrono::sys_seconds{std::chrono::seconds{3600}}, + }; const deliveryoptimizer::api::WeatherForecastOptions options{ .enabled = true, .weather_delay_seconds_per_stop = 200, @@ -177,6 +181,10 @@ TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { deliveryoptimizer::api::BuildWeatherForecastAnnotation(options, impact); EXPECT_EQ(forecast["baseline_duration_seconds"].asInt(), 560); + EXPECT_EQ(forecast["baseline_route_duration_seconds"].asInt(), 560); EXPECT_EQ(forecast["weather_delay_seconds"].asInt(), 400); + EXPECT_EQ(forecast["weather_adjusted_duration_seconds"].asInt(), 960); EXPECT_EQ(forecast["predicted_duration_seconds"].asInt(), 960); + EXPECT_EQ(forecast["planned_start_time"].asInt64(), 600); + EXPECT_EQ(forecast["estimated_finish_time"].asInt64(), 1560); } From 610f24a4db0c1cc406cedb2ae8d9f11835c4ef6a Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 02:08:46 -0700 Subject: [PATCH 05/26] matched OpenWeather hours to route start --- .../api/forecast_optimizer.hpp | 9 ++- app/api/src/forecast_optimizer.cpp | 62 ++++++++++++++----- .../weather_forecast_optimizer_test.cpp | 37 +++++++++++ 3 files changed, 91 insertions(+), 17 deletions(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index c83a3bdcf..79ee43435 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -45,8 +45,13 @@ struct WeatherImpactEstimate { [[nodiscard]] int EstimateServiceSeconds(const OptimizeRequestInput& input); -[[nodiscard]] OpenWeatherDelayEstimate -FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coordinate& coordinate); +[[nodiscard]] OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate( + const WeatherForecastOptions& options, const Coordinate& coordinate, + std::optional route_start_time = std::nullopt); + +[[nodiscard]] int +ReadOpenWeatherDelay(const Json::Value& body, + std::optional route_start_time = std::nullopt); [[nodiscard]] WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, std::size_t stop_count, diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index a05cf1eba..3da6b90fa 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -140,19 +140,19 @@ constexpr double kDefaultWeatherThresholdPercent = 5.0; return delay_seconds; } -[[nodiscard]] int DelayFromOpenWeatherBody(const Json::Value& body) { - const Json::Value& hourly = body["hourly"]; - if (!hourly.isArray()) { - return 0; +[[nodiscard]] bool IsRouteHour(const Json::Value& hour, + const std::optional route_start_time) { + if (!route_start_time.has_value()) { + return true; } - - int delay_seconds = 0; - const Json::ArrayIndex hours_to_scan = std::min(hourly.size(), 6U); - for (Json::ArrayIndex index = 0; index < hours_to_scan; ++index) { - delay_seconds = std::max(delay_seconds, DelayFromHourlyForecast(hourly[index])); + if (!hour["dt"].isInt64() && !hour["dt"].isUInt64()) { + return false; } - return delay_seconds; + const auto forecast_time = + std::chrono::sys_seconds{std::chrono::seconds{hour["dt"].asLargestInt()}}; + return forecast_time >= *route_start_time && + forecast_time < *route_start_time + std::chrono::hours{6}; } void SetRouteTimes(const deliveryoptimizer::api::OptimizeRequestInput& input, @@ -204,8 +204,9 @@ int EstimateServiceSeconds(const OptimizeRequestInput& input) { return static_cast(total); } -OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, - const Coordinate& coordinate) { +OpenWeatherDelayEstimate +FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coordinate& coordinate, + const std::optional route_start_time) { if (!IsOpenWeatherConfigured(options)) { return OpenWeatherDelayEstimate{ .available = false, @@ -223,7 +224,8 @@ OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(const WeatherForecastOpti auto future = promise->get_future(); client->sendRequest( request, - [promise](const drogon::ReqResult result, const drogon::HttpResponsePtr& response) { + [promise, route_start_time](const drogon::ReqResult result, + const drogon::HttpResponsePtr& response) { if (result != drogon::ReqResult::Ok || response == nullptr || response->getStatusCode() != drogon::k200OK) { promise->set_value(OpenWeatherDelayEstimate{ @@ -246,7 +248,7 @@ OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(const WeatherForecastOpti promise->set_value(OpenWeatherDelayEstimate{ .available = true, - .delay_seconds_per_stop = DelayFromOpenWeatherBody(*body), + .delay_seconds_per_stop = ReadOpenWeatherDelay(*body, route_start_time), .source = "openweather", }); }, @@ -264,6 +266,35 @@ OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(const WeatherForecastOpti return future.get(); } +int ReadOpenWeatherDelay(const Json::Value& body, + const std::optional route_start_time) { + const Json::Value& hourly = body["hourly"]; + if (!hourly.isArray()) { + return 0; + } + + int delay_seconds = 0; + Json::ArrayIndex matched_hours = 0U; + for (Json::ArrayIndex index = 0U; index < hourly.size(); ++index) { + if (!IsRouteHour(hourly[index], route_start_time)) { + continue; + } + delay_seconds = std::max(delay_seconds, DelayFromHourlyForecast(hourly[index])); + ++matched_hours; + } + + if (!route_start_time.has_value() || matched_hours > 0U) { + return delay_seconds; + } + + const Json::ArrayIndex hours_to_scan = std::min(hourly.size(), 6U); + for (Json::ArrayIndex index = 0U; index < hours_to_scan; ++index) { + delay_seconds = std::max(delay_seconds, DelayFromHourlyForecast(hourly[index])); + } + + return delay_seconds; +} + WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, const std::size_t stop_count, const int baseline_duration_seconds) { @@ -303,7 +334,8 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); SetRouteTimes(input, impact); const OpenWeatherDelayEstimate openweather = FetchOpenWeatherDelayEstimate( - options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}); + options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, + ReadRouteStartTime(input)); if (openweather.available) { effective_options.weather_delay_seconds_per_stop = openweather.delay_seconds_per_stop; impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 792b9de9d..3112db54d 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -42,6 +42,19 @@ namespace { }; } +[[nodiscard]] Json::Value BuildWeatherHour(const int dt, const int condition_id) { + Json::Value hour{Json::objectValue}; + hour["dt"] = dt; + hour["wind_speed"] = 0.0; + hour["visibility"] = 10000; + + Json::Value condition{Json::objectValue}; + condition["id"] = condition_id; + hour["weather"] = Json::Value{Json::arrayValue}; + hour["weather"].append(condition); + return hour; +} + } // namespace TEST(WeatherForecastOptimizerTest, DisabledWeatherHasNoImpact) { @@ -155,6 +168,30 @@ TEST(WeatherForecastOptimizerTest, MissingVehicleTimeWindowHasNoPlannedStart) { EXPECT_FALSE(deliveryoptimizer::api::ReadRouteStartTime(input).has_value()); } +TEST(WeatherForecastOptimizerTest, ReadsOpenWeatherHourNearRouteStart) { + Json::Value body{Json::objectValue}; + body["hourly"] = Json::Value{Json::arrayValue}; + body["hourly"].append(BuildWeatherHour(0, 201)); + body["hourly"].append(BuildWeatherHour(7200, 800)); + + const int delay = deliveryoptimizer::api::ReadOpenWeatherDelay( + body, std::chrono::sys_seconds{std::chrono::seconds{7200}}); + + EXPECT_EQ(delay, 0); +} + +TEST(WeatherForecastOptimizerTest, ReadsBadOpenWeatherHourNearRouteStart) { + Json::Value body{Json::objectValue}; + body["hourly"] = Json::Value{Json::arrayValue}; + body["hourly"].append(BuildWeatherHour(0, 800)); + body["hourly"].append(BuildWeatherHour(7200, 201)); + + const int delay = deliveryoptimizer::api::ReadOpenWeatherDelay( + body, std::chrono::sys_seconds{std::chrono::seconds{7200}}); + + EXPECT_EQ(delay, 240); +} + TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { auto input = BuildInput(); input.vehicles[0].time_window = deliveryoptimizer::api::TimeWindow{ From 61e54f5b6f8e8931dbdca1b3bef78cd765bb6f47 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 02:22:47 -0700 Subject: [PATCH 06/26] change logic to check weather after initial VROOM route solve --- .../api/forecast_optimizer.hpp | 13 +++-- .../deliveries_optimize_endpoint.cpp | 33 +++++------ app/api/src/forecast_optimizer.cpp | 56 +++++++++---------- app/api/src/optimization_job_runtime.cpp | 19 ++++--- .../weather_forecast_optimizer_test.cpp | 19 +++---- 5 files changed, 67 insertions(+), 73 deletions(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 79ee43435..7ef899a8c 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -47,11 +47,13 @@ struct WeatherImpactEstimate { [[nodiscard]] OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate( const WeatherForecastOptions& options, const Coordinate& coordinate, - std::optional route_start_time = std::nullopt); + std::optional route_start_time = std::nullopt, + std::optional route_duration_seconds = std::nullopt); [[nodiscard]] int ReadOpenWeatherDelay(const Json::Value& body, - std::optional route_start_time = std::nullopt); + std::optional route_start_time = std::nullopt, + std::optional route_duration_seconds = std::nullopt); [[nodiscard]] WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, std::size_t stop_count, @@ -66,10 +68,9 @@ ReadRouteStartTime(const OptimizeRequestInput& input); [[nodiscard]] std::optional ReadVroomDuration(const Json::Value& vroom_output); -[[nodiscard]] WeatherImpactEstimate -RecalculateWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, - const WeatherImpactEstimate& planned_impact, - const Json::Value& vroom_output); +[[nodiscard]] WeatherImpactEstimate RecalculateWeatherImpact(const WeatherForecastOptions& options, + const OptimizeRequestInput& input, + const Json::Value& vroom_output); [[nodiscard]] Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& impact); diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 338cc218b..1e9bb2047 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -102,13 +102,13 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, const SolveAdmissionConfig& admission_config, std::shared_ptr observability) { const WeatherForecastOptions weather_options = ResolveWeatherForecastOptionsFromEnv(); - auto coordinator = std::make_shared( - admission_config, std::make_shared(ResolveVroomRuntimeConfigFromEnv()), - SolveCoordinatorOptions{}, observability); + auto runner = std::make_shared(ResolveVroomRuntimeConfigFromEnv()); + auto coordinator = std::make_shared(admission_config, runner, + SolveCoordinatorOptions{}, observability); app.registerHandler( "/api/v1/deliveries/optimize", - [coordinator = std::move(coordinator), weather_options, + [coordinator = std::move(coordinator), runner = std::move(runner), weather_options, observability = std::move(observability)]( const drogon::HttpRequestPtr& request, std::function&& callback) { @@ -174,30 +174,23 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, .jobs = optimize_request_ptr->jobs.size(), .vehicles = optimize_request_ptr->vehicles.size(), }; - auto weather_impact = std::make_shared>(std::nullopt); - const SolveAdmissionStatus admission_status = coordinator->Submit( - request_size, - [optimize_request_ptr, weather_options, weather_impact] { - const int baseline_seconds = EstimateServiceSeconds(*optimize_request_ptr); - const WeatherImpactEstimate impact = EstimateWeatherImpact( - weather_options, optimize_request_ptr->jobs.size(), baseline_seconds); - *weather_impact = impact; - return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact); - }, - [optimize_request_ptr, weather_options, weather_impact, + request_size, [optimize_request_ptr] { return BuildVroomInput(*optimize_request_ptr); }, + [optimize_request_ptr, runner, weather_options, respond_with_completion](const CoordinatedSolveResult& result) mutable { + CoordinatedSolveResult final_result = result; std::optional forecast; if (result.output.has_value()) { - const WeatherImpactEstimate planned_impact = weather_impact->value_or( - EstimateWeatherImpact(weather_options, optimize_request_ptr->jobs.size(), - EstimateServiceSeconds(*optimize_request_ptr))); const WeatherImpactEstimate impact = RecalculateWeatherImpact( - weather_options, *optimize_request_ptr, planned_impact, *result.output); + weather_options, *optimize_request_ptr, *result.output); forecast = BuildWeatherForecastAnnotation(weather_options, impact); + if (impact.should_reoptimize) { + final_result = ToCoordinatedSolveResult( + runner->Run(BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact))); + } } respond_with_completion(BuildSolveExecutionResponse( - BuildSolveExecutionResult(*optimize_request_ptr, result, forecast))); + BuildSolveExecutionResult(*optimize_request_ptr, final_result, forecast))); }, lifecycle); if (admission_status != SolveAdmissionStatus::kAccepted) { diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 3da6b90fa..f5d2816c1 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -141,18 +141,17 @@ constexpr double kDefaultWeatherThresholdPercent = 5.0; } [[nodiscard]] bool IsRouteHour(const Json::Value& hour, - const std::optional route_start_time) { - if (!route_start_time.has_value()) { - return true; - } + const std::chrono::sys_seconds route_start_time, + const std::optional route_duration_seconds) { if (!hour["dt"].isInt64() && !hour["dt"].isUInt64()) { return false; } const auto forecast_time = std::chrono::sys_seconds{std::chrono::seconds{hour["dt"].asLargestInt()}}; - return forecast_time >= *route_start_time && - forecast_time < *route_start_time + std::chrono::hours{6}; + const int window_seconds = std::max(route_duration_seconds.value_or(6 * 60 * 60), 60 * 60); + return forecast_time >= route_start_time && + forecast_time < route_start_time + std::chrono::seconds{window_seconds}; } void SetRouteTimes(const deliveryoptimizer::api::OptimizeRequestInput& input, @@ -206,7 +205,8 @@ int EstimateServiceSeconds(const OptimizeRequestInput& input) { OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coordinate& coordinate, - const std::optional route_start_time) { + const std::optional route_start_time, + const std::optional route_duration_seconds) { if (!IsOpenWeatherConfigured(options)) { return OpenWeatherDelayEstimate{ .available = false, @@ -224,8 +224,8 @@ FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coord auto future = promise->get_future(); client->sendRequest( request, - [promise, route_start_time](const drogon::ReqResult result, - const drogon::HttpResponsePtr& response) { + [promise, route_start_time, route_duration_seconds](const drogon::ReqResult result, + const drogon::HttpResponsePtr& response) { if (result != drogon::ReqResult::Ok || response == nullptr || response->getStatusCode() != drogon::k200OK) { promise->set_value(OpenWeatherDelayEstimate{ @@ -248,7 +248,8 @@ FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coord promise->set_value(OpenWeatherDelayEstimate{ .available = true, - .delay_seconds_per_stop = ReadOpenWeatherDelay(*body, route_start_time), + .delay_seconds_per_stop = + ReadOpenWeatherDelay(*body, route_start_time, route_duration_seconds), .source = "openweather", }); }, @@ -267,23 +268,34 @@ FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coord } int ReadOpenWeatherDelay(const Json::Value& body, - const std::optional route_start_time) { + const std::optional route_start_time, + const std::optional route_duration_seconds) { const Json::Value& hourly = body["hourly"]; if (!hourly.isArray()) { return 0; } + if (!route_start_time.has_value()) { + int fallback_delay_seconds = 0; + const Json::ArrayIndex hours_to_scan = std::min(hourly.size(), 6U); + for (Json::ArrayIndex index = 0U; index < hours_to_scan; ++index) { + fallback_delay_seconds = + std::max(fallback_delay_seconds, DelayFromHourlyForecast(hourly[index])); + } + return fallback_delay_seconds; + } + int delay_seconds = 0; Json::ArrayIndex matched_hours = 0U; for (Json::ArrayIndex index = 0U; index < hourly.size(); ++index) { - if (!IsRouteHour(hourly[index], route_start_time)) { + if (!IsRouteHour(hourly[index], *route_start_time, route_duration_seconds)) { continue; } delay_seconds = std::max(delay_seconds, DelayFromHourlyForecast(hourly[index])); ++matched_hours; } - if (!route_start_time.has_value() || matched_hours > 0U) { + if (matched_hours > 0U) { return delay_seconds; } @@ -335,7 +347,7 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o SetRouteTimes(input, impact); const OpenWeatherDelayEstimate openweather = FetchOpenWeatherDelayEstimate( options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, - ReadRouteStartTime(input)); + ReadRouteStartTime(input), baseline_duration_seconds); if (openweather.available) { effective_options.weather_delay_seconds_per_stop = openweather.delay_seconds_per_stop; impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); @@ -376,25 +388,13 @@ std::optional ReadVroomDuration(const Json::Value& vroom_output) { WeatherImpactEstimate RecalculateWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, - const WeatherImpactEstimate& planned_impact, const Json::Value& vroom_output) { const std::optional summary_duration = ReadVroomDuration(vroom_output); if (!summary_duration.has_value()) { - return planned_impact; + return EstimateRouteWeatherImpact(options, input, 0); } - const int weather_delay_already_in_route = - planned_impact.should_reoptimize ? planned_impact.weather_delay_seconds : 0; - const int baseline_route_seconds = - std::max(*summary_duration - weather_delay_already_in_route, 0); - - WeatherForecastOptions effective_options = options; - effective_options.weather_delay_seconds_per_stop = planned_impact.delay_seconds_per_stop; - WeatherImpactEstimate impact = - EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_route_seconds); - impact.source = planned_impact.source; - SetRouteTimes(input, impact); - return impact; + return EstimateRouteWeatherImpact(options, input, *summary_duration); } Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, diff --git a/app/api/src/optimization_job_runtime.cpp b/app/api/src/optimization_job_runtime.cpp index 3c9f40775..d057470cf 100644 --- a/app/api/src/optimization_job_runtime.cpp +++ b/app/api/src/optimization_job_runtime.cpp @@ -154,20 +154,21 @@ void OptimizationJobRuntime::WorkerLoop(const std::stop_token stop_token, } } } else { - const int baseline_seconds = EstimateServiceSeconds(parsed_request->input); - const WeatherImpactEstimate impact = - EstimateRouteWeatherImpact(weather_options_, parsed_request->input, baseline_seconds); - const Json::Value vroom_input = BuildWeatherAdjustedVroomInput(parsed_request->input, impact); const CoordinatedSolveResult coordinated_result = - ToCoordinatedSolveResult(runner_->Run(vroom_input)); + ToCoordinatedSolveResult(runner_->Run(BuildVroomInput(parsed_request->input))); + CoordinatedSolveResult final_result = coordinated_result; std::optional forecast; if (coordinated_result.output.has_value()) { - forecast = BuildWeatherForecastAnnotation( - weather_options_, RecalculateWeatherImpact(weather_options_, parsed_request->input, - impact, *coordinated_result.output)); + const WeatherImpactEstimate impact = RecalculateWeatherImpact( + weather_options_, parsed_request->input, *coordinated_result.output); + forecast = BuildWeatherForecastAnnotation(weather_options_, impact); + if (impact.should_reoptimize) { + final_result = ToCoordinatedSolveResult( + runner_->Run(BuildWeatherAdjustedVroomInput(parsed_request->input, impact))); + } } const auto solve_result = - BuildSolveExecutionResult(parsed_request->input, coordinated_result, forecast); + BuildSolveExecutionResult(parsed_request->input, final_result, forecast); if (solve_result.response_body.has_value()) { if (store_->CompleteJobSuccess(claimed_job->record.job_id, claimed_job->worker_id, *solve_result.response_body, solve_result.outcome, diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 3112db54d..2fb3def3f 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -173,9 +173,10 @@ TEST(WeatherForecastOptimizerTest, ReadsOpenWeatherHourNearRouteStart) { body["hourly"] = Json::Value{Json::arrayValue}; body["hourly"].append(BuildWeatherHour(0, 201)); body["hourly"].append(BuildWeatherHour(7200, 800)); + body["hourly"].append(BuildWeatherHour(10800, 201)); const int delay = deliveryoptimizer::api::ReadOpenWeatherDelay( - body, std::chrono::sys_seconds{std::chrono::seconds{7200}}); + body, std::chrono::sys_seconds{std::chrono::seconds{7200}}, 1800); EXPECT_EQ(delay, 0); } @@ -187,7 +188,7 @@ TEST(WeatherForecastOptimizerTest, ReadsBadOpenWeatherHourNearRouteStart) { body["hourly"].append(BuildWeatherHour(7200, 201)); const int delay = deliveryoptimizer::api::ReadOpenWeatherDelay( - body, std::chrono::sys_seconds{std::chrono::seconds{7200}}); + body, std::chrono::sys_seconds{std::chrono::seconds{7200}}, 1800); EXPECT_EQ(delay, 240); } @@ -206,22 +207,20 @@ TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { .openweather_api_key = "", .openweather_base_url = "", }; - const deliveryoptimizer::api::WeatherImpactEstimate planned_impact = - deliveryoptimizer::api::EstimateWeatherImpact(options, input.jobs.size(), 300); Json::Value output{Json::objectValue}; output["summary"] = Json::Value{Json::objectValue}; output["summary"]["duration"] = 960; const deliveryoptimizer::api::WeatherImpactEstimate impact = - deliveryoptimizer::api::RecalculateWeatherImpact(options, input, planned_impact, output); + deliveryoptimizer::api::RecalculateWeatherImpact(options, input, output); const Json::Value forecast = deliveryoptimizer::api::BuildWeatherForecastAnnotation(options, impact); - EXPECT_EQ(forecast["baseline_duration_seconds"].asInt(), 560); - EXPECT_EQ(forecast["baseline_route_duration_seconds"].asInt(), 560); + EXPECT_EQ(forecast["baseline_duration_seconds"].asInt(), 960); + EXPECT_EQ(forecast["baseline_route_duration_seconds"].asInt(), 960); EXPECT_EQ(forecast["weather_delay_seconds"].asInt(), 400); - EXPECT_EQ(forecast["weather_adjusted_duration_seconds"].asInt(), 960); - EXPECT_EQ(forecast["predicted_duration_seconds"].asInt(), 960); + EXPECT_EQ(forecast["weather_adjusted_duration_seconds"].asInt(), 1360); + EXPECT_EQ(forecast["predicted_duration_seconds"].asInt(), 1360); EXPECT_EQ(forecast["planned_start_time"].asInt64(), 600); - EXPECT_EQ(forecast["estimated_finish_time"].asInt64(), 1560); + EXPECT_EQ(forecast["estimated_finish_time"].asInt64(), 1960); } From 458e91c77ee50d78d78a3771f49737f71fd20575 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 12:32:53 -0700 Subject: [PATCH 07/26] fix: avoid blocking weather reroutes --- .../deliveries_optimize_endpoint.cpp | 45 +++++++++++++------ app/api/src/forecast_optimizer.cpp | 43 +++++++++--------- .../weather_forecast_optimizer_test.cpp | 21 ++++++++- 3 files changed, 71 insertions(+), 38 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 1e9bb2047..3c0c9150d 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -108,7 +108,7 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, app.registerHandler( "/api/v1/deliveries/optimize", - [coordinator = std::move(coordinator), runner = std::move(runner), weather_options, + [coordinator = std::move(coordinator), weather_options, observability = std::move(observability)]( const drogon::HttpRequestPtr& request, std::function&& callback) { @@ -176,21 +176,40 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, }; const SolveAdmissionStatus admission_status = coordinator->Submit( request_size, [optimize_request_ptr] { return BuildVroomInput(*optimize_request_ptr); }, - [optimize_request_ptr, runner, weather_options, + [coordinator, optimize_request_ptr, request_size, weather_options, respond_with_completion](const CoordinatedSolveResult& result) mutable { - CoordinatedSolveResult final_result = result; std::optional forecast; - if (result.output.has_value()) { - const WeatherImpactEstimate impact = RecalculateWeatherImpact( - weather_options, *optimize_request_ptr, *result.output); - forecast = BuildWeatherForecastAnnotation(weather_options, impact); - if (impact.should_reoptimize) { - final_result = ToCoordinatedSolveResult( - runner->Run(BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact))); - } + if (!result.output.has_value()) { + respond_with_completion(BuildSolveExecutionResponse( + BuildSolveExecutionResult(*optimize_request_ptr, result, forecast))); + return; + } + + WeatherForecastOptions sync_weather_options = weather_options; + sync_weather_options.openweather_api_key.clear(); + const WeatherImpactEstimate impact = + RecalculateWeatherImpact(sync_weather_options, *optimize_request_ptr, + *result.output); + forecast = BuildWeatherForecastAnnotation(sync_weather_options, impact); + if (!impact.should_reoptimize) { + respond_with_completion(BuildSolveExecutionResponse( + BuildSolveExecutionResult(*optimize_request_ptr, result, forecast))); + return; + } + + const SolveAdmissionStatus rerun_status = coordinator->Submit( + request_size, + [optimize_request_ptr, impact] { + return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact); + }, + [optimize_request_ptr, forecast, respond_with_completion]( + const CoordinatedSolveResult& rerun_result) mutable { + respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( + *optimize_request_ptr, rerun_result, forecast))); + }); + if (rerun_status != SolveAdmissionStatus::kAccepted) { + respond_with_completion(BuildAdmissionRejectionResponse(rerun_status)); } - respond_with_completion(BuildSolveExecutionResponse( - BuildSolveExecutionResult(*optimize_request_ptr, final_result, forecast))); }, lifecycle); if (admission_status != SolveAdmissionStatus::kAccepted) { diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index f5d2816c1..972b11ac9 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -110,33 +110,32 @@ constexpr double kDefaultWeatherThresholdPercent = 5.0; int delay_seconds = 0; const double wind_speed = hour["wind_speed"].isNumeric() ? hour["wind_speed"].asDouble() : 0.0; const int visibility = hour["visibility"].isInt() ? hour["visibility"].asInt() : 10000; + bool has_thunder = false; + const Json::Value& weather = hour["weather"]; + if (weather.isArray()) { + for (const Json::Value& condition : weather) { + const int condition_id = condition["id"].isInt() ? condition["id"].asInt() : 0; + if (condition_id >= 200 && condition_id < 300) { + has_thunder = true; + } + } + } + if (wind_speed >= 10.0) { delay_seconds += 60; } if (visibility < 5000) { delay_seconds += 60; } - if (hour["rain"].isObject()) { + if (has_thunder) { + delay_seconds += 240; + } else if (hour["rain"].isObject()) { delay_seconds += 90; } if (hour["snow"].isObject()) { delay_seconds += 180; } - const Json::Value& weather = hour["weather"]; - if (weather.isArray()) { - bool has_thunder = false; - for (const Json::Value& condition : weather) { - const int condition_id = condition["id"].isInt() ? condition["id"].asInt() : 0; - if (condition_id >= 200 && condition_id < 300) { - has_thunder = true; - } - } - if (has_thunder) { - delay_seconds += 240; - } - } - return delay_seconds; } @@ -154,9 +153,9 @@ constexpr double kDefaultWeatherThresholdPercent = 5.0; forecast_time < route_start_time + std::chrono::seconds{window_seconds}; } -void SetRouteTimes(const deliveryoptimizer::api::OptimizeRequestInput& input, +void SetRouteTimes(const std::optional planned_start_time, deliveryoptimizer::api::WeatherImpactEstimate& impact) { - impact.planned_start_time = deliveryoptimizer::api::ReadRouteStartTime(input); + impact.planned_start_time = planned_start_time; if (!impact.planned_start_time.has_value()) { impact.estimated_finish_time = std::nullopt; return; @@ -344,15 +343,16 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o WeatherForecastOptions effective_options = options; WeatherImpactEstimate impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); - SetRouteTimes(input, impact); + const std::optional route_start_time = ReadRouteStartTime(input); + SetRouteTimes(route_start_time, impact); const OpenWeatherDelayEstimate openweather = FetchOpenWeatherDelayEstimate( options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, - ReadRouteStartTime(input), baseline_duration_seconds); + route_start_time, baseline_duration_seconds); if (openweather.available) { effective_options.weather_delay_seconds_per_stop = openweather.delay_seconds_per_stop; impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); impact.source = openweather.source; - SetRouteTimes(input, impact); + SetRouteTimes(route_start_time, impact); } return impact; @@ -420,12 +420,9 @@ Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options forecast["status"] = options.enabled ? "evaluated" : "disabled"; forecast["provider"] = impact.source; forecast["stop_count"] = impact.stop_count; - forecast["baseline_duration_seconds"] = impact.baseline_duration_seconds; forecast["baseline_route_duration_seconds"] = impact.baseline_route_duration_seconds; forecast["weather_delay_seconds"] = impact.weather_delay_seconds; forecast["weather_adjusted_duration_seconds"] = impact.weather_adjusted_duration_seconds; - forecast["predicted_duration_seconds"] = - impact.baseline_duration_seconds + impact.weather_delay_seconds; forecast["reoptimize_threshold_seconds"] = impact.reoptimize_threshold_seconds; if (impact.planned_start_time.has_value()) { forecast["planned_start_time"] = diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 2fb3def3f..c60be398d 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -55,6 +55,13 @@ namespace { return hour; } +[[nodiscard]] Json::Value BuildRainyThunderHour() { + Json::Value hour = BuildWeatherHour(0, 201); + hour["rain"] = Json::Value{Json::objectValue}; + hour["rain"]["1h"] = 2.0; + return hour; +} + } // namespace TEST(WeatherForecastOptimizerTest, DisabledWeatherHasNoImpact) { @@ -193,6 +200,16 @@ TEST(WeatherForecastOptimizerTest, ReadsBadOpenWeatherHourNearRouteStart) { EXPECT_EQ(delay, 240); } +TEST(WeatherForecastOptimizerTest, ThunderDoesNotAlsoChargeRainDelay) { + Json::Value body{Json::objectValue}; + body["hourly"] = Json::Value{Json::arrayValue}; + body["hourly"].append(BuildRainyThunderHour()); + + const int delay = deliveryoptimizer::api::ReadOpenWeatherDelay(body); + + EXPECT_EQ(delay, 240); +} + TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { auto input = BuildInput(); input.vehicles[0].time_window = deliveryoptimizer::api::TimeWindow{ @@ -216,11 +233,11 @@ TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { const Json::Value forecast = deliveryoptimizer::api::BuildWeatherForecastAnnotation(options, impact); - EXPECT_EQ(forecast["baseline_duration_seconds"].asInt(), 960); + EXPECT_FALSE(forecast.isMember("baseline_duration_seconds")); EXPECT_EQ(forecast["baseline_route_duration_seconds"].asInt(), 960); EXPECT_EQ(forecast["weather_delay_seconds"].asInt(), 400); EXPECT_EQ(forecast["weather_adjusted_duration_seconds"].asInt(), 1360); - EXPECT_EQ(forecast["predicted_duration_seconds"].asInt(), 1360); + EXPECT_FALSE(forecast.isMember("predicted_duration_seconds")); EXPECT_EQ(forecast["planned_start_time"].asInt64(), 600); EXPECT_EQ(forecast["estimated_finish_time"].asInt64(), 1960); } From 3d57661b6dfe6a1c24c087cada89e9da7570508e Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 12:41:40 -0700 Subject: [PATCH 08/26] Fix weather reroute formatting --- .../src/endpoints/deliveries_optimize_endpoint.cpp | 13 ++++++------- app/api/src/forecast_optimizer.cpp | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 3c0c9150d..d6987098b 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -187,9 +187,8 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, WeatherForecastOptions sync_weather_options = weather_options; sync_weather_options.openweather_api_key.clear(); - const WeatherImpactEstimate impact = - RecalculateWeatherImpact(sync_weather_options, *optimize_request_ptr, - *result.output); + const WeatherImpactEstimate impact = RecalculateWeatherImpact( + sync_weather_options, *optimize_request_ptr, *result.output); forecast = BuildWeatherForecastAnnotation(sync_weather_options, impact); if (!impact.should_reoptimize) { respond_with_completion(BuildSolveExecutionResponse( @@ -202,10 +201,10 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, [optimize_request_ptr, impact] { return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact); }, - [optimize_request_ptr, forecast, respond_with_completion]( - const CoordinatedSolveResult& rerun_result) mutable { - respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( - *optimize_request_ptr, rerun_result, forecast))); + [optimize_request_ptr, forecast, + respond_with_completion](const CoordinatedSolveResult& rerun_result) mutable { + respond_with_completion(BuildSolveExecutionResponse( + BuildSolveExecutionResult(*optimize_request_ptr, rerun_result, forecast))); }); if (rerun_status != SolveAdmissionStatus::kAccepted) { respond_with_completion(BuildAdmissionRejectionResponse(rerun_status)); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 972b11ac9..7826a9778 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -346,8 +346,8 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o const std::optional route_start_time = ReadRouteStartTime(input); SetRouteTimes(route_start_time, impact); const OpenWeatherDelayEstimate openweather = FetchOpenWeatherDelayEstimate( - options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, - route_start_time, baseline_duration_seconds); + options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, route_start_time, + baseline_duration_seconds); if (openweather.available) { effective_options.weather_delay_seconds_per_stop = openweather.delay_seconds_per_stop; impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); From e4767ea7bc0aaf17b3d3fcfcf6817368e0c87f5a Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 12:47:32 -0700 Subject: [PATCH 09/26] Clean up driver assist review items --- app/ui/src/app/driver_assist/summary/page.tsx | 38 +++++++++---------- app/ui/src/app/manifest.ts | 2 +- app/ui/src/tests/driverRouteImport.test.ts | 2 +- 3 files changed, 21 insertions(+), 21 deletions(-) diff --git a/app/ui/src/app/driver_assist/summary/page.tsx b/app/ui/src/app/driver_assist/summary/page.tsx index 449bdf678..3f09b54b6 100644 --- a/app/ui/src/app/driver_assist/summary/page.tsx +++ b/app/ui/src/app/driver_assist/summary/page.tsx @@ -1,10 +1,9 @@ "use client"; -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo, useState, useSyncExternalStore } from "react"; import { useRouter } from "next/navigation"; import { downloadRouteSummary } from "@/lib/driver-route/exportSummary"; -import type { DriverRoute } from "@/lib/driver-route/types"; import DriverFooter from "../components/DriverFooter"; import { WarningIcon } from "../components/icons"; @@ -13,27 +12,28 @@ import SummaryStatBlock from "./components/SummaryStatBlock"; import SummaryStopCard from "./components/SummaryStopCard"; import { summaryStyles as styles } from "./styles"; +function subscribeToRouteStore() { + return () => undefined; +} + +function readEmptyRoute() { + return null; +} + export default function DriverAssistSummaryPage() { const router = useRouter(); - const [route, setRoute] = useState(null); - const [hasCheckedRoute, setHasCheckedRoute] = useState(false); + const route = useSyncExternalStore( + subscribeToRouteStore, + readSavedRoute, + readEmptyRoute, + ); const [exportMessage, setExportMessage] = useState(null); useEffect(() => { - // Let the active route page finish its final localStorage write before the - // summary reads it back after the Finish tap. - const timeoutId = window.setTimeout(() => { - const savedRoute = readSavedRoute(); - setRoute(savedRoute); - setHasCheckedRoute(true); - - if (!savedRoute) { - router.replace("/upload-route"); - } - }, 0); - - return () => window.clearTimeout(timeoutId); - }, [router]); + if (!route) { + router.replace("/upload-route"); + } + }, [route, router]); const totals = useMemo(() => { // Failed stops count as remaining because they still need office review. @@ -58,7 +58,7 @@ export default function DriverAssistSummaryPage() { ); }; - if (!hasCheckedRoute || !route) { + if (!route) { // Keep the transition from the driver screen visually quiet. return
; } diff --git a/app/ui/src/app/manifest.ts b/app/ui/src/app/manifest.ts index 315604c44..1a2fccc9c 100644 --- a/app/ui/src/app/manifest.ts +++ b/app/ui/src/app/manifest.ts @@ -22,7 +22,7 @@ export default function manifest(): MetadataRoute.Manifest { src: "/pwa-icon.svg", sizes: "any", type: "image/svg+xml", - purpose: "maskable", + purpose: "any", }, ], }; diff --git a/app/ui/src/tests/driverRouteImport.test.ts b/app/ui/src/tests/driverRouteImport.test.ts index b811e4db5..6c6cf2089 100644 --- a/app/ui/src/tests/driverRouteImport.test.ts +++ b/app/ui/src/tests/driverRouteImport.test.ts @@ -101,7 +101,7 @@ describe("driver route import", () => { it("rejects files that do not match the saved session contract", () => { expect(() => loadSessionFromText(JSON.stringify({ version: 1 }))).toThrow( - 'Invalid save file format at "savedAt".', + 'Invalid save file format at "deliveries".', ); }); From 652a45b60991e8389345fff8a0d0daa16b0ea00d Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Sat, 27 Jun 2026 05:29:24 -0700 Subject: [PATCH 10/26] clarify weather recalculation options --- app/api/src/endpoints/deliveries_optimize_endpoint.cpp | 2 ++ app/api/src/forecast_optimizer.cpp | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index d6987098b..ae2f3e2d1 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -186,6 +186,8 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, } WeatherForecastOptions sync_weather_options = weather_options; + // Clear the key so recalculation short-circuits OpenWeather; sync path must not + // block the event loop. sync_weather_options.openweather_api_key.clear(); const WeatherImpactEstimate impact = RecalculateWeatherImpact( sync_weather_options, *optimize_request_ptr, *result.output); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 7826a9778..cd0cf5194 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -346,8 +346,8 @@ WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& o const std::optional route_start_time = ReadRouteStartTime(input); SetRouteTimes(route_start_time, impact); const OpenWeatherDelayEstimate openweather = FetchOpenWeatherDelayEstimate( - options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, route_start_time, - baseline_duration_seconds); + effective_options, Coordinate{.lon = input.depot_lon, .lat = input.depot_lat}, + route_start_time, baseline_duration_seconds); if (openweather.available) { effective_options.weather_delay_seconds_per_stop = openweather.delay_seconds_per_stop; impact = EstimateWeatherImpact(effective_options, input.jobs.size(), baseline_duration_seconds); @@ -389,6 +389,7 @@ std::optional ReadVroomDuration(const Json::Value& vroom_output) { WeatherImpactEstimate RecalculateWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, const Json::Value& vroom_output) { + // Callers choose whether OpenWeather may be queried by passing or clearing the API key. const std::optional summary_duration = ReadVroomDuration(vroom_output); if (!summary_duration.has_value()) { return EstimateRouteWeatherImpact(options, input, 0); From 51f2d484ffa90f00ef983969f032f6dc9fc74d02 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Sat, 27 Jun 2026 05:34:42 -0700 Subject: [PATCH 11/26] align driver route import error expectation --- app/ui/src/tests/driverRouteImport.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/ui/src/tests/driverRouteImport.test.ts b/app/ui/src/tests/driverRouteImport.test.ts index 6c6cf2089..b811e4db5 100644 --- a/app/ui/src/tests/driverRouteImport.test.ts +++ b/app/ui/src/tests/driverRouteImport.test.ts @@ -101,7 +101,7 @@ describe("driver route import", () => { it("rejects files that do not match the saved session contract", () => { expect(() => loadSessionFromText(JSON.stringify({ version: 1 }))).toThrow( - 'Invalid save file format at "deliveries".', + 'Invalid save file format at "savedAt".', ); }); From dd20793ea540fa60454bacbdbd777e9c35ae69c1 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 02:41:06 -0700 Subject: [PATCH 12/26] added traffic forecast confog --- .../api/forecast_optimizer.hpp | 10 ++++++++ app/api/src/forecast_optimizer.cpp | 25 +++++++++++++++++++ deploy/compose/docker-compose.arm64.yml | 5 ++++ deploy/env/http-server.arm64.env | 5 ++++ 4 files changed, 45 insertions(+) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 7ef899a8c..36540c20a 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -19,6 +19,14 @@ struct WeatherForecastOptions { std::string openweather_base_url; }; +struct TrafficForecastOptions { + bool enabled{false}; + int reoptimize_threshold_seconds{300}; + double reoptimize_threshold_percent{5.0}; + std::string google_maps_api_key; + std::string google_maps_base_url; +}; + struct OpenWeatherDelayEstimate { bool available{false}; int delay_seconds_per_stop{0}; @@ -41,6 +49,8 @@ struct WeatherImpactEstimate { [[nodiscard]] WeatherForecastOptions ResolveWeatherForecastOptionsFromEnv(); +[[nodiscard]] TrafficForecastOptions ResolveTrafficForecastOptionsFromEnv(); + [[nodiscard]] bool IsOpenWeatherConfigured(const WeatherForecastOptions& options); [[nodiscard]] int EstimateServiceSeconds(const OptimizeRequestInput& input); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index cd0cf5194..6cbe4cc71 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -32,9 +32,19 @@ constexpr std::string_view kWeatherThresholdPercentEnv = constexpr std::string_view kOpenWeatherApiKeyEnv = "OPENWEATHER_API_KEY"; constexpr std::string_view kOpenWeatherBaseUrlEnv = "OPENWEATHER_BASE_URL"; constexpr std::string_view kDefaultOpenWeatherBaseUrl = "https://api.openweathermap.org"; +constexpr std::string_view kTrafficEnabledEnv = "DELIVERYOPTIMIZER_TRAFFIC_FORECAST_ENABLED"; +constexpr std::string_view kTrafficThresholdSecondsEnv = + "DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_SECONDS"; +constexpr std::string_view kTrafficThresholdPercentEnv = + "DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_PERCENT"; +constexpr std::string_view kGoogleMapsApiKeyEnv = "GOOGLE_MAPS_API_KEY"; +constexpr std::string_view kGoogleMapsBaseUrlEnv = "GOOGLE_MAPS_BASE_URL"; +constexpr std::string_view kDefaultGoogleMapsBaseUrl = "https://maps.googleapis.com"; constexpr int kOpenWeatherTimeoutSeconds = 4; constexpr int kDefaultWeatherThresholdSeconds = 300; constexpr double kDefaultWeatherThresholdPercent = 5.0; +constexpr int kDefaultTrafficThresholdSeconds = 300; +constexpr double kDefaultTrafficThresholdPercent = 5.0; [[nodiscard]] bool IsEnabledFlag(const char* raw_value) { if (raw_value == nullptr || *raw_value == '\0') { @@ -186,6 +196,21 @@ WeatherForecastOptions ResolveWeatherForecastOptionsFromEnv() { }; } +TrafficForecastOptions ResolveTrafficForecastOptionsFromEnv() { + return TrafficForecastOptions{ + .enabled = IsEnabledFlag(std::getenv(kTrafficEnabledEnv.data())), + .reoptimize_threshold_seconds = + ParseNonNegativeInt(std::getenv(kTrafficThresholdSecondsEnv.data())) + .value_or(kDefaultTrafficThresholdSeconds), + .reoptimize_threshold_percent = + ParseNonNegativeDouble(std::getenv(kTrafficThresholdPercentEnv.data())) + .value_or(kDefaultTrafficThresholdPercent), + .google_maps_api_key = ResolveStringEnvOrDefault(kGoogleMapsApiKeyEnv.data(), ""), + .google_maps_base_url = + ResolveStringEnvOrDefault(kGoogleMapsBaseUrlEnv.data(), kDefaultGoogleMapsBaseUrl), + }; +} + bool IsOpenWeatherConfigured(const WeatherForecastOptions& options) { return options.enabled && !options.openweather_api_key.empty(); } diff --git a/deploy/compose/docker-compose.arm64.yml b/deploy/compose/docker-compose.arm64.yml index fe32cafe1..cde2f87cb 100644 --- a/deploy/compose/docker-compose.arm64.yml +++ b/deploy/compose/docker-compose.arm64.yml @@ -89,6 +89,11 @@ services: DELIVERYOPTIMIZER_WEATHER_REOPTIMIZE_THRESHOLD_PERCENT: ${DELIVERYOPTIMIZER_WEATHER_REOPTIMIZE_THRESHOLD_PERCENT:-5} OPENWEATHER_API_KEY: ${OPENWEATHER_API_KEY:-} OPENWEATHER_BASE_URL: ${OPENWEATHER_BASE_URL:-https://api.openweathermap.org} + DELIVERYOPTIMIZER_TRAFFIC_FORECAST_ENABLED: ${DELIVERYOPTIMIZER_TRAFFIC_FORECAST_ENABLED:-0} + DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_SECONDS: ${DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_SECONDS:-300} + DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_PERCENT: ${DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_PERCENT:-5} + GOOGLE_MAPS_API_KEY: ${GOOGLE_MAPS_API_KEY:-} + GOOGLE_MAPS_BASE_URL: ${GOOGLE_MAPS_BASE_URL:-https://maps.googleapis.com} depends_on: postgres: condition: service_healthy diff --git a/deploy/env/http-server.arm64.env b/deploy/env/http-server.arm64.env index 9a238cded..a2131bb0a 100644 --- a/deploy/env/http-server.arm64.env +++ b/deploy/env/http-server.arm64.env @@ -43,3 +43,8 @@ DELIVERYOPTIMIZER_WEATHER_REOPTIMIZE_THRESHOLD_SECONDS=300 DELIVERYOPTIMIZER_WEATHER_REOPTIMIZE_THRESHOLD_PERCENT=5 OPENWEATHER_API_KEY= OPENWEATHER_BASE_URL=https://api.openweathermap.org +DELIVERYOPTIMIZER_TRAFFIC_FORECAST_ENABLED=0 +DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_SECONDS=300 +DELIVERYOPTIMIZER_TRAFFIC_REOPTIMIZE_THRESHOLD_PERCENT=5 +GOOGLE_MAPS_API_KEY= +GOOGLE_MAPS_BASE_URL=https://maps.googleapis.com From 5222a40124acc7484eba62551d012052b2c3c810 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 02:49:00 -0700 Subject: [PATCH 13/26] added traffic delay and threshold logic --- .../api/forecast_optimizer.hpp | 13 +++++ app/api/src/forecast_optimizer.cpp | 22 ++++++++ .../weather_forecast_optimizer_test.cpp | 51 ++++++++++++++++++- 3 files changed, 85 insertions(+), 1 deletion(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 36540c20a..a0facd9a5 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -47,6 +47,15 @@ struct WeatherImpactEstimate { std::optional estimated_finish_time; }; +struct TrafficImpact { + int baseline_duration_seconds{0}; + int traffic_delay_seconds{0}; + int traffic_adjusted_duration_seconds{0}; + int reoptimize_threshold_seconds{300}; + bool should_reoptimize{false}; + std::string source; +}; + [[nodiscard]] WeatherForecastOptions ResolveWeatherForecastOptionsFromEnv(); [[nodiscard]] TrafficForecastOptions ResolveTrafficForecastOptionsFromEnv(); @@ -69,6 +78,10 @@ ReadOpenWeatherDelay(const Json::Value& body, std::size_t stop_count, int baseline_duration_seconds); +[[nodiscard]] TrafficImpact EstimateTrafficImpact(const TrafficForecastOptions& options, + int baseline_duration_seconds, + int traffic_delay_seconds, std::string source); + [[nodiscard]] WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, int baseline_duration_seconds); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 6cbe4cc71..35a34f596 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -362,6 +362,28 @@ WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& option }; } +TrafficImpact EstimateTrafficImpact(const TrafficForecastOptions& options, + const int baseline_duration_seconds, + const int traffic_delay_seconds, std::string source) { + const int normalized_baseline_seconds = std::max(baseline_duration_seconds, 0); + const int normalized_delay_seconds = options.enabled ? std::max(traffic_delay_seconds, 0) : 0; + const int percent_threshold_seconds = ClampToInt(static_cast( + std::ceil(static_cast(normalized_baseline_seconds) * + (std::max(options.reoptimize_threshold_percent, 0.0) / 100.0)))); + const int threshold_seconds = + std::max(std::max(options.reoptimize_threshold_seconds, 0), percent_threshold_seconds); + + return TrafficImpact{ + .baseline_duration_seconds = normalized_baseline_seconds, + .traffic_delay_seconds = normalized_delay_seconds, + .traffic_adjusted_duration_seconds = normalized_baseline_seconds + normalized_delay_seconds, + .reoptimize_threshold_seconds = threshold_seconds, + .should_reoptimize = + normalized_delay_seconds > 0 && normalized_delay_seconds >= threshold_seconds, + .source = options.enabled ? std::move(source) : "disabled", + }; +} + WeatherImpactEstimate EstimateRouteWeatherImpact(const WeatherForecastOptions& options, const OptimizeRequestInput& input, const int baseline_duration_seconds) { diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index c60be398d..930d2d07c 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -126,7 +126,6 @@ TEST(WeatherForecastOptimizerTest, AboveThresholdWeatherAddsServiceTime) { EXPECT_EQ(forecast["weather_delay_seconds"].asInt(), 400); EXPECT_TRUE(forecast["reoptimization"]["applied"].asBool()); } - TEST(WeatherForecastOptimizerTest, ReadsVroomSummaryDuration) { Json::Value output{Json::objectValue}; output["summary"] = Json::Value{Json::objectValue}; @@ -241,3 +240,53 @@ TEST(WeatherForecastOptimizerTest, RefinesForecastWithVroomSummaryDuration) { EXPECT_EQ(forecast["planned_start_time"].asInt64(), 600); EXPECT_EQ(forecast["estimated_finish_time"].asInt64(), 1960); } + +TEST(TrafficForecastOptimizerTest, DisabledTrafficHasNoImpact) { + const deliveryoptimizer::api::TrafficForecastOptions options{ + .enabled = false, + .reoptimize_threshold_seconds = 100, + .reoptimize_threshold_percent = 0.0, + .google_maps_api_key = "", + .google_maps_base_url = "", + }; + + const deliveryoptimizer::api::TrafficImpact impact = + deliveryoptimizer::api::EstimateTrafficImpact(options, 900, 300, "google_maps"); + + EXPECT_EQ(impact.traffic_delay_seconds, 0); + EXPECT_FALSE(impact.should_reoptimize); + EXPECT_EQ(impact.source, "disabled"); +} + +TEST(TrafficForecastOptimizerTest, BelowThresholdTrafficDoesNotReoptimize) { + const deliveryoptimizer::api::TrafficForecastOptions options{ + .enabled = true, + .reoptimize_threshold_seconds = 300, + .reoptimize_threshold_percent = 0.0, + .google_maps_api_key = "", + .google_maps_base_url = "", + }; + + const deliveryoptimizer::api::TrafficImpact impact = + deliveryoptimizer::api::EstimateTrafficImpact(options, 900, 120, "google_maps"); + + EXPECT_EQ(impact.traffic_delay_seconds, 120); + EXPECT_EQ(impact.traffic_adjusted_duration_seconds, 1020); + EXPECT_FALSE(impact.should_reoptimize); +} + +TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficReoptimizes) { + const deliveryoptimizer::api::TrafficForecastOptions options{ + .enabled = true, + .reoptimize_threshold_seconds = 100, + .reoptimize_threshold_percent = 0.0, + .google_maps_api_key = "", + .google_maps_base_url = "", + }; + + const deliveryoptimizer::api::TrafficImpact impact = + deliveryoptimizer::api::EstimateTrafficImpact(options, 900, 180, "google_maps"); + + EXPECT_EQ(impact.traffic_delay_seconds, 180); + EXPECT_TRUE(impact.should_reoptimize); +} From 78851e476587d420312df01428de11aeda70f354 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 03:09:29 -0700 Subject: [PATCH 14/26] added Google traffic response helpers --- .../api/forecast_optimizer.hpp | 6 +++ app/api/src/forecast_optimizer.cpp | 37 +++++++++++++++++++ .../weather_forecast_optimizer_test.cpp | 37 +++++++++++++++++++ 3 files changed, 80 insertions(+) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index a0facd9a5..daeb25fce 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -74,6 +74,12 @@ ReadOpenWeatherDelay(const Json::Value& body, std::optional route_start_time = std::nullopt, std::optional route_duration_seconds = std::nullopt); +[[nodiscard]] std::string BuildTrafficPath(const Coordinate& origin, const Coordinate& destination, + std::chrono::sys_seconds departure_time, + const std::string& api_key); + +[[nodiscard]] std::optional ReadTrafficDelay(const Json::Value& body); + [[nodiscard]] WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, std::size_t stop_count, int baseline_duration_seconds); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 35a34f596..83e4c8dca 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -331,6 +331,43 @@ int ReadOpenWeatherDelay(const Json::Value& body, return delay_seconds; } +std::string BuildTrafficPath(const Coordinate& origin, const Coordinate& destination, + const std::chrono::sys_seconds departure_time, + const std::string& api_key) { + const std::string origin_text = FormatCoordinate(origin.lat) + "," + FormatCoordinate(origin.lon); + const std::string destination_text = + FormatCoordinate(destination.lat) + "," + FormatCoordinate(destination.lon); + const auto departure_seconds = + std::chrono::duration_cast(departure_time.time_since_epoch()).count(); + + return "/maps/api/distancematrix/json?origins=" + origin_text + + "&destinations=" + destination_text + + "&departure_time=" + std::to_string(departure_seconds) + + "&traffic_model=best_guess&key=" + api_key; +} + +std::optional ReadTrafficDelay(const Json::Value& body) { + const Json::Value& rows = body["rows"]; + if (!rows.isArray() || rows.empty()) { + return std::nullopt; + } + + const Json::Value& elements = rows[0]["elements"]; + if (!elements.isArray() || elements.empty()) { + return std::nullopt; + } + + const Json::Value& leg = elements[0]; + if (leg["status"].isString() && leg["status"].asString() != "OK") { + return std::nullopt; + } + if (!leg["duration"]["value"].isInt() || !leg["duration_in_traffic"]["value"].isInt()) { + return std::nullopt; + } + + return std::max(leg["duration_in_traffic"]["value"].asInt() - leg["duration"]["value"].asInt(), + 0); +} WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, const std::size_t stop_count, const int baseline_duration_seconds) { diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 930d2d07c..8bab77485 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -290,3 +290,40 @@ TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficReoptimizes) { EXPECT_EQ(impact.traffic_delay_seconds, 180); EXPECT_TRUE(impact.should_reoptimize); } +TEST(TrafficForecastOptimizerTest, BuildsGoogleTrafficPath) { + const std::string path = deliveryoptimizer::api::BuildTrafficPath( + deliveryoptimizer::api::Coordinate{.lon = -121.7405, .lat = 38.5449}, + deliveryoptimizer::api::Coordinate{.lon = -121.752, .lat = 38.548}, + std::chrono::sys_seconds{std::chrono::seconds{1800}}, "test-key"); + + EXPECT_NE(path.find("/maps/api/distancematrix/json?"), std::string::npos); + EXPECT_NE(path.find("origins=38.544900,-121.740500"), std::string::npos); + EXPECT_NE(path.find("destinations=38.548000,-121.752000"), std::string::npos); + EXPECT_NE(path.find("departure_time=1800"), std::string::npos); + EXPECT_NE(path.find("traffic_model=best_guess"), std::string::npos); + EXPECT_NE(path.find("key=test-key"), std::string::npos); +} + +TEST(TrafficForecastOptimizerTest, ReadsGoogleTrafficDelay) { + Json::Value body{Json::objectValue}; + body["rows"] = Json::Value{Json::arrayValue}; + Json::Value row{Json::objectValue}; + row["elements"] = Json::Value{Json::arrayValue}; + Json::Value leg{Json::objectValue}; + leg["status"] = "OK"; + leg["duration"]["value"] = 600; + leg["duration_in_traffic"]["value"] = 780; + row["elements"].append(leg); + body["rows"].append(row); + + const std::optional delay = deliveryoptimizer::api::ReadTrafficDelay(body); + + ASSERT_TRUE(delay.has_value()); + EXPECT_EQ(*delay, 180); +} + +TEST(TrafficForecastOptimizerTest, IgnoresMissingTrafficDuration) { + const Json::Value body{Json::objectValue}; + + EXPECT_FALSE(deliveryoptimizer::api::ReadTrafficDelay(body).has_value()); +} From 1c8edfc384819ccb29e7e53a7641e7f0f7bd86cc Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 03:18:47 -0700 Subject: [PATCH 15/26] added google traffic route delay --- .../api/forecast_optimizer.hpp | 26 ++++ app/api/src/forecast_optimizer.cpp | 130 ++++++++++++++++++ .../weather_forecast_optimizer_test.cpp | 41 ++++++ 3 files changed, 197 insertions(+) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index daeb25fce..78105ebb1 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -7,6 +7,7 @@ #include #include #include +#include namespace deliveryoptimizer::api { @@ -56,6 +57,18 @@ struct TrafficImpact { std::string source; }; +struct TrafficDelayEstimate { + bool available{false}; + int delay_seconds{0}; + std::string source; +}; + +struct TrafficLeg { + Coordinate origin; + Coordinate destination; + std::chrono::sys_seconds departure_time; +}; + [[nodiscard]] WeatherForecastOptions ResolveWeatherForecastOptionsFromEnv(); [[nodiscard]] TrafficForecastOptions ResolveTrafficForecastOptionsFromEnv(); @@ -74,12 +87,25 @@ ReadOpenWeatherDelay(const Json::Value& body, std::optional route_start_time = std::nullopt, std::optional route_duration_seconds = std::nullopt); +[[nodiscard]] bool IsGoogleMapsConfigured(const TrafficForecastOptions& options); + [[nodiscard]] std::string BuildTrafficPath(const Coordinate& origin, const Coordinate& destination, std::chrono::sys_seconds departure_time, const std::string& api_key); [[nodiscard]] std::optional ReadTrafficDelay(const Json::Value& body); +[[nodiscard]] TrafficDelayEstimate FetchTrafficDelay(const TrafficForecastOptions& options, + const TrafficLeg& leg); + +[[nodiscard]] std::vector +ReadTrafficLegs(const Json::Value& vroom_output, + std::optional route_start_time = std::nullopt); + +[[nodiscard]] TrafficDelayEstimate +ReadRouteTraffic(const TrafficForecastOptions& options, const Json::Value& vroom_output, + std::optional route_start_time = std::nullopt); + [[nodiscard]] WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, std::size_t stop_count, int baseline_duration_seconds); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 83e4c8dca..525791176 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -41,6 +41,7 @@ constexpr std::string_view kGoogleMapsApiKeyEnv = "GOOGLE_MAPS_API_KEY"; constexpr std::string_view kGoogleMapsBaseUrlEnv = "GOOGLE_MAPS_BASE_URL"; constexpr std::string_view kDefaultGoogleMapsBaseUrl = "https://maps.googleapis.com"; constexpr int kOpenWeatherTimeoutSeconds = 4; +constexpr int kGoogleMapsTimeoutSeconds = 4; constexpr int kDefaultWeatherThresholdSeconds = 300; constexpr double kDefaultWeatherThresholdPercent = 5.0; constexpr int kDefaultTrafficThresholdSeconds = 300; @@ -227,6 +228,10 @@ int EstimateServiceSeconds(const OptimizeRequestInput& input) { return static_cast(total); } +bool IsGoogleMapsConfigured(const TrafficForecastOptions& options) { + return options.enabled && !options.google_maps_api_key.empty(); +} + OpenWeatherDelayEstimate FetchOpenWeatherDelayEstimate(const WeatherForecastOptions& options, const Coordinate& coordinate, const std::optional route_start_time, @@ -368,6 +373,131 @@ std::optional ReadTrafficDelay(const Json::Value& body) { return std::max(leg["duration_in_traffic"]["value"].asInt() - leg["duration"]["value"].asInt(), 0); } + +TrafficDelayEstimate FetchTrafficDelay(const TrafficForecastOptions& options, + const TrafficLeg& leg) { + if (!IsGoogleMapsConfigured(options)) { + return TrafficDelayEstimate{ + .available = false, + .delay_seconds = 0, + .source = "", + }; + } + + auto client = drogon::HttpClient::newHttpClient(options.google_maps_base_url); + auto request = drogon::HttpRequest::newHttpRequest(); + request->setMethod(drogon::Get); + request->setPath(BuildTrafficPath(leg.origin, leg.destination, leg.departure_time, + options.google_maps_api_key)); + + auto promise = std::make_shared>(); + auto future = promise->get_future(); + client->sendRequest( + request, + [promise](const drogon::ReqResult result, const drogon::HttpResponsePtr& response) { + if (result != drogon::ReqResult::Ok || response == nullptr || + response->getStatusCode() != drogon::k200OK) { + promise->set_value(TrafficDelayEstimate{ + .available = false, + .delay_seconds = 0, + .source = "", + }); + return; + } + + const auto body = response->getJsonObject(); + const std::optional delay = body == nullptr ? std::nullopt : ReadTrafficDelay(*body); + promise->set_value(TrafficDelayEstimate{ + .available = delay.has_value(), + .delay_seconds = delay.value_or(0), + .source = delay.has_value() ? "google_maps" : "", + }); + }, + kGoogleMapsTimeoutSeconds); + + if (future.wait_for(std::chrono::seconds{kGoogleMapsTimeoutSeconds + 1}) != + std::future_status::ready) { + return TrafficDelayEstimate{ + .available = false, + .delay_seconds = 0, + .source = "", + }; + } + + return future.get(); +} + +std::vector +ReadTrafficLegs(const Json::Value& vroom_output, + const std::optional route_start_time) { + const Json::Value& routes = vroom_output["routes"]; + if (!routes.isArray()) { + return {}; + } + + const std::chrono::sys_seconds start_time = route_start_time.value_or( + std::chrono::time_point_cast(std::chrono::system_clock::now())); + std::vector legs; + for (const Json::Value& route : routes) { + const Json::Value& steps = route["steps"]; + if (!steps.isArray() || steps.size() < 2U) { + continue; + } + + for (Json::ArrayIndex index = 1U; index < steps.size(); ++index) { + const Json::Value& from = steps[index - 1U]; + const Json::Value& to = steps[index]; + const Json::Value& from_location = from["location"]; + const Json::Value& to_location = to["location"]; + if (!from_location.isArray() || from_location.size() != 2U || !to_location.isArray() || + to_location.size() != 2U) { + continue; + } + + const int arrival = from["arrival"].isInt() ? from["arrival"].asInt() : 0; + const int service = from["service"].isInt() ? from["service"].asInt() : 0; + legs.push_back(TrafficLeg{ + .origin = + Coordinate{.lon = from_location[0U].asDouble(), .lat = from_location[1U].asDouble()}, + .destination = + Coordinate{.lon = to_location[0U].asDouble(), .lat = to_location[1U].asDouble()}, + .departure_time = start_time + std::chrono::seconds{std::max(arrival + service, 0)}, + }); + } + } + + return legs; +} + +TrafficDelayEstimate +ReadRouteTraffic(const TrafficForecastOptions& options, const Json::Value& vroom_output, + const std::optional route_start_time) { + if (!IsGoogleMapsConfigured(options)) { + return TrafficDelayEstimate{ + .available = false, + .delay_seconds = 0, + .source = "", + }; + } + + int delay_seconds = 0; + bool saw_traffic = false; + for (const TrafficLeg& leg : ReadTrafficLegs(vroom_output, route_start_time)) { + const TrafficDelayEstimate estimate = FetchTrafficDelay(options, leg); + if (!estimate.available) { + continue; + } + delay_seconds += estimate.delay_seconds; + saw_traffic = true; + } + + return TrafficDelayEstimate{ + .available = saw_traffic, + .delay_seconds = delay_seconds, + .source = saw_traffic ? "google_maps" : "", + }; +} + WeatherImpactEstimate EstimateWeatherImpact(const WeatherForecastOptions& options, const std::size_t stop_count, const int baseline_duration_seconds) { diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 8bab77485..1bc1c5b92 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -327,3 +327,44 @@ TEST(TrafficForecastOptimizerTest, IgnoresMissingTrafficDuration) { EXPECT_FALSE(deliveryoptimizer::api::ReadTrafficDelay(body).has_value()); } +TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { + Json::Value output{Json::objectValue}; + output["routes"] = Json::Value{Json::arrayValue}; + + Json::Value route{Json::objectValue}; + route["steps"] = Json::Value{Json::arrayValue}; + + Json::Value start{Json::objectValue}; + start["arrival"] = 0; + start["location"] = Json::Value{Json::arrayValue}; + start["location"].append(-121.7405); + start["location"].append(38.5449); + + Json::Value stop{Json::objectValue}; + stop["arrival"] = 600; + stop["service"] = 120; + stop["location"] = Json::Value{Json::arrayValue}; + stop["location"].append(-121.752); + stop["location"].append(38.548); + + Json::Value end{Json::objectValue}; + end["arrival"] = 1200; + end["location"] = Json::Value{Json::arrayValue}; + end["location"].append(-121.7405); + end["location"].append(38.5449); + + route["steps"].append(start); + route["steps"].append(stop); + route["steps"].append(end); + output["routes"].append(route); + + const std::vector legs = + deliveryoptimizer::api::ReadTrafficLegs(output, + std::chrono::sys_seconds{std::chrono::seconds{1800}}); + + ASSERT_EQ(legs.size(), 2U); + EXPECT_EQ(legs[0].departure_time.time_since_epoch(), std::chrono::seconds{1800}); + EXPECT_EQ(legs[1].departure_time.time_since_epoch(), std::chrono::seconds{2520}); + EXPECT_DOUBLE_EQ(legs[0].origin.lon, -121.7405); + EXPECT_DOUBLE_EQ(legs[0].destination.lon, -121.752); +} From cb31613fb98b22dfdf7f31b7cc4f57aed8bb519f Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 03:27:02 -0700 Subject: [PATCH 16/26] feat: use traffic delay in route solve --- .../api/forecast_optimizer.hpp | 7 +++ .../deliveries_optimize_endpoint.cpp | 60 ++++++++++++++++--- app/api/src/forecast_optimizer.cpp | 37 ++++++++++++ app/api/src/optimization_job_runtime.cpp | 13 ++++ .../weather_forecast_optimizer_test.cpp | 45 ++++++++++++++ 5 files changed, 154 insertions(+), 8 deletions(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 78105ebb1..3da68d390 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -130,7 +130,14 @@ ReadRouteStartTime(const OptimizeRequestInput& input); [[nodiscard]] Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& impact); +[[nodiscard]] Json::Value BuildTrafficAdjustedVroomInput(const OptimizeRequestInput& input, + const WeatherImpactEstimate& weather, + const TrafficImpact& traffic); + [[nodiscard]] Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options, const WeatherImpactEstimate& impact); +void AddTrafficForecast(Json::Value& forecast, const TrafficForecastOptions& options, + const TrafficImpact& impact); + } // namespace deliveryoptimizer::api diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index ae2f3e2d1..8a3ba196c 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -102,13 +102,14 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, const SolveAdmissionConfig& admission_config, std::shared_ptr observability) { const WeatherForecastOptions weather_options = ResolveWeatherForecastOptionsFromEnv(); + const TrafficForecastOptions traffic_options = ResolveTrafficForecastOptionsFromEnv(); auto runner = std::make_shared(ResolveVroomRuntimeConfigFromEnv()); auto coordinator = std::make_shared(admission_config, runner, SolveCoordinatorOptions{}, observability); app.registerHandler( "/api/v1/deliveries/optimize", - [coordinator = std::move(coordinator), weather_options, + [coordinator = std::move(coordinator), weather_options, traffic_options, observability = std::move(observability)]( const drogon::HttpRequestPtr& request, std::function&& callback) { @@ -176,7 +177,7 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, }; const SolveAdmissionStatus admission_status = coordinator->Submit( request_size, [optimize_request_ptr] { return BuildVroomInput(*optimize_request_ptr); }, - [coordinator, optimize_request_ptr, request_size, weather_options, + [coordinator, optimize_request_ptr, request_size, weather_options, traffic_options, respond_with_completion](const CoordinatedSolveResult& result) mutable { std::optional forecast; if (!result.output.has_value()) { @@ -192,9 +193,54 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, const WeatherImpactEstimate impact = RecalculateWeatherImpact( sync_weather_options, *optimize_request_ptr, *result.output); forecast = BuildWeatherForecastAnnotation(sync_weather_options, impact); + + const auto finish_with_traffic = + [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, + impact, respond_with_completion](const CoordinatedSolveResult& weather_result) { + std::optional final_forecast = forecast; + if (!weather_result.output.has_value()) { + respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( + *optimize_request_ptr, weather_result, final_forecast))); + return; + } + + if (final_forecast.has_value()) { + const TrafficDelayEstimate traffic_delay = + ReadRouteTraffic(traffic_options, *weather_result.output, + ReadRouteStartTime(*optimize_request_ptr)); + const TrafficImpact traffic = EstimateTrafficImpact( + traffic_options, ReadVroomDuration(*weather_result.output).value_or(0), + traffic_delay.delay_seconds, traffic_delay.source); + AddTrafficForecast(*final_forecast, traffic_options, traffic); + + if (traffic.should_reoptimize) { + const SolveAdmissionStatus traffic_rerun_status = coordinator->Submit( + request_size, + [optimize_request_ptr, impact, traffic] { + return BuildTrafficAdjustedVroomInput(*optimize_request_ptr, impact, + traffic); + }, + [optimize_request_ptr, final_forecast, + respond_with_completion]( + const CoordinatedSolveResult& traffic_result) mutable { + respond_with_completion(BuildSolveExecutionResponse( + BuildSolveExecutionResult(*optimize_request_ptr, traffic_result, + final_forecast))); + }); + if (traffic_rerun_status != SolveAdmissionStatus::kAccepted) { + respond_with_completion( + BuildAdmissionRejectionResponse(traffic_rerun_status)); + } + return; + } + } + + respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( + *optimize_request_ptr, weather_result, final_forecast))); + }; + if (!impact.should_reoptimize) { - respond_with_completion(BuildSolveExecutionResponse( - BuildSolveExecutionResult(*optimize_request_ptr, result, forecast))); + finish_with_traffic(result); return; } @@ -203,10 +249,8 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, [optimize_request_ptr, impact] { return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact); }, - [optimize_request_ptr, forecast, - respond_with_completion](const CoordinatedSolveResult& rerun_result) mutable { - respond_with_completion(BuildSolveExecutionResponse( - BuildSolveExecutionResult(*optimize_request_ptr, rerun_result, forecast))); + [finish_with_traffic](const CoordinatedSolveResult& rerun_result) mutable { + finish_with_traffic(rerun_result); }); if (rerun_status != SolveAdmissionStatus::kAccepted) { respond_with_completion(BuildAdmissionRejectionResponse(rerun_status)); diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 525791176..3988d0742 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -629,6 +629,25 @@ Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, return payload; } +Json::Value BuildTrafficAdjustedVroomInput(const OptimizeRequestInput& input, + const WeatherImpactEstimate& weather, + const TrafficImpact& traffic) { + Json::Value payload = BuildWeatherAdjustedVroomInput(input, weather); + if (!traffic.should_reoptimize || input.jobs.empty()) { + return payload; + } + + const int delay_per_stop = static_cast(std::ceil( + static_cast(traffic.traffic_delay_seconds) / static_cast(input.jobs.size()))); + for (Json::ArrayIndex index = 0; index < payload["jobs"].size(); ++index) { + Json::Value& job = payload["jobs"][index]; + const int current_service = job["service"].isInt() ? job["service"].asInt() : 0; + job["service"] = current_service + delay_per_stop; + } + + return payload; +} + Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options, const WeatherImpactEstimate& impact) { Json::Value forecast{Json::objectValue}; @@ -657,4 +676,22 @@ Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options return forecast; } +void AddTrafficForecast(Json::Value& forecast, const TrafficForecastOptions& options, + const TrafficImpact& impact) { + Json::Value traffic{Json::objectValue}; + traffic["status"] = options.enabled ? "evaluated" : "disabled"; + traffic["provider"] = impact.source; + traffic["baseline_duration_seconds"] = impact.baseline_duration_seconds; + traffic["traffic_delay_seconds"] = impact.traffic_delay_seconds; + traffic["traffic_adjusted_duration_seconds"] = impact.traffic_adjusted_duration_seconds; + traffic["reoptimize_threshold_seconds"] = impact.reoptimize_threshold_seconds; + + Json::Value reoptimization{Json::objectValue}; + reoptimization["applied"] = impact.should_reoptimize; + reoptimization["reason"] = impact.should_reoptimize ? "traffic_delay_crossed_threshold" + : "traffic_delay_below_threshold"; + traffic["reoptimization"] = std::move(reoptimization); + forecast["traffic"] = std::move(traffic); +} + } // namespace deliveryoptimizer::api diff --git a/app/api/src/optimization_job_runtime.cpp b/app/api/src/optimization_job_runtime.cpp index d057470cf..c1d831f4f 100644 --- a/app/api/src/optimization_job_runtime.cpp +++ b/app/api/src/optimization_job_runtime.cpp @@ -166,6 +166,19 @@ void OptimizationJobRuntime::WorkerLoop(const std::stop_token stop_token, final_result = ToCoordinatedSolveResult( runner_->Run(BuildWeatherAdjustedVroomInput(parsed_request->input, impact))); } + if (final_result.output.has_value()) { + const TrafficForecastOptions traffic_options = ResolveTrafficForecastOptionsFromEnv(); + const TrafficDelayEstimate traffic_delay = ReadRouteTraffic( + traffic_options, *final_result.output, ReadRouteStartTime(parsed_request->input)); + const TrafficImpact traffic = EstimateTrafficImpact( + traffic_options, ReadVroomDuration(*final_result.output).value_or(0), + traffic_delay.delay_seconds, traffic_delay.source); + AddTrafficForecast(*forecast, traffic_options, traffic); + if (traffic.should_reoptimize) { + final_result = ToCoordinatedSolveResult(runner_->Run( + BuildTrafficAdjustedVroomInput(parsed_request->input, impact, traffic))); + } + } } const auto solve_result = BuildSolveExecutionResult(parsed_request->input, final_result, forecast); diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 1bc1c5b92..a56023e6a 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -290,6 +290,26 @@ TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficReoptimizes) { EXPECT_EQ(impact.traffic_delay_seconds, 180); EXPECT_TRUE(impact.should_reoptimize); } +TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficAddsServiceTime) { + const auto input = BuildInput(); + const deliveryoptimizer::api::WeatherImpactEstimate weather{}; + const deliveryoptimizer::api::TrafficImpact traffic{ + .baseline_duration_seconds = 900, + .traffic_delay_seconds = 180, + .traffic_adjusted_duration_seconds = 1080, + .reoptimize_threshold_seconds = 100, + .should_reoptimize = true, + .source = "google_maps", + }; + + const Json::Value payload = + deliveryoptimizer::api::BuildTrafficAdjustedVroomInput(input, weather, traffic); + + ASSERT_TRUE(payload["jobs"].isArray()); + ASSERT_EQ(payload["jobs"].size(), 2U); + EXPECT_EQ(payload["jobs"][0]["service"].asInt(), 270); + EXPECT_EQ(payload["jobs"][1]["service"].asInt(), 210); +} TEST(TrafficForecastOptimizerTest, BuildsGoogleTrafficPath) { const std::string path = deliveryoptimizer::api::BuildTrafficPath( deliveryoptimizer::api::Coordinate{.lon = -121.7405, .lat = 38.5449}, @@ -327,6 +347,31 @@ TEST(TrafficForecastOptimizerTest, IgnoresMissingTrafficDuration) { EXPECT_FALSE(deliveryoptimizer::api::ReadTrafficDelay(body).has_value()); } +TEST(TrafficForecastOptimizerTest, AddsTrafficForecastBlock) { + Json::Value forecast{Json::objectValue}; + const deliveryoptimizer::api::TrafficForecastOptions options{ + .enabled = true, + .reoptimize_threshold_seconds = 100, + .reoptimize_threshold_percent = 0.0, + .google_maps_api_key = "", + .google_maps_base_url = "", + }; + const deliveryoptimizer::api::TrafficImpact impact{ + .baseline_duration_seconds = 900, + .traffic_delay_seconds = 180, + .traffic_adjusted_duration_seconds = 1080, + .reoptimize_threshold_seconds = 100, + .should_reoptimize = true, + .source = "google_maps", + }; + + deliveryoptimizer::api::AddTrafficForecast(forecast, options, impact); + + EXPECT_EQ(forecast["traffic"]["status"].asString(), "evaluated"); + EXPECT_EQ(forecast["traffic"]["provider"].asString(), "google_maps"); + EXPECT_EQ(forecast["traffic"]["traffic_delay_seconds"].asInt(), 180); + EXPECT_TRUE(forecast["traffic"]["reoptimization"]["applied"].asBool()); +} TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { Json::Value output{Json::objectValue}; output["routes"] = Json::Value{Json::arrayValue}; From 14d69f0f5c899c6d84bcda133bc7dca6b7c6b6da Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 03:44:34 -0700 Subject: [PATCH 17/26] fix: traffic leg departure times --- app/api/src/forecast_optimizer.cpp | 26 +++++++++++++++---- .../weather_forecast_optimizer_test.cpp | 16 +++++++----- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 3988d0742..38aa06e0a 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -176,6 +176,26 @@ void SetRouteTimes(const std::optional planned_start_t *impact.planned_start_time + std::chrono::seconds{impact.weather_adjusted_duration_seconds}; } +[[nodiscard]] std::chrono::sys_seconds +ReadLegDeparture(const Json::Value& step, + const std::optional route_start_time) { + const int arrival = step["arrival"].isInt() ? step["arrival"].asInt() : 0; + const int service = step["service"].isInt() ? step["service"].asInt() : 0; + const std::chrono::seconds offset{std::max(arrival + service, 0)}; + if (route_start_time.has_value()) { + const std::chrono::seconds route_start_seconds = + std::chrono::duration_cast(route_start_time->time_since_epoch()); + if (offset >= route_start_seconds - std::chrono::hours{24}) { + return std::chrono::sys_seconds{offset}; + } + + return *route_start_time + offset; + } + + return std::chrono::time_point_cast(std::chrono::system_clock::now()) + + offset; +} + } // namespace namespace deliveryoptimizer::api { @@ -435,8 +455,6 @@ ReadTrafficLegs(const Json::Value& vroom_output, return {}; } - const std::chrono::sys_seconds start_time = route_start_time.value_or( - std::chrono::time_point_cast(std::chrono::system_clock::now())); std::vector legs; for (const Json::Value& route : routes) { const Json::Value& steps = route["steps"]; @@ -454,14 +472,12 @@ ReadTrafficLegs(const Json::Value& vroom_output, continue; } - const int arrival = from["arrival"].isInt() ? from["arrival"].asInt() : 0; - const int service = from["service"].isInt() ? from["service"].asInt() : 0; legs.push_back(TrafficLeg{ .origin = Coordinate{.lon = from_location[0U].asDouble(), .lat = from_location[1U].asDouble()}, .destination = Coordinate{.lon = to_location[0U].asDouble(), .lat = to_location[1U].asDouble()}, - .departure_time = start_time + std::chrono::seconds{std::max(arrival + service, 0)}, + .departure_time = ReadLegDeparture(from, route_start_time), }); } } diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index a56023e6a..dcf0546eb 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -373,6 +373,8 @@ TEST(TrafficForecastOptimizerTest, AddsTrafficForecastBlock) { EXPECT_TRUE(forecast["traffic"]["reoptimization"]["applied"].asBool()); } TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { + constexpr int kRouteStart = 1767225600; + Json::Value output{Json::objectValue}; output["routes"] = Json::Value{Json::arrayValue}; @@ -380,20 +382,20 @@ TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { route["steps"] = Json::Value{Json::arrayValue}; Json::Value start{Json::objectValue}; - start["arrival"] = 0; + start["arrival"] = kRouteStart; start["location"] = Json::Value{Json::arrayValue}; start["location"].append(-121.7405); start["location"].append(38.5449); Json::Value stop{Json::objectValue}; - stop["arrival"] = 600; + stop["arrival"] = kRouteStart + 600; stop["service"] = 120; stop["location"] = Json::Value{Json::arrayValue}; stop["location"].append(-121.752); stop["location"].append(38.548); Json::Value end{Json::objectValue}; - end["arrival"] = 1200; + end["arrival"] = kRouteStart + 1200; end["location"] = Json::Value{Json::arrayValue}; end["location"].append(-121.7405); end["location"].append(38.5449); @@ -404,12 +406,12 @@ TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { output["routes"].append(route); const std::vector legs = - deliveryoptimizer::api::ReadTrafficLegs(output, - std::chrono::sys_seconds{std::chrono::seconds{1800}}); + deliveryoptimizer::api::ReadTrafficLegs( + output, std::chrono::sys_seconds{std::chrono::seconds{kRouteStart}}); ASSERT_EQ(legs.size(), 2U); - EXPECT_EQ(legs[0].departure_time.time_since_epoch(), std::chrono::seconds{1800}); - EXPECT_EQ(legs[1].departure_time.time_since_epoch(), std::chrono::seconds{2520}); + EXPECT_EQ(legs[0].departure_time.time_since_epoch(), std::chrono::seconds{kRouteStart}); + EXPECT_EQ(legs[1].departure_time.time_since_epoch(), std::chrono::seconds{kRouteStart + 720}); EXPECT_DOUBLE_EQ(legs[0].origin.lon, -121.7405); EXPECT_DOUBLE_EQ(legs[0].destination.lon, -121.752); } From 498ea31b6380a1a76d0b899ee1a1dc258e43a90c Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Fri, 22 May 2026 03:54:47 -0700 Subject: [PATCH 18/26] comment to clarify traffic leg timing test --- tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index dcf0546eb..a9eb8968b 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -373,6 +373,7 @@ TEST(TrafficForecastOptimizerTest, AddsTrafficForecastBlock) { EXPECT_TRUE(forecast["traffic"]["reoptimization"]["applied"].asBool()); } TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { + // These arrivals already include the route start time. constexpr int kRouteStart = 1767225600; Json::Value output{Json::objectValue}; From 89879bcaee37915caee55300ad854b4d13fa2902 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 17:25:23 -0700 Subject: [PATCH 19/26] fix: avoid blocking sync traffic forecasts --- .../deliveries_optimize_endpoint.cpp | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 8a3ba196c..427afb543 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -196,22 +197,29 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, const auto finish_with_traffic = [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, - impact, respond_with_completion](const CoordinatedSolveResult& weather_result) { - std::optional final_forecast = forecast; + impact, + respond_with_completion](CoordinatedSolveResult weather_result) { if (!weather_result.output.has_value()) { respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( - *optimize_request_ptr, weather_result, final_forecast))); + *optimize_request_ptr, weather_result, forecast))); return; } - if (final_forecast.has_value()) { + std::thread([coordinator, optimize_request_ptr, request_size, traffic_options, + forecast, impact, respond_with_completion, + weather_result = std::move(weather_result)]() mutable { + std::optional final_forecast = forecast; + const Json::Value& route_output = *weather_result.output; + const TrafficDelayEstimate traffic_delay = - ReadRouteTraffic(traffic_options, *weather_result.output, + ReadRouteTraffic(traffic_options, route_output, ReadRouteStartTime(*optimize_request_ptr)); const TrafficImpact traffic = EstimateTrafficImpact( - traffic_options, ReadVroomDuration(*weather_result.output).value_or(0), + traffic_options, ReadVroomDuration(route_output).value_or(0), traffic_delay.delay_seconds, traffic_delay.source); - AddTrafficForecast(*final_forecast, traffic_options, traffic); + if (final_forecast.has_value()) { + AddTrafficForecast(*final_forecast, traffic_options, traffic); + } if (traffic.should_reoptimize) { const SolveAdmissionStatus traffic_rerun_status = coordinator->Submit( @@ -233,10 +241,10 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, } return; } - } - respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( - *optimize_request_ptr, weather_result, final_forecast))); + respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( + *optimize_request_ptr, weather_result, final_forecast))); + }).detach(); }; if (!impact.should_reoptimize) { From 2eca47bbc4b7088bb8406d26d42dc1873a5ba584 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 17:29:36 -0700 Subject: [PATCH 20/26] Store traffic forecast options once --- .../deliveryoptimizer/api/optimization_job_runtime.hpp | 1 + app/api/src/optimization_job_runtime.cpp | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/app/api/include/deliveryoptimizer/api/optimization_job_runtime.hpp b/app/api/include/deliveryoptimizer/api/optimization_job_runtime.hpp index 96c223716..c7a46ba0f 100644 --- a/app/api/include/deliveryoptimizer/api/optimization_job_runtime.hpp +++ b/app/api/include/deliveryoptimizer/api/optimization_job_runtime.hpp @@ -67,6 +67,7 @@ class OptimizationJobRuntime { std::shared_ptr observability_; OptimizationJobRuntimeOptions options_; WeatherForecastOptions weather_options_; + TrafficForecastOptions traffic_options_; std::deque worker_states_; std::vector workers_; std::jthread heartbeat_thread_; diff --git a/app/api/src/optimization_job_runtime.cpp b/app/api/src/optimization_job_runtime.cpp index c1d831f4f..7a4bbedb2 100644 --- a/app/api/src/optimization_job_runtime.cpp +++ b/app/api/src/optimization_job_runtime.cpp @@ -25,7 +25,8 @@ OptimizationJobRuntime::OptimizationJobRuntime(std::shared_ptrIsConfigured()) { schema_ready_ = store_->EnsureSchema(&schema_status_detail_); } @@ -167,13 +168,12 @@ void OptimizationJobRuntime::WorkerLoop(const std::stop_token stop_token, runner_->Run(BuildWeatherAdjustedVroomInput(parsed_request->input, impact))); } if (final_result.output.has_value()) { - const TrafficForecastOptions traffic_options = ResolveTrafficForecastOptionsFromEnv(); const TrafficDelayEstimate traffic_delay = ReadRouteTraffic( - traffic_options, *final_result.output, ReadRouteStartTime(parsed_request->input)); + traffic_options_, *final_result.output, ReadRouteStartTime(parsed_request->input)); const TrafficImpact traffic = EstimateTrafficImpact( - traffic_options, ReadVroomDuration(*final_result.output).value_or(0), + traffic_options_, ReadVroomDuration(*final_result.output).value_or(0), traffic_delay.delay_seconds, traffic_delay.source); - AddTrafficForecast(*forecast, traffic_options, traffic); + AddTrafficForecast(*forecast, traffic_options_, traffic); if (traffic.should_reoptimize) { final_result = ToCoordinatedSolveResult(runner_->Run( BuildTrafficAdjustedVroomInput(parsed_request->input, impact, traffic))); From b859fed52cd42c7c82be7c346ee5a777a44a2023 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 17:52:19 -0700 Subject: [PATCH 21/26] Fix traffic endpoint formatting --- .../endpoints/deliveries_optimize_endpoint.cpp | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 427afb543..232d474ff 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -196,12 +196,11 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, forecast = BuildWeatherForecastAnnotation(sync_weather_options, impact); const auto finish_with_traffic = - [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, - impact, + [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, impact, respond_with_completion](CoordinatedSolveResult weather_result) { if (!weather_result.output.has_value()) { - respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( - *optimize_request_ptr, weather_result, forecast))); + respond_with_completion(BuildSolveExecutionResponse( + BuildSolveExecutionResult(*optimize_request_ptr, weather_result, forecast))); return; } @@ -211,9 +210,8 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, std::optional final_forecast = forecast; const Json::Value& route_output = *weather_result.output; - const TrafficDelayEstimate traffic_delay = - ReadRouteTraffic(traffic_options, route_output, - ReadRouteStartTime(*optimize_request_ptr)); + const TrafficDelayEstimate traffic_delay = ReadRouteTraffic( + traffic_options, route_output, ReadRouteStartTime(*optimize_request_ptr)); const TrafficImpact traffic = EstimateTrafficImpact( traffic_options, ReadVroomDuration(route_output).value_or(0), traffic_delay.delay_seconds, traffic_delay.source); @@ -228,8 +226,7 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, return BuildTrafficAdjustedVroomInput(*optimize_request_ptr, impact, traffic); }, - [optimize_request_ptr, final_forecast, - respond_with_completion]( + [optimize_request_ptr, final_forecast, respond_with_completion]( const CoordinatedSolveResult& traffic_result) mutable { respond_with_completion(BuildSolveExecutionResponse( BuildSolveExecutionResult(*optimize_request_ptr, traffic_result, From 84714f565f8a3a1b4cfb042aca9c2fea97c1366e Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 18:09:17 -0700 Subject: [PATCH 22/26] added explanation comments for ReadLegDeparture and #undef GetJob --- app/api/src/endpoints/optimization_jobs_endpoint.cpp | 1 + app/api/src/forecast_optimizer.cpp | 1 + app/api/src/optimization_job_store.cpp | 1 + 3 files changed, 3 insertions(+) diff --git a/app/api/src/endpoints/optimization_jobs_endpoint.cpp b/app/api/src/endpoints/optimization_jobs_endpoint.cpp index d50c33d9a..ac1e89e2b 100644 --- a/app/api/src/endpoints/optimization_jobs_endpoint.cpp +++ b/app/api/src/endpoints/optimization_jobs_endpoint.cpp @@ -8,6 +8,7 @@ #include +// Windows SDK defines GetJob as a macro so it is undef before including jsoncpp. #ifdef GetJob #undef GetJob #endif diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 38aa06e0a..b71ff9d79 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -185,6 +185,7 @@ ReadLegDeparture(const Json::Value& step, if (route_start_time.has_value()) { const std::chrono::seconds route_start_seconds = std::chrono::duration_cast(route_start_time->time_since_epoch()); + // Treat large arrivals near the route start as Unix timestamps and smaller ones are route offsets. if (offset >= route_start_seconds - std::chrono::hours{24}) { return std::chrono::sys_seconds{offset}; } diff --git a/app/api/src/optimization_job_store.cpp b/app/api/src/optimization_job_store.cpp index 8198c2632..274b7c5b2 100644 --- a/app/api/src/optimization_job_store.cpp +++ b/app/api/src/optimization_job_store.cpp @@ -7,6 +7,7 @@ #include #include +// Windows SDK defines GetJob as a macro so it is undef before including jsoncpp. #ifdef GetJob #undef GetJob #endif From d3bd6a17e98487c7ea1b0a402aa98855c4c50dbb Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 18:20:53 -0700 Subject: [PATCH 23/26] Cover relative traffic leg timing --- .../deliveries_optimize_endpoint.cpp | 11 ++--- app/api/src/forecast_optimizer.cpp | 2 +- .../weather_forecast_optimizer_test.cpp | 41 +++++++++++++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 232d474ff..bdd622e1a 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -199,8 +199,9 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, impact, respond_with_completion](CoordinatedSolveResult weather_result) { if (!weather_result.output.has_value()) { - respond_with_completion(BuildSolveExecutionResponse( - BuildSolveExecutionResult(*optimize_request_ptr, weather_result, forecast))); + const SolveExecutionResult response_result = + BuildSolveExecutionResult(*optimize_request_ptr, weather_result, forecast); + respond_with_completion(BuildSolveExecutionResponse(response_result)); return; } @@ -228,9 +229,9 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, }, [optimize_request_ptr, final_forecast, respond_with_completion]( const CoordinatedSolveResult& traffic_result) mutable { - respond_with_completion(BuildSolveExecutionResponse( - BuildSolveExecutionResult(*optimize_request_ptr, traffic_result, - final_forecast))); + const SolveExecutionResult response_result = BuildSolveExecutionResult( + *optimize_request_ptr, traffic_result, final_forecast); + respond_with_completion(BuildSolveExecutionResponse(response_result)); }); if (traffic_rerun_status != SolveAdmissionStatus::kAccepted) { respond_with_completion( diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index b71ff9d79..7ae6d8c9b 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -185,7 +185,7 @@ ReadLegDeparture(const Json::Value& step, if (route_start_time.has_value()) { const std::chrono::seconds route_start_seconds = std::chrono::duration_cast(route_start_time->time_since_epoch()); - // Treat large arrivals near the route start as Unix timestamps and smaller ones are route offsets. + // Large arrivals are Unix timestamps; smaller arrivals are route offsets. if (offset >= route_start_seconds - std::chrono::hours{24}) { return std::chrono::sys_seconds{offset}; } diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index a9eb8968b..17e22502f 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -416,3 +416,44 @@ TEST(TrafficForecastOptimizerTest, ReadsTrafficLegsFromVroomSteps) { EXPECT_DOUBLE_EQ(legs[0].origin.lon, -121.7405); EXPECT_DOUBLE_EQ(legs[0].destination.lon, -121.752); } +TEST(TrafficForecastOptimizerTest, ReadsRelativeTrafficLegDepartures) { + constexpr int kRouteStart = 1767225600; + + Json::Value output{Json::objectValue}; + output["routes"] = Json::Value{Json::arrayValue}; + + Json::Value route{Json::objectValue}; + route["steps"] = Json::Value{Json::arrayValue}; + + Json::Value start{Json::objectValue}; + start["arrival"] = 0; + start["location"] = Json::Value{Json::arrayValue}; + start["location"].append(-121.7405); + start["location"].append(38.5449); + + Json::Value stop{Json::objectValue}; + stop["arrival"] = 600; + stop["service"] = 120; + stop["location"] = Json::Value{Json::arrayValue}; + stop["location"].append(-121.752); + stop["location"].append(38.548); + + Json::Value end{Json::objectValue}; + end["arrival"] = 1200; + end["location"] = Json::Value{Json::arrayValue}; + end["location"].append(-121.7405); + end["location"].append(38.5449); + + route["steps"].append(start); + route["steps"].append(stop); + route["steps"].append(end); + output["routes"].append(route); + + const std::vector legs = + deliveryoptimizer::api::ReadTrafficLegs( + output, std::chrono::sys_seconds{std::chrono::seconds{kRouteStart}}); + + ASSERT_EQ(legs.size(), 2U); + EXPECT_EQ(legs[0].departure_time.time_since_epoch(), std::chrono::seconds{kRouteStart}); + EXPECT_EQ(legs[1].departure_time.time_since_epoch(), std::chrono::seconds{kRouteStart + 720}); +} From 146ba6e76a829c57534f37647abf07a21c654d38 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 19:47:22 -0700 Subject: [PATCH 24/26] fix: pr check errors --- .../deliveries_optimize_endpoint.cpp | 124 ++++++++++-------- 1 file changed, 71 insertions(+), 53 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index bdd622e1a..500e21983 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -8,6 +8,7 @@ #include "deliveryoptimizer/api/vroom_runner.hpp" #include +#include #include #include #include @@ -95,6 +96,68 @@ void DispatchResponse( response_loop->queueInLoop([callback, response] { (*callback)(response); }); } +void FinishWithTraffic(const std::shared_ptr& coordinator, + std::shared_ptr optimize_request, + const deliveryoptimizer::api::SolveRequestSize request_size, + deliveryoptimizer::api::TrafficForecastOptions traffic_options, + std::optional forecast, + const deliveryoptimizer::api::WeatherImpactEstimate weather_impact, + std::function respond_with_completion, + deliveryoptimizer::api::CoordinatedSolveResult weather_result) { + if (!weather_result.output.has_value()) { + const deliveryoptimizer::api::SolveExecutionResult response_result = + deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, weather_result, + forecast); + respond_with_completion(BuildSolveExecutionResponse(response_result)); + return; + } + + std::thread([coordinator, optimize_request = std::move(optimize_request), request_size, + traffic_options = std::move(traffic_options), forecast = std::move(forecast), + weather_impact, respond_with_completion = std::move(respond_with_completion), + weather_result = std::move(weather_result)]() mutable { + std::optional final_forecast = forecast; + const Json::Value& route_output = *weather_result.output; + + const deliveryoptimizer::api::TrafficDelayEstimate traffic_delay = + deliveryoptimizer::api::ReadRouteTraffic( + traffic_options, route_output, + deliveryoptimizer::api::ReadRouteStartTime(*optimize_request)); + const deliveryoptimizer::api::TrafficImpact traffic = + deliveryoptimizer::api::EstimateTrafficImpact( + traffic_options, deliveryoptimizer::api::ReadVroomDuration(route_output).value_or(0), + traffic_delay.delay_seconds, traffic_delay.source); + if (final_forecast.has_value()) { + deliveryoptimizer::api::AddTrafficForecast(*final_forecast, traffic_options, traffic); + } + + if (traffic.should_reoptimize) { + const deliveryoptimizer::api::SolveAdmissionStatus traffic_rerun_status = coordinator->Submit( + request_size, + [optimize_request, weather_impact, traffic] { + return deliveryoptimizer::api::BuildTrafficAdjustedVroomInput(*optimize_request, + weather_impact, traffic); + }, + [optimize_request, final_forecast, respond_with_completion]( + const deliveryoptimizer::api::CoordinatedSolveResult& traffic_result) mutable { + const deliveryoptimizer::api::SolveExecutionResult response_result = + deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, + traffic_result, final_forecast); + respond_with_completion(BuildSolveExecutionResponse(response_result)); + }); + if (traffic_rerun_status != deliveryoptimizer::api::SolveAdmissionStatus::kAccepted) { + respond_with_completion(BuildAdmissionRejectionResponse(traffic_rerun_status)); + } + return; + } + + const deliveryoptimizer::api::SolveExecutionResult response_result = + deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, weather_result, + final_forecast); + respond_with_completion(BuildSolveExecutionResponse(response_result)); + }).detach(); +} + } // namespace namespace deliveryoptimizer::api { @@ -195,58 +258,9 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, sync_weather_options, *optimize_request_ptr, *result.output); forecast = BuildWeatherForecastAnnotation(sync_weather_options, impact); - const auto finish_with_traffic = - [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, impact, - respond_with_completion](CoordinatedSolveResult weather_result) { - if (!weather_result.output.has_value()) { - const SolveExecutionResult response_result = - BuildSolveExecutionResult(*optimize_request_ptr, weather_result, forecast); - respond_with_completion(BuildSolveExecutionResponse(response_result)); - return; - } - - std::thread([coordinator, optimize_request_ptr, request_size, traffic_options, - forecast, impact, respond_with_completion, - weather_result = std::move(weather_result)]() mutable { - std::optional final_forecast = forecast; - const Json::Value& route_output = *weather_result.output; - - const TrafficDelayEstimate traffic_delay = ReadRouteTraffic( - traffic_options, route_output, ReadRouteStartTime(*optimize_request_ptr)); - const TrafficImpact traffic = EstimateTrafficImpact( - traffic_options, ReadVroomDuration(route_output).value_or(0), - traffic_delay.delay_seconds, traffic_delay.source); - if (final_forecast.has_value()) { - AddTrafficForecast(*final_forecast, traffic_options, traffic); - } - - if (traffic.should_reoptimize) { - const SolveAdmissionStatus traffic_rerun_status = coordinator->Submit( - request_size, - [optimize_request_ptr, impact, traffic] { - return BuildTrafficAdjustedVroomInput(*optimize_request_ptr, impact, - traffic); - }, - [optimize_request_ptr, final_forecast, respond_with_completion]( - const CoordinatedSolveResult& traffic_result) mutable { - const SolveExecutionResult response_result = BuildSolveExecutionResult( - *optimize_request_ptr, traffic_result, final_forecast); - respond_with_completion(BuildSolveExecutionResponse(response_result)); - }); - if (traffic_rerun_status != SolveAdmissionStatus::kAccepted) { - respond_with_completion( - BuildAdmissionRejectionResponse(traffic_rerun_status)); - } - return; - } - - respond_with_completion(BuildSolveExecutionResponse(BuildSolveExecutionResult( - *optimize_request_ptr, weather_result, final_forecast))); - }).detach(); - }; - if (!impact.should_reoptimize) { - finish_with_traffic(result); + FinishWithTraffic(coordinator, optimize_request_ptr, request_size, traffic_options, + forecast, impact, respond_with_completion, result); return; } @@ -255,8 +269,12 @@ void RegisterDeliveriesOptimizeEndpoint(drogon::HttpAppFramework& app, [optimize_request_ptr, impact] { return BuildWeatherAdjustedVroomInput(*optimize_request_ptr, impact); }, - [finish_with_traffic](const CoordinatedSolveResult& rerun_result) mutable { - finish_with_traffic(rerun_result); + [coordinator, optimize_request_ptr, request_size, traffic_options, forecast, + impact, respond_with_completion]( + const CoordinatedSolveResult& rerun_result) mutable { + FinishWithTraffic(coordinator, optimize_request_ptr, request_size, + traffic_options, forecast, impact, respond_with_completion, + rerun_result); }); if (rerun_status != SolveAdmissionStatus::kAccepted) { respond_with_completion(BuildAdmissionRejectionResponse(rerun_status)); From bcfdcdf7d89fef7d9880aea7bb5def330e324c80 Mon Sep 17 00:00:00 2001 From: Hiral Arora Date: Mon, 25 May 2026 20:11:15 -0700 Subject: [PATCH 25/26] Fix traffic endpoint clang formatting --- .../deliveries_optimize_endpoint.cpp | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 500e21983..7b26ea82e 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -96,14 +96,15 @@ void DispatchResponse( response_loop->queueInLoop([callback, response] { (*callback)(response); }); } -void FinishWithTraffic(const std::shared_ptr& coordinator, - std::shared_ptr optimize_request, - const deliveryoptimizer::api::SolveRequestSize request_size, - deliveryoptimizer::api::TrafficForecastOptions traffic_options, - std::optional forecast, - const deliveryoptimizer::api::WeatherImpactEstimate weather_impact, - std::function respond_with_completion, - deliveryoptimizer::api::CoordinatedSolveResult weather_result) { +void FinishWithTraffic( + const std::shared_ptr& coordinator, + std::shared_ptr optimize_request, + const deliveryoptimizer::api::SolveRequestSize request_size, + deliveryoptimizer::api::TrafficForecastOptions traffic_options, + std::optional forecast, + const deliveryoptimizer::api::WeatherImpactEstimate weather_impact, + std::function respond_with_completion, + deliveryoptimizer::api::CoordinatedSolveResult weather_result) { if (!weather_result.output.has_value()) { const deliveryoptimizer::api::SolveExecutionResult response_result = deliveryoptimizer::api::BuildSolveExecutionResult(*optimize_request, weather_result, @@ -141,8 +142,8 @@ void FinishWithTraffic(const std::shared_ptr Date: Mon, 25 May 2026 20:36:15 -0700 Subject: [PATCH 26/26] Split traffic delay by route leg timing --- .../api/forecast_optimizer.hpp | 3 +- .../deliveries_optimize_endpoint.cpp | 7 +- app/api/src/forecast_optimizer.cpp | 105 +++++++++++++++++- app/api/src/optimization_job_runtime.cpp | 4 +- .../weather_forecast_optimizer_test.cpp | 40 ++++++- 5 files changed, 146 insertions(+), 13 deletions(-) diff --git a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp index 3da68d390..6ea836932 100644 --- a/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp +++ b/app/api/include/deliveryoptimizer/api/forecast_optimizer.hpp @@ -132,7 +132,8 @@ ReadRouteStartTime(const OptimizeRequestInput& input); [[nodiscard]] Json::Value BuildTrafficAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& weather, - const TrafficImpact& traffic); + const TrafficImpact& traffic, + const Json::Value& vroom_output); [[nodiscard]] Json::Value BuildWeatherForecastAnnotation(const WeatherForecastOptions& options, const WeatherImpactEstimate& impact); diff --git a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp index 7b26ea82e..c15cb8390 100644 --- a/app/api/src/endpoints/deliveries_optimize_endpoint.cpp +++ b/app/api/src/endpoints/deliveries_optimize_endpoint.cpp @@ -133,11 +133,12 @@ void FinishWithTraffic( } if (traffic.should_reoptimize) { + Json::Value route_output_copy = route_output; const deliveryoptimizer::api::SolveAdmissionStatus traffic_rerun_status = coordinator->Submit( request_size, - [optimize_request, weather_impact, traffic] { - return deliveryoptimizer::api::BuildTrafficAdjustedVroomInput(*optimize_request, - weather_impact, traffic); + [optimize_request, weather_impact, traffic, route_output_copy] { + return deliveryoptimizer::api::BuildTrafficAdjustedVroomInput( + *optimize_request, weather_impact, traffic, route_output_copy); }, [optimize_request, final_forecast, respond_with_completion]( const deliveryoptimizer::api::CoordinatedSolveResult& traffic_result) mutable { diff --git a/app/api/src/forecast_optimizer.cpp b/app/api/src/forecast_optimizer.cpp index 7ae6d8c9b..ba80b2a1d 100644 --- a/app/api/src/forecast_optimizer.cpp +++ b/app/api/src/forecast_optimizer.cpp @@ -14,11 +14,13 @@ #include #include #include +#include #include #include #include #include #include +#include namespace { @@ -197,6 +199,100 @@ ReadLegDeparture(const Json::Value& step, offset; } +[[nodiscard]] std::vector BuildEvenTrafficDelays(const std::size_t job_count, + const int total_delay_seconds) { + std::vector delays(job_count, 0); + if (job_count == 0U || total_delay_seconds <= 0) { + return delays; + } + + const int delay_per_stop = static_cast( + std::ceil(static_cast(total_delay_seconds) / static_cast(job_count))); + std::fill(delays.begin(), delays.end(), delay_per_stop); + return delays; +} + +[[nodiscard]] std::vector ReadJobTravelSeconds(const Json::Value& vroom_output, + const std::size_t job_count) { + std::vector travel_seconds(job_count, 0); + const Json::Value& routes = vroom_output["routes"]; + if (!routes.isArray()) { + return travel_seconds; + } + + for (const Json::Value& route : routes) { + const Json::Value& steps = route["steps"]; + if (!steps.isArray() || steps.size() < 2U) { + continue; + } + + for (Json::ArrayIndex index = 1U; index < steps.size(); ++index) { + const Json::Value& from = steps[index - 1U]; + const Json::Value& to = steps[index]; + if (to["type"].isString() && to["type"].asString() != "job") { + continue; + } + if (!to["id"].isUInt64()) { + continue; + } + + const std::uint64_t raw_job_id = to["id"].asUInt64(); + if (raw_job_id == 0U || raw_job_id > job_count) { + continue; + } + + const int from_arrival = from["arrival"].isInt() ? from["arrival"].asInt() : 0; + const int from_service = from["service"].isInt() ? from["service"].asInt() : 0; + const int to_arrival = to["arrival"].isInt() ? to["arrival"].asInt() : from_arrival; + const int leg_seconds = std::max(to_arrival - from_arrival - from_service, 0); + travel_seconds[static_cast(raw_job_id - 1U)] += leg_seconds; + } + } + + return travel_seconds; +} + +[[nodiscard]] std::vector BuildWeightedTrafficDelays(const Json::Value& vroom_output, + const std::size_t job_count, + const int total_delay_seconds) { + if (job_count == 0U || total_delay_seconds <= 0) { + return std::vector(job_count, 0); + } + + const std::vector travel_seconds = ReadJobTravelSeconds(vroom_output, job_count); + const int total_travel_seconds = std::accumulate(travel_seconds.begin(), travel_seconds.end(), 0); + if (total_travel_seconds <= 0) { + return BuildEvenTrafficDelays(job_count, total_delay_seconds); + } + + std::vector delays(job_count, 0); + int assigned_delay = 0; + std::vector> remainders; + remainders.reserve(job_count); + for (std::size_t index = 0U; index < job_count; ++index) { + const double raw_delay = static_cast(total_delay_seconds) * + static_cast(travel_seconds[index]) / + static_cast(total_travel_seconds); + delays[index] = static_cast(std::floor(raw_delay)); + assigned_delay += delays[index]; + remainders.emplace_back(raw_delay - static_cast(delays[index]), index); + } + + std::sort(remainders.begin(), remainders.end(), + [](const auto& left, const auto& right) { return left.first > right.first; }); + int remaining_delay = total_delay_seconds - assigned_delay; + for (const auto& remainder : remainders) { + if (remaining_delay <= 0) { + break; + } + const std::size_t index = remainder.second; + ++delays[index]; + --remaining_delay; + } + + return delays; +} + } // namespace namespace deliveryoptimizer::api { @@ -648,18 +744,19 @@ Json::Value BuildWeatherAdjustedVroomInput(const OptimizeRequestInput& input, Json::Value BuildTrafficAdjustedVroomInput(const OptimizeRequestInput& input, const WeatherImpactEstimate& weather, - const TrafficImpact& traffic) { + const TrafficImpact& traffic, + const Json::Value& vroom_output) { Json::Value payload = BuildWeatherAdjustedVroomInput(input, weather); if (!traffic.should_reoptimize || input.jobs.empty()) { return payload; } - const int delay_per_stop = static_cast(std::ceil( - static_cast(traffic.traffic_delay_seconds) / static_cast(input.jobs.size()))); + const std::vector traffic_delays = + BuildWeightedTrafficDelays(vroom_output, input.jobs.size(), traffic.traffic_delay_seconds); for (Json::ArrayIndex index = 0; index < payload["jobs"].size(); ++index) { Json::Value& job = payload["jobs"][index]; const int current_service = job["service"].isInt() ? job["service"].asInt() : 0; - job["service"] = current_service + delay_per_stop; + job["service"] = current_service + traffic_delays[static_cast(index)]; } return payload; diff --git a/app/api/src/optimization_job_runtime.cpp b/app/api/src/optimization_job_runtime.cpp index 7a4bbedb2..71b854f83 100644 --- a/app/api/src/optimization_job_runtime.cpp +++ b/app/api/src/optimization_job_runtime.cpp @@ -175,8 +175,8 @@ void OptimizationJobRuntime::WorkerLoop(const std::stop_token stop_token, traffic_delay.delay_seconds, traffic_delay.source); AddTrafficForecast(*forecast, traffic_options_, traffic); if (traffic.should_reoptimize) { - final_result = ToCoordinatedSolveResult(runner_->Run( - BuildTrafficAdjustedVroomInput(parsed_request->input, impact, traffic))); + final_result = ToCoordinatedSolveResult(runner_->Run(BuildTrafficAdjustedVroomInput( + parsed_request->input, impact, traffic, *final_result.output))); } } } diff --git a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp index 17e22502f..dc9cd457d 100644 --- a/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp +++ b/tests/api/forecast_optimizer/weather_forecast_optimizer_test.cpp @@ -292,6 +292,40 @@ TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficReoptimizes) { } TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficAddsServiceTime) { const auto input = BuildInput(); + Json::Value output{Json::objectValue}; + output["routes"] = Json::Value{Json::arrayValue}; + Json::Value route{Json::objectValue}; + route["steps"] = Json::Value{Json::arrayValue}; + + Json::Value start{Json::objectValue}; + start["arrival"] = 0; + start["location"] = Json::Value{Json::arrayValue}; + start["location"].append(-121.7405); + start["location"].append(38.5449); + + Json::Value first_stop{Json::objectValue}; + first_stop["type"] = "job"; + first_stop["id"] = 1; + first_stop["arrival"] = 300; + first_stop["service"] = 180; + first_stop["location"] = Json::Value{Json::arrayValue}; + first_stop["location"].append(-121.748); + first_stop["location"].append(38.545); + + Json::Value second_stop{Json::objectValue}; + second_stop["type"] = "job"; + second_stop["id"] = 2; + second_stop["arrival"] = 1080; + second_stop["service"] = 120; + second_stop["location"] = Json::Value{Json::arrayValue}; + second_stop["location"].append(-121.752); + second_stop["location"].append(38.548); + + route["steps"].append(start); + route["steps"].append(first_stop); + route["steps"].append(second_stop); + output["routes"].append(route); + const deliveryoptimizer::api::WeatherImpactEstimate weather{}; const deliveryoptimizer::api::TrafficImpact traffic{ .baseline_duration_seconds = 900, @@ -303,12 +337,12 @@ TEST(TrafficForecastOptimizerTest, AboveThresholdTrafficAddsServiceTime) { }; const Json::Value payload = - deliveryoptimizer::api::BuildTrafficAdjustedVroomInput(input, weather, traffic); + deliveryoptimizer::api::BuildTrafficAdjustedVroomInput(input, weather, traffic, output); ASSERT_TRUE(payload["jobs"].isArray()); ASSERT_EQ(payload["jobs"].size(), 2U); - EXPECT_EQ(payload["jobs"][0]["service"].asInt(), 270); - EXPECT_EQ(payload["jobs"][1]["service"].asInt(), 210); + EXPECT_EQ(payload["jobs"][0]["service"].asInt(), 240); + EXPECT_EQ(payload["jobs"][1]["service"].asInt(), 240); } TEST(TrafficForecastOptimizerTest, BuildsGoogleTrafficPath) { const std::string path = deliveryoptimizer::api::BuildTrafficPath(