diff --git a/README.md b/README.md index 959ddeb..68c6eef 100644 --- a/README.md +++ b/README.md @@ -1473,6 +1473,20 @@ routing.solve( ) ``` +Set a native time limit for route restoration after closing the model. The limit +is in seconds and accepts fractional values: + +```ruby +routing.close_model +routing.update_time_limit(0.5) +assignment = routing.read_assignment_from_routes(routes, true) +``` + +The limit is checked cooperatively, so the call can exceed it. Restoration can +return a valid assignment found before the limit. A `nil` result does not +distinguish an invalid route from a limit reached before finding a solution. +A subsequent solve uses the time limit in its own search parameters. + Native improvement-rate stopping is available through search parameters: ```ruby diff --git a/ext/or-tools/routing.cpp b/ext/or-tools/routing.cpp index 6539bd2..011ccc0 100644 --- a/ext/or-tools/routing.cpp +++ b/ext/or-tools/routing.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -714,6 +715,14 @@ void init_routing(Rice::Module& m) { self.CloseModel(); } }) + .define_method( + "update_time_limit", + [](RoutingModel& self, double seconds) { + if (!std::isfinite(seconds) || seconds < 0) { + throw std::invalid_argument{"time limit must be finite and nonnegative"}; + } + self.UpdateTimeLimit(absl::Seconds(seconds)); + }) .define_method( "_enable_solution_trace", [](RoutingModel& self, int64_t max_samples, int64_t sample_interval_ms) { diff --git a/test/routing_time_limit_test.rb b/test/routing_time_limit_test.rb new file mode 100644 index 0000000..0f71d94 --- /dev/null +++ b/test/routing_time_limit_test.rb @@ -0,0 +1,55 @@ +require_relative "test_helper" + +class RoutingTimeLimitTest < Minitest::Test + def test_restoration_limit_can_be_reset_after_timeout + routing = build_routing + routing.close_model + routing.update_time_limit(0) + + assert_nil routing.read_assignment_from_routes([[1, 2]], true) + assert_equal :fail, routing.status + + routing.update_time_limit(0.5) + assignment = routing.read_assignment_from_routes([[1, 2]], true) + + refute_nil assignment + assert_equal 4, assignment.objective_value + assert_equal :success, routing.status + end + + def test_solve_replaces_restoration_time_limit + routing = build_routing + routing.close_model + routing.update_time_limit(0.5) + assignment = routing.read_assignment_from_routes([[1, 2]], true) + refute_nil assignment + + routing.update_time_limit(0) + parameters = ORTools.default_routing_search_parameters + parameters.time_limit = 1 + parameters.solution_limit = 1 + solution = routing.solve_from_assignment_with_parameters(assignment, parameters) + + refute_nil solution + assert_equal 4, solution.objective_value + end + + def test_rejects_invalid_time_limits + routing = build_routing + + [-1, Float::INFINITY, -Float::INFINITY, Float::NAN].each do |seconds| + error = assert_raises(ArgumentError) { routing.update_time_limit(seconds) } + assert_equal "time limit must be finite and nonnegative", error.message + end + end + + private + + def build_routing + manager = ORTools::RoutingIndexManager.new(3, 1, 0) + routing = ORTools::RoutingModel.new(manager) + transit = routing.register_transit_matrix([[0, 1, 2], [1, 0, 1], [2, 1, 0]]) + routing.set_arc_cost_evaluator_of_all_vehicles(transit) + routing + end +end