From 111f43b139f4788fe8f86fc3ec1add379157b8fb Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Wed, 3 Feb 2021 15:39:48 +0100 Subject: [PATCH] add deterministic time limit to the graph symmetry finder; improve support for scheduling constraints in regular LNS for CP-SAT; improve symmetry detection for CP-SAT --- ortools/algorithms/find_graph_symmetries.cc | 53 ++- ortools/algorithms/find_graph_symmetries.h | 17 +- ortools/sat/boolean_problem.cc | 5 +- ortools/sat/cp_model_lns.cc | 130 +++++- ortools/sat/cp_model_lns.h | 21 +- ortools/sat/cp_model_solver.cc | 19 +- ortools/sat/cp_model_symmetries.cc | 468 ++++++++++---------- ortools/sat/cp_model_symmetries.h | 4 +- ortools/sat/sat_parameters.proto | 7 +- 9 files changed, 448 insertions(+), 276 deletions(-) diff --git a/ortools/algorithms/find_graph_symmetries.cc b/ortools/algorithms/find_graph_symmetries.cc index ac1b520828..301e881b6a 100644 --- a/ortools/algorithms/find_graph_symmetries.cc +++ b/ortools/algorithms/find_graph_symmetries.cc @@ -105,7 +105,7 @@ GraphSymmetryFinder::GraphSymmetryFinder(const Graph& graph, bool is_undirected) tmp_degree_(NumNodes(), 0), tmp_nodes_with_degree_(NumNodes() + 1) { // Set up an "unlimited" time limit by default. - time_limit_ = TimeLimit::Infinite(); + time_limit_ = &dummy_time_limit_; tmp_partition_.Reset(NumNodes()); if (is_undirected) { DCHECK(GraphIsSymmetric(graph)); @@ -178,7 +178,9 @@ template inline void IncrementCounterForNonSingletons(const T& nodes, const DynamicPartition& partition, std::vector* node_count, - std::vector* nodes_seen) { + std::vector* nodes_seen, + int64* num_operations) { + *num_operations += nodes.end() - nodes.begin(); for (const int node : nodes) { if (partition.ElementsInSamePartAs(node).size() == 1) continue; const int count = ++(*node_count)[node]; @@ -192,6 +194,15 @@ void GraphSymmetryFinder::RecursivelyRefinePartitionByAdjacency( // Rename, for readability of the code below. std::vector& tmp_nodes_with_nonzero_degree = tmp_stack_; + // This function is the main bottleneck of the whole algorithm. We count the + // number of blocks in the inner-most loops in num_operations. At the end we + // will multiply it by a factor to have some deterministic time that we will + // append to the deterministic time counter. + // + // TODO(user): We are really imprecise in our counting, but it is fine. We + // just need a way to enforce a deterministic limit on the computation effort. + int64 num_operations = 0; + // Assuming that the partition was refined based on the adjacency on // parts [0 .. first_unrefined_part_index) already, we simply need to // refine parts first_unrefined_part_index ... NumParts()-1, the latter bound @@ -215,19 +226,20 @@ void GraphSymmetryFinder::RecursivelyRefinePartitionByAdjacency( // come from/to the current part. if (outgoing_adjacency) { for (const int node : partition->ElementsInPart(part_index)) { - IncrementCounterForNonSingletons(graph_[node], *partition, - &tmp_degree_, - &tmp_nodes_with_nonzero_degree); + IncrementCounterForNonSingletons( + graph_[node], *partition, &tmp_degree_, + &tmp_nodes_with_nonzero_degree, &num_operations); } } else { for (const int node : partition->ElementsInPart(part_index)) { - IncrementCounterForNonSingletons(TailsOfIncomingArcsTo(node), - *partition, &tmp_degree_, - &tmp_nodes_with_nonzero_degree); + IncrementCounterForNonSingletons( + TailsOfIncomingArcsTo(node), *partition, &tmp_degree_, + &tmp_nodes_with_nonzero_degree, &num_operations); } } // Group the nodes by (nonzero) degree. Remember the maximum degree. int max_degree = 0; + num_operations += 3 + tmp_nodes_with_nonzero_degree.size(); for (const int node : tmp_nodes_with_nonzero_degree) { const int degree = tmp_degree_[node]; tmp_degree_[node] = 0; // To clean up after us. @@ -238,11 +250,20 @@ void GraphSymmetryFinder::RecursivelyRefinePartitionByAdjacency( // For each degree, refine the partition by the set of nodes with that // degree. for (int degree = 1; degree <= max_degree; ++degree) { + // We use a manually tuned factor 3 because Refine() does quite a bit of + // operations for each node in its argument. + num_operations += 1 + 3 * tmp_nodes_with_degree_[degree].size(); partition->Refine(tmp_nodes_with_degree_[degree]); tmp_nodes_with_degree_[degree].clear(); // To clean up after us. } } } + + // The coefficient was manually tuned (only on a few instances) so that the + // time is roughly correlated with seconds on a fast desktop computer from + // 2020. + time_limit_->AdvanceDeterministicTime(1e-8 * + static_cast(num_operations)); } void GraphSymmetryFinder::DistinguishNodeInPartition( @@ -353,11 +374,12 @@ void GetAllOtherRepresentativesInSamePartAs( } // namespace absl::Status GraphSymmetryFinder::FindSymmetries( - double time_limit_seconds, std::vector* node_equivalence_classes_io, + std::vector* node_equivalence_classes_io, std::vector>* generators, - std::vector* factorized_automorphism_group_size) { + std::vector* factorized_automorphism_group_size, + TimeLimit* time_limit) { // Initialization. - time_limit_ = absl::make_unique(time_limit_seconds); + time_limit_ = time_limit == nullptr ? &dummy_time_limit_ : time_limit; IF_STATS_ENABLED(stats_.initialization_time.StartTimer()); generators->clear(); factorized_automorphism_group_size->clear(); @@ -744,8 +766,10 @@ GraphSymmetryFinder::FindOneSuitablePermutation( tmp_dynamic_permutation_.UndoLastMappings(&base_singletons); } else { ScopedTimeDistributionUpdater u(&stats_.map_reelection_time); - // TODO(user): try to get the non-singleton part from - // DynamicPermutation in O(1), if it matters to the overall speed. + // TODO(user, viger): try to get the non-singleton part from + // DynamicPermutation in O(1). On some graphs like the symmetry of the + // mip problem lectsched-4-obj.mps.gz, this take the majority of the + // time! int non_singleton_part = 0; { ScopedTimeDistributionUpdater u(&stats_.non_singleton_search_time); @@ -754,6 +778,9 @@ GraphSymmetryFinder::FindOneSuitablePermutation( DCHECK_LT(non_singleton_part, base_partition->NumParts()); } } + time_limit_->AdvanceDeterministicTime( + 1e-9 * static_cast(non_singleton_part)); + // The partitions are compatible, but we'll deepen the search on some // non-singleton part. We can pick any base and image node in this case. GetBestMapping(*base_partition, *image_partition, non_singleton_part, diff --git a/ortools/algorithms/find_graph_symmetries.h b/ortools/algorithms/find_graph_symmetries.h index 3006a4d953..a1fc12aa1e 100644 --- a/ortools/algorithms/find_graph_symmetries.h +++ b/ortools/algorithms/find_graph_symmetries.h @@ -92,8 +92,9 @@ class GraphSymmetryFinder { // large as N!). // // DEADLINE AND PARTIAL COMPLETION: - // If the deadline passed as argument is reached, this method will return - // quickly (within a few milliseconds). The outputs may be partially filled: + // If the deadline passed as argument (via TimeLimit) is reached, this method + // will return quickly (within a few milliseconds of the limit). The outputs + // may be partially filled: // - Each element of "generators", if non-empty, will be a valid permutation. // - "node_equivalence_classes_io" will contain the equivalence classes // corresponding to the orbits under all the generators in "generators". @@ -101,9 +102,10 @@ class GraphSymmetryFinder { // partially valid: its last element may be undervalued. But all prior // elements are valid factors of the automorphism group size. absl::Status FindSymmetries( - double time_limit_seconds, std::vector* node_equivalence_classes_io, + std::vector* node_equivalence_classes_io, std::vector >* generators, - std::vector* factorized_automorphism_group_size); + std::vector* factorized_automorphism_group_size, + TimeLimit* time_limit = nullptr); // Fully refine the partition of nodes, using the graph as symmetry breaker. // This means applying the following steps on each part P of the partition: @@ -148,8 +150,11 @@ class GraphSymmetryFinder { util::BeginEndWrapper::const_iterator> TailsOfIncomingArcsTo( int node) const; - // Deadline management. Populated upon FindSymmetries(). - mutable std::unique_ptr time_limit_; + // Deadline management. Populated upon FindSymmetries(). If the passed + // time limit is nullptr, time_limit_ will point to dummy_time_limit_ which + // is an object with infinite limits by default. + TimeLimit dummy_time_limit_; + TimeLimit* time_limit_; // Internal search code used in FindSymmetries(), split out for readability: // find one permutation (if it exists) that maps root_node to root_image_node diff --git a/ortools/sat/boolean_problem.cc b/ortools/sat/boolean_problem.cc index a0d5d97c8c..48e6bfe850 100644 --- a/ortools/sat/boolean_problem.cc +++ b/ortools/sat/boolean_problem.cc @@ -705,9 +705,8 @@ void FindLinearBooleanProblemSymmetries( /*is_undirected=*/true); std::vector factorized_automorphism_group_size; // TODO(user): inject the appropriate time limit here. - CHECK_OK(symmetry_finder.FindSymmetries( - /*time_limit_seconds=*/std::numeric_limits::infinity(), - &equivalence_classes, generators, &factorized_automorphism_group_size)); + CHECK_OK(symmetry_finder.FindSymmetries(&equivalence_classes, generators, + &factorized_automorphism_group_size)); // Remove from the permutations the part not concerning the literals. // Note that some permutation may becomes empty, which means that we had diff --git a/ortools/sat/cp_model_lns.cc b/ortools/sat/cp_model_lns.cc index 6789a237c5..4807d1d422 100644 --- a/ortools/sat/cp_model_lns.cc +++ b/ortools/sat/cp_model_lns.cc @@ -111,14 +111,30 @@ void NeighborhoodGeneratorHelper::RecomputeHelperData() { // presolve? var_to_constraint_.assign(model_proto_.variables_size(), {}); constraint_to_var_.assign(model_proto_.constraints_size(), {}); + const auto register_var = [&](int var, int ct_index) { + DCHECK(RefIsPositive(var)); + if (IsConstant(var)) return; + var_to_constraint_[var].push_back(ct_index); + constraint_to_var_[ct_index].push_back(var); + CHECK_GE(var, 0); + CHECK_LT(var, model_proto_.variables_size()); + }; + for (int ct_index = 0; ct_index < model_proto_.constraints_size(); ++ct_index) { for (const int var : UsedVariables(model_proto_.constraints(ct_index))) { - if (IsConstant(var)) continue; - var_to_constraint_[var].push_back(ct_index); - constraint_to_var_[ct_index].push_back(var); - CHECK_GE(var, 0); - CHECK_LT(var, model_proto_.variables_size()); + register_var(var, ct_index); + } + + // We replace intervals by their underlying integer variables. + if (parameters_.lns_expand_intervals_in_constraint_graph()) { + for (const int interval : + UsedIntervals(model_proto_.constraints(ct_index))) { + for (const int var : + UsedVariables(model_proto_.constraints(interval))) { + register_var(var, ct_index); + } + } } } @@ -179,6 +195,37 @@ Neighborhood NeighborhoodGeneratorHelper::FullNeighborhood() const { return neighborhood; } +std::vector NeighborhoodGeneratorHelper::GetActiveIntervals( + const CpSolverResponse& initial_solution) const { + std::vector active_intervals; + for (const int i : TypeToConstraints(ConstraintProto::kInterval)) { + const ConstraintProto& interval_ct = ModelProto().constraints(i); + // We only look at intervals that are performed in the solution. The + // unperformed intervals should be automatically freed during the generation + // phase. + if (interval_ct.enforcement_literal().size() == 1) { + const int enforcement_ref = interval_ct.enforcement_literal(0); + const int enforcement_var = PositiveRef(enforcement_ref); + const int value = initial_solution.solution(enforcement_var); + if (RefIsPositive(enforcement_ref) == (value == 0)) { + continue; + } + } + + // We filter out fixed intervals. Because of presolve, if there is an + // enforcement literal, it cannot be fixed. + if (interval_ct.enforcement_literal().empty() && + IsConstant(PositiveRef(interval_ct.interval().start())) && + IsConstant(PositiveRef(interval_ct.interval().size())) && + IsConstant(PositiveRef(interval_ct.interval().end()))) { + continue; + } + + active_intervals.push_back(i); + } + return active_intervals; +} + Neighborhood NeighborhoodGeneratorHelper::FixGivenVariables( const CpSolverResponse& initial_solution, const std::vector& variables_to_fix) const { @@ -363,6 +410,47 @@ Neighborhood SimpleNeighborhoodGenerator::Generate( return helper_.FixGivenVariables(initial_solution, fixed_variables); } +Neighborhood SimpleConstraintNeighborhoodGenerator::Generate( + const CpSolverResponse& initial_solution, double difficulty, + absl::BitGenRef random) { + std::vector active_constraints; + for (int ct = 0; ct < helper_.ModelProto().constraints_size(); ++ct) { + if (helper_.ModelProto().constraints(ct).constraint_case() == + ConstraintProto::CONSTRAINT_NOT_SET) { + continue; + } + active_constraints.push_back(ct); + } + + const int num_active_vars = helper_.ActiveVariables().size(); + const int num_model_vars = helper_.ModelProto().variables_size(); + const int target_size = std::ceil(difficulty * num_active_vars); + const int num_constraints = helper_.ConstraintToVar().size(); + if (num_constraints == 0 || target_size == num_active_vars) { + return helper_.FullNeighborhood(); + } + CHECK_GT(target_size, 0); + + std::shuffle(active_constraints.begin(), active_constraints.end(), random); + + std::vector visited_variables_set(num_model_vars, false); + std::vector relaxed_variables; + + for (const int constraint_index : active_constraints) { + CHECK_LT(constraint_index, num_constraints); + for (const int var : helper_.ConstraintToVar()[constraint_index]) { + if (visited_variables_set[var]) continue; + visited_variables_set[var] = true; + if (helper_.IsActive(var)) { + relaxed_variables.push_back(var); + if (relaxed_variables.size() == target_size) break; + } + } + if (relaxed_variables.size() == target_size) break; + } + return helper_.RelaxGivenVariables(initial_solution, relaxed_variables); +} + Neighborhood VariableGraphNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, double difficulty, absl::BitGenRef random) { @@ -442,14 +530,14 @@ Neighborhood ConstraintGraphNeighborhoodGenerator::Generate( // Pick a random unprocessed constraint. const int i = absl::Uniform(random, 0, next_constraints.size()); - const int contraint_index = next_constraints[i]; + const int constraint_index = next_constraints[i]; std::swap(next_constraints[i], next_constraints.back()); next_constraints.pop_back(); // Add all the variable of this constraint and increase the set of next // possible constraints. - CHECK_LT(contraint_index, num_constraints); - random_variables = helper_.ConstraintToVar()[contraint_index]; + CHECK_LT(constraint_index, num_constraints); + random_variables = helper_.ConstraintToVar()[constraint_index]; std::shuffle(random_variables.begin(), random_variables.end(), random); for (const int var : random_variables) { if (visited_variables_set[var]) continue; @@ -494,20 +582,25 @@ Neighborhood GenerateSchedulingNeighborhoodForRelaxation( const int enforcement_var = PositiveRef(enforcement_ref); const int value = initial_solution.solution(enforcement_var); + // If the interval is not enforced, we just relax it. If it belongs to an + // exactly one constraint, and the enforced interval is not relaxed, then + // propagation will force this interval to stay not enforced. Otherwise, + // LNS will be able to change which interval will be enforced among all + // alternatives. + if (RefIsPositive(enforcement_ref) == (value == 0)) { + ignored_intervals.insert(i); + continue; + } + // Fix the value. neighborhood.cp_model.mutable_variables(enforcement_var)->clear_domain(); neighborhood.cp_model.mutable_variables(enforcement_var)->add_domain(value); neighborhood.cp_model.mutable_variables(enforcement_var)->add_domain(value); - - // If the interval is ignored, skip for the loop below as there is no - // point adding precedence on it. - if (RefIsPositive(enforcement_ref) == (value == 0)) { - ignored_intervals.insert(i); - } } for (const int c : helper.TypeToConstraints(ConstraintProto::kNoOverlap)) { - // Sort all non-relaxed intervals of this constraint by current start time. + // Sort all non-relaxed intervals of this constraint by current start + // time. std::vector> start_interval_pairs; for (const int i : neighborhood.cp_model.constraints(c).no_overlap().intervals()) { @@ -565,8 +658,8 @@ Neighborhood GenerateSchedulingNeighborhoodForRelaxation( Neighborhood SchedulingNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, double difficulty, absl::BitGenRef random) { - const auto span = helper_.TypeToConstraints(ConstraintProto::kInterval); - std::vector intervals_to_relax(span.begin(), span.end()); + std::vector intervals_to_relax = + helper_.GetActiveIntervals(initial_solution); GetRandomSubset(difficulty, &intervals_to_relax, random); return GenerateSchedulingNeighborhoodForRelaxation(intervals_to_relax, @@ -577,9 +670,8 @@ Neighborhood SchedulingTimeWindowNeighborhoodGenerator::Generate( const CpSolverResponse& initial_solution, double difficulty, absl::BitGenRef random) { std::vector> start_interval_pairs; - for (const int i : helper_.TypeToConstraints(ConstraintProto::kInterval)) { + for (const int i : helper_.GetActiveIntervals(initial_solution)) { const ConstraintProto& interval_ct = helper_.ModelProto().constraints(i); - const int start_var = interval_ct.interval().start(); const int64 start_value = initial_solution.solution(start_var); start_interval_pairs.push_back({start_value, i}); diff --git a/ortools/sat/cp_model_lns.h b/ortools/sat/cp_model_lns.h index 0da1423155..9aa4f7f685 100644 --- a/ortools/sat/cp_model_lns.h +++ b/ortools/sat/cp_model_lns.h @@ -121,6 +121,13 @@ class NeighborhoodGeneratorHelper : public SubSolver { return absl::MakeSpan(type_to_constraints_[type]); } + // Returns the list of indices of active interval constraints according + // to the initial_solution and the parameter lns_focus_on_performed_intervals. + // If true, this method returns the list of performed intervals in the + // solution. If false, it returns all intervals of the model. + std::vector GetActiveIntervals( + const CpSolverResponse& initial_solution) const; + // The initial problem. // Note that the domain of the variables are not updated here. const CpModelProto& ModelProto() const { return model_proto_; } @@ -348,6 +355,18 @@ class SimpleNeighborhoodGenerator : public NeighborhoodGenerator { double difficulty, absl::BitGenRef random) final; }; +// Pick a random subset of constraints and relax all the variables of these +// constraints. Note that to satisfy the difficulty, we might not relax all the +// variable of the "last" constraint. +class SimpleConstraintNeighborhoodGenerator : public NeighborhoodGenerator { + public: + explicit SimpleConstraintNeighborhoodGenerator( + NeighborhoodGeneratorHelper const* helper, const std::string& name) + : NeighborhoodGenerator(name, helper) {} + Neighborhood Generate(const CpSolverResponse& initial_solution, + double difficulty, absl::BitGenRef random) final; +}; + // Pick a random subset of variables that are constructed by a BFS in the // variable <-> constraint graph. That is, pick a random variable, then all the // variable connected by some constraint to the first one, and so on. The @@ -362,7 +381,7 @@ class VariableGraphNeighborhoodGenerator : public NeighborhoodGenerator { }; // Pick a random subset of constraint and relax all of their variables. We are a -// bit smarter than this because after the first contraint is selected, we only +// bit smarter than this because after the first constraint is selected, we only // select constraints that share at least one variable with the already selected // constraints. The variable from the "last" constraint are selected randomly. class ConstraintGraphNeighborhoodGenerator : public NeighborhoodGenerator { diff --git a/ortools/sat/cp_model_solver.cc b/ortools/sat/cp_model_solver.cc index a93666749f..d8d4f8dd87 100644 --- a/ortools/sat/cp_model_solver.cc +++ b/ortools/sat/cp_model_solver.cc @@ -1197,7 +1197,14 @@ void LoadBaseModel(const CpModelProto& model_proto, model_proto.search_strategy().empty()); mapping->CreateVariables(model_proto, view_all_booleans_as_integers, model); mapping->DetectOptionalVariables(model_proto, model); - mapping->DetectAndLoadBooleanSymmetries(model_proto, model); + + // TODO(user): The core algo and symmetries seems to be problematic in some + // cases. See for instance: neos-691058.mps.gz. This is probably because as + // we modify the model, our symmetry might be wrong? investigate. + if (!parameters.optimize_with_core()) { + mapping->DetectAndLoadBooleanSymmetries(model_proto, model); + } + mapping->ExtractEncoding(model_proto, model); mapping->PropagateEncodingFromEquivalenceRelations(model_proto, model); @@ -2760,15 +2767,19 @@ void SolveCpModelParallel(const CpModelProto& model_proto, // Each will have their own metrics. subsolvers.push_back(absl::make_unique( absl::make_unique( - helper, absl::StrCat("rnd_lns_", local_params.name())), + helper, absl::StrCat("rnd_var_lns_", local_params.name())), + local_params, helper, &shared)); + subsolvers.push_back(absl::make_unique( + absl::make_unique( + helper, absl::StrCat("rnd_cst_lns_", local_params.name())), local_params, helper, &shared)); subsolvers.push_back(absl::make_unique( absl::make_unique( - helper, absl::StrCat("var_lns_", local_params.name())), + helper, absl::StrCat("graph_var_lns_", local_params.name())), local_params, helper, &shared)); subsolvers.push_back(absl::make_unique( absl::make_unique( - helper, absl::StrCat("cst_lns_", local_params.name())), + helper, absl::StrCat("graph_cst_lns_", local_params.name())), local_params, helper, &shared)); if (!helper->TypeToConstraints(ConstraintProto::kNoOverlap).empty()) { diff --git a/ortools/sat/cp_model_symmetries.cc b/ortools/sat/cp_model_symmetries.cc index d9af14ae38..94a489b915 100644 --- a/ortools/sat/cp_model_symmetries.cc +++ b/ortools/sat/cp_model_symmetries.cc @@ -44,10 +44,10 @@ class IdGenerator { public: IdGenerator() {} - // If the key was never seen before, then generate a new id, otherwise return - // the previously generated id. - int GetId(const std::vector& key) { - return gtl::LookupOrInsert(&id_map_, key, id_map_.size()); + // If the color was never seen before, then generate a new id, otherwise + // return the previously generated id. + int GetId(const std::vector& color) { + return gtl::LookupOrInsert(&id_map_, color, id_map_.size()); } int NextFreeId() const { return id_map_.size(); } @@ -91,173 +91,177 @@ std::unique_ptr GenerateGraphForSymmetryDetection( const int num_variables = problem.variables_size(); auto graph = absl::make_unique(); - initial_equivalence_classes->clear(); + + // Each node will be created with a given color. Two nodes of different color + // can never be send one into another by a symmetry. The first element of + // the color vector will always be the NodeType. + // + // TODO(user): Using a full int64 for storing 3 values is not great. We + // can optimize this at the price of a bit more code. enum NodeType { VARIABLE_NODE, + VAR_COEFFICIENT_NODE, CONSTRAINT_NODE, - CONSTRAINT_COEFFICIENT_NODE, - ENFORCEMENT_LITERAL }; - IdGenerator id_generator; + IdGenerator color_id_generator; + initial_equivalence_classes->clear(); + auto new_node = [&initial_equivalence_classes, + &color_id_generator](const std::vector& color) { + // Since we add nodes one by one, initial_equivalence_classes->size() gives + // the number of nodes at any point, which we use as the next node index. + const int node = initial_equivalence_classes->size(); + initial_equivalence_classes->push_back(color_id_generator.GetId(color)); + return node; + }; // For two variables to be in the same equivalence class, they need to have // the same objective coefficient, and the same possible bounds. + // + // TODO(user): We could ignore the objective coefficients, and just make sure + // that when we break symmetry amongst variables, we choose the possibility + // with the smallest cost? std::vector objective_by_var(num_variables, 0); for (int i = 0; i < problem.objective().vars_size(); ++i) { objective_by_var[problem.objective().vars(i)] = problem.objective().coeffs(i); } - auto new_node = [&initial_equivalence_classes, - &id_generator](const std::vector& key) { - // Since we add nodes one by one, initial_equivalence_classes->size() gives - // the number of nodes at any point, which we use as the next node index. - const int node = initial_equivalence_classes->size(); - initial_equivalence_classes->push_back(id_generator.GetId(key)); - return node; - }; - - std::vector tmp_key; + // Create one node for each variable. Note that the code rely on the fact that + // the index of a VARIABLE_NODE type is the same as the variable index. + std::vector tmp_color; for (int v = 0; v < num_variables; ++v) { - tmp_key = {VARIABLE_NODE, objective_by_var[v]}; - Append(problem.variables(v).domain(), &tmp_key); - - // Note that the code rely on the fact that the index of a VARIABLE_NODE - // type is the same as the variable index. - CHECK_EQ(v, new_node(tmp_key)); + tmp_color = {VARIABLE_NODE, objective_by_var[v]}; + Append(problem.variables(v).domain(), &tmp_color); + CHECK_EQ(v, new_node(tmp_color)); // Make sure the graph contains all the variable nodes, even if no edges are // attached to them through constraints. graph->AddNode(v); } - auto add_edge = [&graph](int node_1, int node_2) { - graph->AddArc(node_1, node_2); - graph->AddArc(node_2, node_1); - }; + // We will lazily create "coefficient nodes" that correspond to a variable + // with a given coefficient. + absl::flat_hash_map, int> coefficient_nodes; + auto get_coefficient_node = [&new_node, &graph, &coefficient_nodes, + &tmp_color](int var, int64 coeff) { + const int var_node = var; + DCHECK(RefIsPositive(var)); - // We will create a bunch of nodes linked to a variable node. Only one node - // per (var, type) will be required so we cache them to not create more node - // than necessary. - absl::flat_hash_map, int, VectorHash> secondary_var_nodes; - auto get_secondary_var_node = [&new_node, &add_edge, &secondary_var_nodes, - &tmp_key](int var_node, - absl::Span type) { - tmp_key.assign(type.begin(), type.end()); - tmp_key.push_back(var_node); - const auto insert = secondary_var_nodes.insert({tmp_key, 0}); + // For a coefficient of one, which are the most common, we can optimize the + // size of the graph by omitting the coefficient node altogether and using + // directly the var_node in this case. + if (coeff == 1) return var_node; + + const auto insert = + coefficient_nodes.insert({std::make_pair(var, coeff), 0}); if (!insert.second) return insert.first->second; - tmp_key.pop_back(); - const int secondary_node = new_node(tmp_key); - add_edge(var_node, secondary_node); + tmp_color = {VAR_COEFFICIENT_NODE, coeff}; + const int secondary_node = new_node(tmp_color); + graph->AddArc(var_node, secondary_node); insert.first->second = secondary_node; return secondary_node; }; - auto add_literal_edge = [&add_edge, &get_secondary_var_node]( - int ref, int constraint_node) { - const int variable_node = PositiveRef(ref); - if (RefIsPositive(ref)) { - // For all coefficients equal to one, which are the most common, we - // can optimize the size of the graph by omitting the coefficient - // node altogether. - add_edge(variable_node, constraint_node); - } else { - const int coefficient_node = get_secondary_var_node( - variable_node, {CONSTRAINT_COEFFICIENT_NODE, -1}); - add_edge(coefficient_node, constraint_node); - } + // For a literal we use the same as a coefficient 1 or -1. We can do that + // because literal and (var, coefficient) never appear together in the same + // constraint. + auto get_literal_node = [&get_coefficient_node](int ref) { + return get_coefficient_node(PositiveRef(ref), RefIsPositive(ref) ? 1 : -1); + }; + + // Because the implication can be numerous, we encode them without constraints + // node by using an arc from the lhs to the rhs. Note that we also always add + // the other direction. We use a set to remove duplicates both for efficiency + // and to not artificially break symmetries by using multi-arcs. + absl::flat_hash_set> implications; + auto add_implication = [&get_literal_node, &graph, &implications](int ref_a, + int ref_b) { + const auto insert = implications.insert({ref_a, ref_b}); + if (!insert.second) return; + graph->AddArc(get_literal_node(ref_a), get_literal_node(ref_b)); + + // Always add the other side. + implications.insert({NegatedRef(ref_b), NegatedRef(ref_a)}); + graph->AddArc(get_literal_node(NegatedRef(ref_b)), + get_literal_node(NegatedRef(ref_a))); }; // Add constraints to the graph. for (const ConstraintProto& constraint : problem.constraints()) { const int constraint_node = initial_equivalence_classes->size(); - std::vector key = {CONSTRAINT_NODE, constraint.constraint_case()}; + std::vector color = {CONSTRAINT_NODE, constraint.constraint_case()}; switch (constraint.constraint_case()) { case ConstraintProto::CONSTRAINT_NOT_SET: break; case ConstraintProto::kLinear: { - Append(constraint.linear().domain(), &key); - CHECK_EQ(constraint_node, new_node(key)); - + // TODO(user): We can use the same trick as for the implications to + // encode relations of the form coeff * var_a <= coeff * var_b without + // creating a constraint node by directly adding an arc between the two + // var coefficient nodes. + Append(constraint.linear().domain(), &color); + CHECK_EQ(constraint_node, new_node(color)); for (int i = 0; i < constraint.linear().vars_size(); ++i) { - if (constraint.linear().coeffs(i) == 0) continue; const int ref = constraint.linear().vars(i); const int variable_node = PositiveRef(ref); const int64 coeff = RefIsPositive(ref) ? constraint.linear().coeffs(i) : -constraint.linear().coeffs(i); - if (coeff == 1) { - // For all coefficients equal to one, which are the most common, we - // can optimize the size of the graph by omitting the coefficient - // node altogether. - add_edge(variable_node, constraint_node); - } else { - const int coefficient_node = get_secondary_var_node( - variable_node, {CONSTRAINT_COEFFICIENT_NODE, coeff}); - add_edge(coefficient_node, constraint_node); - } + graph->AddArc(get_coefficient_node(variable_node, coeff), + constraint_node); } break; } case ConstraintProto::kBoolOr: { - CHECK_EQ(constraint_node, new_node(key)); + CHECK_EQ(constraint_node, new_node(color)); for (const int ref : constraint.bool_or().literals()) { - add_literal_edge(ref, constraint_node); + graph->AddArc(get_literal_node(ref), constraint_node); } break; } case ConstraintProto::kAtMostOne: { - CHECK_EQ(constraint_node, new_node(key)); + if (constraint.at_most_one().literals().size() == 2) { + // Treat it as an implication to avoid creating a node. + add_implication(constraint.at_most_one().literals(0), + NegatedRef(constraint.at_most_one().literals(1))); + break; + } + + CHECK_EQ(constraint_node, new_node(color)); for (const int ref : constraint.at_most_one().literals()) { - add_literal_edge(ref, constraint_node); + graph->AddArc(get_literal_node(ref), constraint_node); } break; } case ConstraintProto::kExactlyOne: { - CHECK_EQ(constraint_node, new_node(key)); + CHECK_EQ(constraint_node, new_node(color)); for (const int ref : constraint.exactly_one().literals()) { - add_literal_edge(ref, constraint_node); + graph->AddArc(get_literal_node(ref), constraint_node); } break; } case ConstraintProto::kBoolXor: { - CHECK_EQ(constraint_node, new_node(key)); + CHECK_EQ(constraint_node, new_node(color)); for (const int ref : constraint.bool_xor().literals()) { - add_literal_edge(ref, constraint_node); + graph->AddArc(get_literal_node(ref), constraint_node); } break; } - - // TODO(user): We could directly connect variable node together to - // deal more efficiently with this constraint. Make sure not to create - // multi-arc since I am not sure the symmetry code works with these - // though. case ConstraintProto::kBoolAnd: { - if (constraint.enforcement_literal_size() == 0) { - // All literals are true in this case. - CHECK_EQ(constraint_node, new_node(key)); - for (const int ref : constraint.bool_and().literals()) { - add_literal_edge(ref, constraint_node); - } - break; + // The other cases should be presolved before this is called. + // TODO(user): not 100% true, this happen on rmatr200-p5, Fix. + if (constraint.enforcement_literal_size() != 1) { + VLOG(1) << "BoolAnd with multiple enforcement literal are not " + "supported in symmetry code:" + << constraint.ShortDebugString(); + return nullptr; } - // To make the BoolAnd constraint more generic in the graph, we expand - // it into a set of BoolOr constraints where - // not(enforcement_literal) OR literal = true - // for all constraint's literals. This is equivalent to - // enforcement_literal => literal - // for all literals. - std::vector key = {CONSTRAINT_NODE, ConstraintProto::kBoolOr}; - const int non_enforcement_literal = - NegatedRef(constraint.enforcement_literal(0)); - for (const int literal : constraint.bool_and().literals()) { - const int constraint_node = new_node(key); - add_literal_edge(non_enforcement_literal, constraint_node); - add_literal_edge(literal, constraint_node); + CHECK_EQ(constraint.enforcement_literal_size(), 1); + const int ref_a = constraint.enforcement_literal(0); + for (const int ref_b : constraint.bool_and().literals()) { + add_implication(ref_a, ref_b); } break; } @@ -274,11 +278,13 @@ std::unique_ptr GenerateGraphForSymmetryDetection( } } + // For enforcement, we use a similar trick than for the implications. + // Because all our constraint arcs are in the direction var_node to + // constraint_node, we just use the reverse direction for the enforcement + // part. This way we can reuse the same get_literal_node() function. if (constraint.constraint_case() != ConstraintProto::kBoolAnd) { for (const int ref : constraint.enforcement_literal()) { - const int enforcement_node = get_secondary_var_node( - PositiveRef(ref), {ENFORCEMENT_LITERAL, RefIsPositive(ref)}); - add_edge(constraint_node, enforcement_node); + graph->AddArc(constraint_node, get_literal_node(ref)); } } } @@ -294,7 +300,7 @@ std::unique_ptr GenerateGraphForSymmetryDetection( // // TODO(user): It will probably be more efficient to not even create these // nodes, but we will need a mapping to know the variable <-> node index. - int next_id = id_generator.NextFreeId(); + int next_id = color_id_generator.NextFreeId(); for (int i = 0; i < num_variables; ++i) { if ((*graph)[i].empty()) { (*initial_equivalence_classes)[i] = next_id++; @@ -319,7 +325,7 @@ std::unique_ptr GenerateGraphForSymmetryDetection( void FindCpModelSymmetries( const SatParameters& params, const CpModelProto& problem, std::vector>* generators, - double time_limit_seconds) { + double deterministic_limit) { const bool log_info = params.log_search_progress() || VLOG_IS_ON(1); CHECK(generators != nullptr); generators->clear(); @@ -333,15 +339,17 @@ void FindCpModelSymmetries( if (log_info) { LOG(INFO) << "Graph for symmetry has " << graph->num_nodes() - << " nodes and " << graph->num_arcs() / 2 << " edges."; + << " nodes and " << graph->num_arcs() << " arcs."; } if (graph->num_nodes() == 0) return; - GraphSymmetryFinder symmetry_finder(*graph, /*is_undirected=*/true); + GraphSymmetryFinder symmetry_finder(*graph, /*is_undirected=*/false); std::vector factorized_automorphism_group_size; + std::unique_ptr time_limit = + TimeLimit::FromDeterministicTime(deterministic_limit); const absl::Status status = symmetry_finder.FindSymmetries( - time_limit_seconds, &equivalence_classes, generators, - &factorized_automorphism_group_size); + &equivalence_classes, generators, &factorized_automorphism_group_size, + time_limit.get()); // TODO(user): Change the API to not return an error when the time limit is // reached. @@ -384,12 +392,17 @@ void FindCpModelSymmetries( } generators->resize(num_generators); average_support_size /= num_generators; - if (log_info && num_generators > 0) { - LOG(INFO) << "# of generators: " << num_generators; - LOG(INFO) << "Average support size: " << average_support_size; - if (num_duplicate_constraints > 0) { - LOG(INFO) << "The model contains " << num_duplicate_constraints - << " duplicate constraints !"; + if (log_info) { + LOG(INFO) << "Symmetry computation done. time: " + << time_limit->GetElapsedTime() + << " dtime: " << time_limit->GetElapsedDeterministicTime(); + if (num_generators > 0) { + LOG(INFO) << "# of generators: " << num_generators; + LOG(INFO) << "Average support size: " << average_support_size; + if (num_duplicate_constraints > 0) { + LOG(INFO) << "The model contains " << num_duplicate_constraints + << " duplicate constraints !"; + } } } } @@ -463,7 +476,7 @@ bool DetectAndExploitSymmetriesInPresolve(PresolveContext* context) { // Collect the at most ones. // - // Note(user): This relies on the fact that the pointer remain stable when + // Note(user): This relies on the fact that the pointers remain stable when // we adds new constraints. It should be the case, but it is a bit unsafe. // On the other hand it is annoying to deal with both cases below. std::vector*> at_most_ones; @@ -496,7 +509,8 @@ bool DetectAndExploitSymmetriesInPresolve(PresolveContext* context) { } } - while (!orbitope.empty() && !orbitope[0].empty()) { + while (!orbitope.empty() && orbitope[0].size() > 1) { + const int num_cols = orbitope[0].size(); const std::vector orbits = GetOrbitopeOrbits(num_vars, orbitope); // Because in the orbitope case, we have a full symmetry group of the @@ -528,25 +542,39 @@ bool DetectAndExploitSymmetriesInPresolve(PresolveContext* context) { // Note(user): On the miplib, only 1/ happens currently. Not sure with LNS // though. std::vector all_equivalent_rows(orbitope.size(), false); - std::vector at_most_one_one(orbitope.size(), false); - std::vector at_most_one_zero(orbitope.size(), false); + // The result described above can be generalized if an at most one intersect + // many of the orbitope rows, each in at leat two positions. We will track + // the set of best rows on which we have an at most one (or at most one + // zero) on all their entries. + bool at_most_one_in_best_rows; // The alternative is at most one zero. + int64 best_score = 0; + std::vector best_rows; + + std::vector rows_in_at_most_one; for (const google::protobuf::RepeatedField* literals : at_most_ones) { tmp_to_clear.clear(); - int num_in_intersections = 0; for (const int literal : *literals) { if (context->IsFixed(literal)) continue; const int var = PositiveRef(literal); const int rep = orbits[var]; if (rep == -1) continue; - ++num_in_intersections; if (tmp_sizes[rep] == 0) tmp_to_clear.push_back(rep); tmp_sizes[rep]++; if (RefIsPositive(literal)) tmp_num_positive[rep]++; } + int num_positive_direction = 0; + int num_negative_direction = 0; + + // An at most one touching two positions in an orbitope row can possibly + // be extended, depending if it has singleton intersection swith other + // rows and where. + bool possible_extension = false; + + rows_in_at_most_one.clear(); for (const int row : tmp_to_clear) { const int size = tmp_sizes[row]; const int num_positive = tmp_num_positive[row]; @@ -555,37 +583,55 @@ bool DetectAndExploitSymmetriesInPresolve(PresolveContext* context) { tmp_num_positive[row] = 0; if (num_positive > 1 && num_negative == 0) { - at_most_one_one[row] = true; + if (size < num_cols) possible_extension = true; + rows_in_at_most_one.push_back(row); + ++num_positive_direction; } else if (num_positive == 0 && num_negative > 1) { - at_most_one_zero[row] = true; + if (size < num_cols) possible_extension = true; + rows_in_at_most_one.push_back(row); + ++num_negative_direction; } else if (num_positive > 0 && num_negative > 0) { all_equivalent_rows[row] = true; } + } - // We might be able to presolve more in these cases. - if (at_most_one_zero[row] || at_most_one_one[row] || - all_equivalent_rows[row]) { - if (tmp_to_clear.size() > 1) { - context->UpdateRuleStats( - "TODO symmetry: at most one across orbits."); - } else if (size < orbitope[0].size()) { - context->UpdateRuleStats( - "TODO symmetry: at most one can be extended"); - } - } + if (possible_extension) { + context->UpdateRuleStats( + "TODO symmetry: possible at most one extension."); + } + + if (num_positive_direction > 0 && num_negative_direction > 0) { + return context->NotifyThatModelIsUnsat("Symmetry and at most ones"); + } + const bool direction = num_positive_direction > 0; + + // Because of symmetry, the choice of the column shouldn't matter (they + // will all appear in the same number of constraints of the same types), + // however we prefer to fix the variables that seems to touch more + // constraints. + // + // TODO(user): maybe we should simplify the constraint using the variable + // we fix before choosing the next row to break symmetry on. If there are + // multiple row involved, we could also take the intersection instead of + // probably counting the same constraints more than once. + int64 score = 0; + for (const int row : rows_in_at_most_one) { + score += + context->VarToConstraints(PositiveRef(orbitope[row][0])).size(); + } + if (score > best_score) { + at_most_one_in_best_rows = direction; + best_score = score; + best_rows = rows_in_at_most_one; } } - // Heuristically choose a "best" row/col to "distinguish" and break the - // symmetry on. - int best_row = 0; - int best_col = 0; - int best_score = 0; - bool fix_others_to_zero = true; + // Mark all the equivalence. + // Note that this operation do not change the symmetry group. + // + // TODO(user): We could remove these rows from the orbitope. Note that + // currently this never happen on the miplib (maybe in LNS though). for (int i = 0; i < all_equivalent_rows.size(); ++i) { - const int num_cols = orbitope[i].size(); - - // Note that this operation do not change the symmetry group. if (all_equivalent_rows[i]) { for (int j = 1; j < num_cols; ++j) { context->StoreBooleanEqualityRelation(orbitope[i][0], orbitope[i][j]); @@ -593,94 +639,68 @@ bool DetectAndExploitSymmetriesInPresolve(PresolveContext* context) { if (context->ModelIsUnsat()) return false; } } - - // Because of symmetry, the choice of the column shouldn't matter (they - // will all appear in the same number of constraints of the same types), - // however we prefer to fix a variable that seems to touch more - // constraints. - // - // TODO(user): maybe we should simplify the constraint using the variable - // we fix before choosing the next row to break symmetry on. - const int row_score = - context->VarToConstraints(PositiveRef(orbitope[i][0])).size(); - - // TODO(user): If one variable make the line already fixed, we should just - // ignore this row. Not too important as actually this shouldn't happen - // because we never compute symmetries involving fixed variables. But in - // the future, fixing some literal might have some side effects and fix - // others. - if (at_most_one_one[i] && row_score > best_score) { - best_col = 0; - for (int j = 0; j < num_cols; ++j) { - if (context->LiteralIsTrue(orbitope[i][j])) { - best_col = j; - break; - } - } - best_row = i; - best_score = row_score; - fix_others_to_zero = true; - } - if (at_most_one_zero[i] && row_score > best_score) { - best_col = 0; - for (int j = 0; j < num_cols; ++j) { - if (context->LiteralIsFalse(orbitope[i][j])) { - best_col = j; - break; - } - } - best_row = i; - best_score = row_score; - fix_others_to_zero = false; - } } - if (best_score == 0) break; - for (int j = 1; j < orbitope[best_row].size(); ++j) { - if (fix_others_to_zero) { - context->UpdateRuleStats("symmetry: fixed to false"); - if (!context->SetLiteralToFalse(orbitope[best_row][j])) return false; - } else { - context->UpdateRuleStats("symmetry: fixed to true"); - if (!context->SetLiteralToTrue(orbitope[best_row][j])) return false; - } - } - - // We add the symmetry breaking inequalities: best_var >= all other var - // in orbit. That is not(best_var) => not(other) for Booleans. We only add - // them if we didn't fix any variable just above. + // Break the symmetry on our set of best rows by picking one columns + // and setting all the other entries to zero or one. Note that the at most + // one applies to all entries in all rows. // - // TODO(user): Add the inequality for non-Boolean too? Also note that this - // code only run if the code above is disabled. It is here for testing - // alternatives. In particular, if there is no at most one, we cannot fix - // n-1 variables, but we can still add inequalities. - const int best_var = orbitope[best_row][best_col]; - const bool maximize_best_var = fix_others_to_zero; - if (context->CanBeUsedAsLiteral(best_var) && !context->IsFixed(best_var)) { - ConstraintProto* ct = context->working_model->add_constraints(); - ct->add_enforcement_literal(maximize_best_var ? NegatedRef(best_var) - : best_var); - for (const int other : orbitope[best_row]) { - if (other == best_var) continue; - if (context->IsFixed(other)) continue; - ct->mutable_bool_and()->add_literals( - maximize_best_var ? NegatedRef(other) : other); - context->UpdateRuleStats("symmetry: added implication"); - } - context->UpdateNewConstraintsVariableUsage(); + // TODO(user): We don't have any at most one relation on this orbitope, + // but we could still add symmetry breaking inequality by picking any matrix + // entry and making it the largest/lowest value on its row. This also work + // for non-Booleans. + if (best_score == 0) { + context->UpdateRuleStats( + "TODO symmetry: add symmetry breaking inequalities?"); + break; } - // Remove the column of best_var. - CHECK_NE(best_col, -1); + // If our symmetry group is valid, they cannot be any variable already + // fixed to one (or zero if !at_most_one_in_best_rows). Otherwise all would + // be fixed to one and the problem would be unsat. + for (const int i : best_rows) { + for (int j = 0; j < num_cols; ++j) { + const int var = orbitope[i][j]; + if ((at_most_one_in_best_rows && context->LiteralIsTrue(var)) || + (!at_most_one_in_best_rows && context->LiteralIsFalse(var))) { + return context->NotifyThatModelIsUnsat("Symmetry and at most one"); + } + } + } + + // We have an at most one on a set of rows, we will pick a column, and set + // all other entries on these rows to zero. + // + // TODO(user): All choices should be equivalent, but double check? + const int best_col = 0; + for (const int i : best_rows) { + for (int j = 0; j < num_cols; ++j) { + if (j == best_col) continue; + const int var = orbitope[i][j]; + if (at_most_one_in_best_rows) { + context->UpdateRuleStats("symmetry: fixed to false"); + if (!context->SetLiteralToFalse(var)) return false; + } else { + context->UpdateRuleStats("symmetry: fixed to true"); + if (!context->SetLiteralToTrue(var)) return false; + } + } + } + + // Remove all best rows. + for (const int i : best_rows) orbitope[i].clear(); + int new_size = 0; + for (int i = 0; i < orbitope.size(); ++i) { + if (!orbitope[i].empty()) orbitope[new_size++] = orbitope[i]; + } + CHECK_LT(new_size, orbitope.size()); + orbitope.resize(new_size); + + // Remove best_col. for (int i = 0; i < orbitope.size(); ++i) { std::swap(orbitope[i][best_col], orbitope[i].back()); orbitope[i].pop_back(); } - - // We also remove the line of best_var since heuristicially, it is better to - // not add symmetries involving any of the variable on this line. - std::swap(orbitope[best_row], orbitope.back()); - orbitope.pop_back(); } return true; diff --git a/ortools/sat/cp_model_symmetries.h b/ortools/sat/cp_model_symmetries.h index 40705f2707..e1d8c96eaa 100644 --- a/ortools/sat/cp_model_symmetries.h +++ b/ortools/sat/cp_model_symmetries.h @@ -41,12 +41,10 @@ namespace sat { // TODO(user): As long as we only exploit symmetry involving only Boolean // variables we can make this code more efficient by not detecting symmetries // involing integer variable. -// -// TODO(user): Make the limit deterministic. void FindCpModelSymmetries( const SatParameters& params, const CpModelProto& problem, std::vector>* generators, - double time_limit_seconds = std::numeric_limits::infinity()); + double deterministic_limit = std::numeric_limits::infinity()); // Basic implementation of some symmetry breaking during presolve. // diff --git a/ortools/sat/sat_parameters.proto b/ortools/sat/sat_parameters.proto index 182af6b1c2..82f518ba87 100644 --- a/ortools/sat/sat_parameters.proto +++ b/ortools/sat/sat_parameters.proto @@ -21,7 +21,7 @@ option java_multiple_files = true; // Contains the definitions for all the sat algorithm parameters and their // default values. // -// NEXT TAG: 184 +// NEXT TAG: 185 message SatParameters { // In some context, like in a portfolio of search, it makes sense to name a // given parameters set for logging purpose. @@ -686,8 +686,8 @@ message SatParameters { // Target number of constraints to remove during cleanup. optional int32 cut_cleanup_target = 157 [default = 1000]; - // Add that many lazy contraints (or cuts) at once in the LP. Note that at the - // beginning of the solve, we do add more than this. + // Add that many lazy constraints (or cuts) at once in the LP. Note that at + // the beginning of the solve, we do add more than this. optional int32 new_constraints_batch_size = 122 [default = 50]; // The search branching will be used to decide how to branch on unfixed nodes. @@ -872,6 +872,7 @@ message SatParameters { // LNS parameters. optional bool use_lns_only = 101 [default = false]; optional bool lns_focus_on_decision_variables = 105 [default = false]; + optional bool lns_expand_intervals_in_constraint_graph = 184 [default = true]; // Turns on relaxation induced neighborhood generator. optional bool use_rins_lns = 129 [default = true];