Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
shaving_solver.cc
Go to the documentation of this file.
1// Copyright 2010-2024 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
15
16#include <cstdint>
17#include <functional>
18#include <memory>
19#include <string>
20#include <utility>
21#include <vector>
22
23#include "absl/base/thread_annotations.h"
24#include "absl/flags/flag.h"
25#include "absl/log/check.h"
26#include "absl/random/distributions.h"
27#include "absl/strings/str_cat.h"
28#include "absl/synchronization/mutex.h"
35#include "ortools/sat/integer.h"
36#include "ortools/sat/model.h"
40#include "ortools/sat/util.h"
43
44namespace operations_research {
45namespace sat {
46
48 const SatParameters& local_parameters, NeighborhoodGeneratorHelper* helper,
49 SharedClasses* shared)
50 : SubSolver(local_parameters.name(), FULL_PROBLEM),
51 local_params_(local_parameters),
52 helper_(helper),
53 shared_(shared),
54 local_proto_(shared->model_proto) {}
55
59
61 if (shared_->SearchIsDone()) return false;
62
63 // We only support one task at the time.
64 absl::MutexLock mutex_lock(&mutex_);
65 return !task_in_flight_;
66}
67
68std::function<void()> ObjectiveShavingSolver::GenerateTask(int64_t task_id) {
69 {
70 absl::MutexLock mutex_lock(&mutex_);
71 stop_current_chunk_.store(false);
72 task_in_flight_ = true;
73 objective_lb_ = shared_->response->GetInnerObjectiveLowerBound();
74 objective_ub_ = shared_->response->GetInnerObjectiveUpperBound();
75 }
76 return [this, task_id]() {
77 if (ResetModel(task_id)) {
78 SolveLoadedCpModel(local_proto_, local_sat_model_.get());
79 const CpSolverResponse local_response =
80 local_sat_model_->GetOrCreate<SharedResponseManager>()->GetResponse();
81
82 if (local_response.status() == CpSolverStatus::OPTIMAL ||
83 local_response.status() == CpSolverStatus::FEASIBLE) {
84 std::vector<int64_t> solution_values(local_response.solution().begin(),
85 local_response.solution().end());
86 if (local_params_.cp_model_presolve()) {
87 const int num_original_vars = shared_->model_proto.variables_size();
88 PostsolveResponseWrapper(local_params_, num_original_vars,
89 mapping_proto_, postsolve_mapping_,
90 &solution_values);
91 }
92 shared_->response->NewSolution(solution_values, Info());
93 } else if (local_response.status() == CpSolverStatus::INFEASIBLE) {
94 absl::MutexLock mutex_lock(&mutex_);
96 Info(), current_objective_target_ub_ + 1, objective_ub_);
97 }
98 }
99
100 absl::MutexLock mutex_lock(&mutex_);
101 task_in_flight_ = false;
102 if (local_sat_model_ != nullptr) {
103 const double dtime = local_sat_model_->GetOrCreate<TimeLimit>()
104 ->GetElapsedDeterministicTime();
106 shared_->time_limit->AdvanceDeterministicTime(dtime);
107 }
108 };
109}
110
112 absl::MutexLock mutex_lock(&mutex_);
113 if (!task_in_flight_) return;
114
115 // We are just waiting for the inner code to check the time limit or
116 // to return nicely.
117 if (stop_current_chunk_) return;
118
119 // TODO(user): Also stop if we have enough newly fixed / improved root level
120 // bounds so that we think it is worth represolving and restarting.
121 if (shared_->SearchIsDone()) {
122 stop_current_chunk_.store(true);
123 }
124
125 // The current objective lower bound has been improved, restarting.
126 if (shared_->response->GetInnerObjectiveLowerBound() > objective_lb_) {
127 stop_current_chunk_.store(true);
128 }
129
130 // A solution has been found that is better than the current target
131 // objective upper bound. Restarting to use a smaller delta.
132 if (shared_->response->GetInnerObjectiveUpperBound() <=
133 current_objective_target_ub_ &&
134 current_objective_target_ub_ != objective_lb_) {
135 stop_current_chunk_.store(true);
136 }
137
138 // If the range has been reduced enough to warrant a delta of 1, while the
139 // current search uses a delta > 1. Restarting to switch to the delta of 1.
140 if (current_objective_target_ub_ != objective_lb_ &&
143 local_params_.shaving_search_threshold()) {
144 stop_current_chunk_.store(true);
145 }
146}
147
148std::string ObjectiveShavingSolver::Info() {
149 return absl::StrCat(name(), " (vars=", local_proto_.variables().size(),
150 " csts=", local_proto_.constraints().size(), ")");
151}
152
153bool ObjectiveShavingSolver::ResetModel(int64_t task_id) {
154 local_sat_model_ = std::make_unique<Model>(name());
155 *local_sat_model_->GetOrCreate<SatParameters>() = local_params_;
156 local_sat_model_->GetOrCreate<SatParameters>()->set_random_seed(
157 CombineSeed(local_params_.random_seed(), task_id));
158
159 auto* time_limit = local_sat_model_->GetOrCreate<TimeLimit>();
161 time_limit->RegisterSecondaryExternalBooleanAsLimit(&stop_current_chunk_);
162
163 auto* random = local_sat_model_->GetOrCreate<ModelRandomGenerator>();
164
165 // We copy the model.
166 local_proto_ = shared_->model_proto;
167 *local_proto_.mutable_variables() =
168 helper_->FullNeighborhood().delta.variables();
169
170 // Store the current lb in local variable.
171 IntegerValue objective_lb;
172 IntegerValue chosen_objective_ub;
173 {
174 absl::MutexLock mutex_lock(&mutex_);
175 objective_lb = objective_lb_;
176 if (objective_ub_ - objective_lb <=
177 local_params_.shaving_search_threshold()) {
178 current_objective_target_ub_ = objective_lb;
179 } else {
180 const IntegerValue mid = (objective_ub_ - objective_lb) / 2;
181 current_objective_target_ub_ =
182 objective_lb + absl::LogUniform<int64_t>(*random, 0, mid.value());
183 }
184 chosen_objective_ub = current_objective_target_ub_;
185 VLOG(2) << name() << ": from [" << objective_lb.value() << ".."
186 << objective_ub_.value() << "] <= " << chosen_objective_ub.value();
187 }
188
189 // We replace the objective by a constraint, objective in [lb, target_ub].
190 // We modify local_proto_ to a pure feasibility problem.
191 // Not having the objective open up more presolve reduction.
192 Domain obj_domain = Domain(objective_lb.value(), chosen_objective_ub.value());
193 if (local_proto_.objective().domain_size() > 1) {
194 // Intersect with the first interval of the objective domain.
195 obj_domain =
196 obj_domain.IntersectionWith(Domain(local_proto_.objective().domain(0),
197 local_proto_.objective().domain(1)));
198 }
199 if (local_proto_.objective().vars().size() == 1 &&
200 local_proto_.objective().coeffs(0) == 1) {
201 auto* obj_var =
202 local_proto_.mutable_variables(local_proto_.objective().vars(0));
203 const Domain reduced_var_domain = obj_domain.IntersectionWith(
204 Domain(obj_var->domain(0), obj_var->domain(1)));
205 FillDomainInProto(reduced_var_domain, obj_var);
206 } else {
207 auto* obj = local_proto_.add_constraints()->mutable_linear();
208 *obj->mutable_vars() = local_proto_.objective().vars();
209 *obj->mutable_coeffs() = local_proto_.objective().coeffs();
210 FillDomainInProto(obj_domain, obj);
211 }
212
213 // Clear the objective.
214 local_proto_.clear_objective();
215
216 // Dump?
217 if (absl::GetFlag(FLAGS_cp_model_dump_submodels)) {
218 const std::string name =
219 absl::StrCat(absl::GetFlag(FLAGS_cp_model_dump_prefix),
220 "objective_shaving_", objective_lb.value(), ".pb.txt");
221 LOG(INFO) << "Dumping objective shaving model to '" << name << "'.";
222 CHECK(WriteModelProtoToFile(local_proto_, name));
223 }
224
225 // Presolve if asked.
226 if (local_params_.cp_model_presolve()) {
227 mapping_proto_.Clear();
228 postsolve_mapping_.clear();
229 auto context = std::make_unique<PresolveContext>(
230 local_sat_model_.get(), &local_proto_, &mapping_proto_);
231 const CpSolverStatus presolve_status =
232 PresolveCpModel(context.get(), &postsolve_mapping_);
233 if (presolve_status == CpSolverStatus::INFEASIBLE) {
234 absl::MutexLock mutex_lock(&mutex_);
236 Info(), chosen_objective_ub + 1, kMaxIntegerValue);
237 return false;
238 }
239 }
240
241 // Tricky: If we aborted during the presolve above, some constraints might
242 // be in a non-canonical form (like having duplicates, etc...) and it seem
243 // not all our propagator code deal with that properly. So it is important
244 // to abort right away here.
245 //
246 // We had a bug when the LoadCpModel() below was returning infeasible on
247 // such non fully-presolved model.
248 if (time_limit->LimitReached()) return false;
249
250 LoadCpModel(local_proto_, local_sat_model_.get());
251 return true;
252}
253
255 const SatParameters& local_parameters, SharedClasses* shared)
256 : SubSolver(local_parameters.name(), FULL_PROBLEM),
257 local_params_(local_parameters),
258 shared_(shared),
259 stop_current_chunk_(false),
260 model_proto_(shared->model_proto) {
261 if (shared_->bounds != nullptr) {
262 shared_bounds_id_ = shared_->bounds->RegisterNewId();
263 }
264
265 absl::MutexLock mutex_lock(&mutex_);
266 for (const IntegerVariableProto& var_proto : model_proto_.variables()) {
267 var_domains_.push_back(ReadDomainFromProto(var_proto));
268 }
269}
270
272 if (!VLOG_IS_ON(1)) return;
273 if (shared_ == nullptr || shared_->stats == nullptr) return;
274 std::vector<std::pair<std::string, int64_t>> stats;
275 absl::MutexLock mutex_lock(&mutex_);
276 stats.push_back({"variable_shaving/num_vars_tried", num_vars_tried_});
277 stats.push_back({"variable_shaving/num_vars_shaved", num_vars_shaved_});
278 stats.push_back(
279 {"variable_shaving/num_infeasible_found", num_infeasible_found_});
280 shared_->stats->AddStats(stats);
281}
282
284 return !shared_->SearchIsDone();
285}
286
288 const CpSolverResponse& local_response, const State& state) {
289 if (local_response.status() != CpSolverStatus::INFEASIBLE) return;
290
291 absl::MutexLock lock(&mutex_);
292 const Domain domain = var_domains_[state.var_index];
293 Domain new_domain = domain;
294 ++num_infeasible_found_;
295 new_domain = domain.IntersectionWith(state.reduced_domain.Complement());
296 VLOG(1) << name() << ": var(" << state.var_index << ") " << domain << " ==> "
297 << new_domain;
298
299 if (domain != new_domain) {
300 ++num_vars_shaved_;
301 if (shared_->bounds != nullptr && !new_domain.IsEmpty()) {
302 shared_->bounds->ReportPotentialNewBounds(
303 name(), {state.var_index}, {new_domain.Min()}, {new_domain.Max()});
304 }
305 var_domains_[state.var_index] = new_domain;
306 if (var_domains_[state.var_index].IsEmpty()) {
308 "Unsat during variables shaving");
309 return;
310 }
311 }
312}
313
314std::function<void()> VariablesShavingSolver::GenerateTask(int64_t task_id) {
315 return [this, task_id]() mutable {
316 Model local_sat_model;
317 CpModelProto shaving_proto;
318 State state;
319 if (ResetModel(task_id, &state, &local_sat_model, &shaving_proto)) {
320 SolveLoadedCpModel(shaving_proto, &local_sat_model);
321 const CpSolverResponse local_response =
322 local_sat_model.GetOrCreate<SharedResponseManager>()->GetResponse();
323 ProcessLocalResponse(local_response, state);
324 }
325
326 absl::MutexLock mutex_lock(&mutex_);
327 const double dtime =
328 local_sat_model.GetOrCreate<TimeLimit>()->GetElapsedDeterministicTime();
330 shared_->time_limit->AdvanceDeterministicTime(dtime);
331 };
332}
333
335 absl::MutexLock mutex_lock(&mutex_);
336 // We are just waiting for the inner code to check the time limit or
337 // to return nicely.
338 if (stop_current_chunk_) return;
339
340 if (shared_->SearchIsDone()) {
341 stop_current_chunk_.store(true);
342 }
343
344 if (shared_->bounds != nullptr) {
345 std::vector<int> model_variables;
346 std::vector<int64_t> new_lower_bounds;
347 std::vector<int64_t> new_upper_bounds;
348 shared_->bounds->GetChangedBounds(shared_bounds_id_, &model_variables,
349 &new_lower_bounds, &new_upper_bounds);
350
351 for (int i = 0; i < model_variables.size(); ++i) {
352 const int var = model_variables[i];
353 const int64_t new_lb = new_lower_bounds[i];
354 const int64_t new_ub = new_upper_bounds[i];
355 const Domain& old_domain = var_domains_[var];
356 const Domain new_domain =
357 old_domain.IntersectionWith(Domain(new_lb, new_ub));
358 if (new_domain.IsEmpty()) {
360 "Unsat during variables shaving");
361 continue;
362 }
363 var_domains_[var] = new_domain;
364 }
365 }
366}
367
368std::string VariablesShavingSolver::Info() {
369 return absl::StrCat(name(), " (vars=", model_proto_.variables().size(),
370 " csts=", model_proto_.constraints().size(), ")");
371}
372
373int64_t VariablesShavingSolver::DomainSize(int var) const
374 ABSL_SHARED_LOCKS_REQUIRED(mutex_) {
375 return var_domains_[var].Size();
376}
377
378bool VariablesShavingSolver::VarIsFixed(int int_var) const
379 ABSL_SHARED_LOCKS_REQUIRED(mutex_) {
380 return var_domains_[int_var].IsFixed();
381}
382
383bool VariablesShavingSolver::ConstraintIsInactive(int c) const
384 ABSL_SHARED_LOCKS_REQUIRED(mutex_) {
385 for (const int ref : model_proto_.constraints(c).enforcement_literal()) {
386 const int var = PositiveRef(ref);
387 if (VarIsFixed(var) && var_domains_[var].Min() == (var == ref ? 0 : 1)) {
388 return true;
389 }
390 }
391 return false;
392}
393
394bool VariablesShavingSolver::FindNextVar(State* state)
395 ABSL_SHARED_LOCKS_REQUIRED(mutex_) {
396 const int num_vars = var_domains_.size();
397 const int max_index = 2 * num_vars;
398 for (int i = 0; i < 2 * num_vars; ++i) {
399 if (++current_index_ == max_index) current_index_ = 0;
400 const int var = current_index_ / 2;
401 if (VarIsFixed(var)) continue;
402 // Let's not shave the single var objective. There are enough workers
403 // looking at it.
404 if (model_proto_.has_objective() &&
405 model_proto_.objective().vars_size() == 1 &&
406 var == model_proto_.objective().vars(0)) {
407 continue;
408 }
409
410 state->var_index = var;
411 state->minimize = current_index_ % 2 == 0;
412 return true;
413 }
414 return false;
415}
416
417void VariablesShavingSolver::CopyModelConnectedToVar(
418 State* state, Model* local_sat_model, CpModelProto* shaving_proto)
419 ABSL_SHARED_LOCKS_REQUIRED(mutex_) {
420 auto var_to_node = [](int var) { return var; };
421 auto ct_to_node = [this](int ct) {
422 return ct + model_proto_.variables_size();
423 };
424
425 // Build the connected component graph.
427 cc_finder.SetNumberOfNodes(model_proto_.constraints_size() +
428 model_proto_.variables_size());
429 for (int i = 0; i < model_proto_.constraints_size(); ++i) {
430 if (ConstraintIsInactive(i)) continue;
431 const ConstraintProto& ct = model_proto_.constraints(i);
432 const int ct_node = ct_to_node(i);
433 for (const int var : UsedVariables(ct)) {
434 if (VarIsFixed(var)) continue;
435 cc_finder.AddEdge(ct_node, var_to_node(var));
436 }
437 for (const int interval : UsedIntervals(ct)) {
438 cc_finder.AddEdge(ct_node, ct_to_node(interval));
439 }
440 }
441
442 DCHECK(shaving_proto->variables().empty());
443 DCHECK(shaving_proto->constraints().empty());
444 const int root_index = var_to_node(state->var_index);
445
446 const auto active_constraints = [&cc_finder, root_index,
447 &ct_to_node](int ct) {
448 return cc_finder.Connected(root_index, ct_to_node(ct));
449 };
450
451 PresolveContext context(local_sat_model, shaving_proto, nullptr);
453 model_proto_, var_domains_, active_constraints, &context);
454
455 if (VLOG_IS_ON(2) &&
456 shaving_proto->constraints_size() < model_proto_.constraints_size()) {
457 int num_active_variables = 0;
458 for (int i = 0; i < var_domains_.size(); ++i) {
459 if (cc_finder.Connected(root_index, var_to_node(i))) {
460 ++num_active_variables;
461 }
462 }
463
464 LOG(INFO) << "#constraints:" << shaving_proto->constraints_size() << "/"
465 << model_proto_.constraints_size()
466 << " #variables:" << num_active_variables << "/"
467 << var_domains_.size();
468 }
469
470 const Domain domain =
471 ReadDomainFromProto(shaving_proto->variables(state->var_index));
472 shaving_proto->clear_objective();
473
474 int64_t delta = 0;
475 if (domain.Size() > local_params_.shaving_search_threshold()) {
476 const int64_t mid_range = (domain.Max() - domain.Min()) / 2;
477 auto* random = local_sat_model->GetOrCreate<ModelRandomGenerator>();
478 delta = absl::LogUniform<int64_t>(*random, 0, mid_range);
479 }
480
481 if (state->minimize) {
482 state->reduced_domain =
483 domain.IntersectionWith({domain.Min(), domain.Min() + delta});
484 } else {
485 state->reduced_domain =
486 domain.IntersectionWith({domain.Max() - delta, domain.Max()});
487 }
488
489 FillDomainInProto(state->reduced_domain,
490 shaving_proto->mutable_variables(state->var_index));
491
492 if (absl::GetFlag(FLAGS_cp_model_dump_submodels)) {
493 const std::string shaving_name = absl::StrCat(
494 absl::GetFlag(FLAGS_cp_model_dump_prefix), "shaving_var_",
495 state->var_index, (state->minimize ? "_min" : "_max"), ".pb.txt");
496 LOG(INFO) << "Dumping shaving model to '" << shaving_name << "'.";
497 CHECK(WriteModelProtoToFile(*shaving_proto, shaving_name));
498 }
499}
500
501bool VariablesShavingSolver::ResetModel(int64_t task_id, State* state,
502 Model* local_sat_model,
503 CpModelProto* shaving_proto) {
504 *local_sat_model->GetOrCreate<SatParameters>() = local_params_;
505 local_sat_model->GetOrCreate<SatParameters>()->set_random_seed(
506 CombineSeed(local_params_.random_seed(), task_id));
507
508 {
509 absl::MutexLock lock(&mutex_);
510 if (!FindNextVar(state)) return false;
511 CopyModelConnectedToVar(state, local_sat_model, shaving_proto);
512 ++num_vars_tried_;
513 }
514
515 auto* time_limit = local_sat_model->GetOrCreate<TimeLimit>();
517 time_limit->RegisterSecondaryExternalBooleanAsLimit(&stop_current_chunk_);
518 time_limit->ChangeDeterministicLimit(
519 time_limit->GetElapsedDeterministicTime() +
520 local_params_.shaving_search_deterministic_time());
521
522 // Presolve if asked.
523 if (local_params_.cp_model_presolve()) {
524 std::vector<int> postsolve_mapping;
525 CpModelProto mapping_proto;
526 auto context = std::make_unique<PresolveContext>(
527 local_sat_model, shaving_proto, &mapping_proto);
528 const CpSolverStatus presolve_status =
529 PresolveCpModel(context.get(), &postsolve_mapping);
530 if (presolve_status == CpSolverStatus::INFEASIBLE) {
531 CpSolverResponse tmp_response;
532 tmp_response.set_status(CpSolverStatus::INFEASIBLE);
533 ProcessLocalResponse(tmp_response, *state);
534 return false;
535 }
536 }
537
538 auto* local_response_manager =
539 local_sat_model->GetOrCreate<SharedResponseManager>();
540 local_response_manager->InitializeObjective(*shaving_proto);
541 local_response_manager->SetSynchronizationMode(true);
542
543 // Tricky: If we aborted during the presolve above, some constraints might
544 // be in a non-canonical form (like having duplicates, etc...) and it seem
545 // not all our propagator code deal with that properly. So it is important
546 // to abort right away here.
547 //
548 // We had a bug when the LoadCpModel() below was returning infeasible on
549 // such non fully-presolved model.
550 if (time_limit->LimitReached()) return false;
551
552 LoadCpModel(*shaving_proto, local_sat_model);
553 return true;
554}
555
556} // namespace sat
557} // namespace operations_research
A connected components finder that only works on dense ints.
bool Connected(int node1, int node2)
bool AddEdge(int node1, int node2)
Domain IntersectionWith(const Domain &domain) const
void UpdateLocalLimit(TimeLimit *local_limit)
Definition time_limit.h:346
void AdvanceDeterministicTime(double deterministic_duration)
Definition time_limit.h:351
Neighborhood FullNeighborhood() const
Returns a neighborhood that correspond to the full problem.
ObjectiveShavingSolver(const SatParameters &local_parameters, NeighborhoodGeneratorHelper *helper, SharedClasses *shared)
std::function< void()> GenerateTask(int64_t task_id) override
void NotifyThatImprovingProblemIsInfeasible(const std::string &worker_info)
void NewSolution(absl::Span< const int64_t > solution_values, const std::string &solution_info, Model *model=nullptr)
void UpdateInnerObjectiveBounds(const std::string &update_info, IntegerValue lb, IntegerValue ub)
Updates the inner objective bounds.
void AddTimingStat(const SubSolver &subsolver)
Add a line to the corresponding table.
void AddStats(absl::Span< const std::pair< std::string, int64_t > > stats)
Adds a bunch of stats, adding count for the same key together.
void AddTaskDeterministicDuration(double deterministic_duration)
Definition subsolver.h:109
std::string name() const
Returns the name of this SubSolver. Used in logs.
Definition subsolver.h:96
VariablesShavingSolver(const SatParameters &local_parameters, SharedClasses *shared)
std::function< void()> GenerateTask(int64_t task_id) override
void ProcessLocalResponse(const CpSolverResponse &local_response, const State &state)
const std::string name
A name for logging purposes.
const Constraint * ct
IntVar * var
GurobiMPCallbackContext * context
time_limit
Definition solve.cc:22
void SolveLoadedCpModel(const CpModelProto &model_proto, Model *model)
constexpr IntegerValue kMaxIntegerValue(std::numeric_limits< IntegerValue::ValueType >::max() - 1)
bool ImportModelAndDomainsWithBasicPresolveIntoContext(const CpModelProto &in_model, const std::vector< Domain > &domains, std::function< bool(int)> active_constraints, PresolveContext *context)
int CombineSeed(int base_seed, int64_t delta)
We assume delta >= 0 and we only use the low bit of delta.
bool WriteModelProtoToFile(const M &proto, absl::string_view filename)
void PostsolveResponseWrapper(const SatParameters &params, int num_variable_in_original_model, const CpModelProto &mapping_proto, const std::vector< int > &postsolve_mapping, std::vector< int64_t > *solution)
CpSolverStatus PresolveCpModel(PresolveContext *context, std::vector< int > *postsolve_mapping)
Convenient wrapper to call the full presolve.
std::vector< int > UsedVariables(const ConstraintProto &ct)
std::vector< int > UsedIntervals(const ConstraintProto &ct)
Returns the sorted list of interval used by a constraint.
void FillDomainInProto(const Domain &domain, ProtoWithDomain *proto)
Serializes a Domain into the domain field of a proto.
Domain ReadDomainFromProto(const ProtoWithDomain &proto)
Reads a Domain from the domain field of a proto.
void LoadCpModel(const CpModelProto &model_proto, Model *model)
In SWIG mode, we don't want anything besides these top-level includes.
int64_t delta
Definition resource.cc:1709
IntervalVar * interval
Definition resource.cc:101
const CpModelProto & model_proto
These are never nullptr.
std::unique_ptr< SharedBoundsManager > bounds
These can be nullptr depending on the options.
SharedStatTables stat_tables
For displaying summary at the end.