ortools整数规划模型求解cp-sat solve源码分析
求解模型入口代码
CpSolverResponse Solve(const CpModelProto& model_proto) {
Model model;
return SolveCpModel(model_proto, &model);
}
1.class Model
功能
该源码为OR-Tools库中的Model类,用于拥有和管理一个优化模型。Model类提供了一些功能,包括:
- 可以给模型命名,方便调试和记录。
- 可以通过Add函数向模型添加约束,并支持返回约束对象的模板函数。
- 可以通过GetOrCreate函数获取模型中的单例对象,并支持返回对象的可变和不可变版本。
- 可以通过TakeOwnership函数将指针的所有权交给模型,由模型负责在销毁时释放内存。
- 可以通过Create函数创建一个非单例对象,并自动将其所有权交给模型。
- 可以通过Register函数注册一个非拥有的类作为模型中的单例对象。
总之,Model类为优化模型提供了管理和维护的功能,使得构建和操作优化模型更加方便和灵活。
用法
Model()
: 默认构造函数,创建一个空的Model对象。用于初始化一个新的模型。~Model()
: 析构函数,释放Model对象及其所有拥有的资源。在销毁Model对象时调用,确保释放所有的资源。Model(std::string name)
: 带名称的构造函数,创建一个具有指定名称的Model对象。可用于给模型命名,方便调试和记录。template <typename T> T Add(std::function<T(Model*)> f)
: 向模型中添加约束或其他元素的方法。接受一个函数对象作为参数,该函数使用模型作为参数,并返回所添加的约束或元素。可以通过lambda表达式或其他函数对象来创建并添加约束。template <typename T> T Get(std::function<T(const Model&)> f) const
: 获取模型中特定类型的对象的方法。接受一个函数对象作为参数,该函数使用模型的常量引用作为参数,并返回所需的对象。可以通过lambda表达式或其他函数对象来获取对象。template <typename T> T* GetOrCreate()
: 获取模型中唯一的对象,如果对象不存在则创建一个新对象。根据对象的类型,在模型中查找是否已经存在该类型的对象实例,如果存在则返回该实例,否则创建一个新对象,并将其添加到模型中。template <typename T> const T* Get() const
: 获取模型中特定类型的不可变对象的方法。根据对象的类型,在模型中查找是否存在该类型的对象实例,如果存在则返回该实例,否则返回空指针。template <typename T> T* Mutable() const
: 获取模型中特定类型的可变对象的方法。根据对象的类型,在模型中查找是否存在该类型的对象实例,如果存在则返回该实例,否则返回空指针。可以用于修改模型中对象的状态或属性。template <typename T> T* TakeOwnership(T* t)
: 将指针的所有权交给模型,由模型在销毁时释放内存。将指定的指针添加到模型的资源管理列表中,以确保在模型销毁时正确释放内存。template <typename T> T* Create()
: 创建一个非单例对象,并将其所有权交给模型。通过调用T类的构造函数来创建一个新对象,并将其添加到模型的资源管理列表中,以确保在模型销毁时正确释放内存。template <typename T> void Register(T* non_owned_class)
: 注册一个非拥有的类作为模型中的单例对象。将指定的非拥有类添加到模型的单例对象映射中,以便在需要时可以获取到该对象的唯一实例。
这些方法提供了对模型的管理和操作的功能。可以使用Add方法向模型中添加约束或其他元素,使用Get方法获取模型中特定类型的对象,使用GetOrCreate方法获取或创建模型中的唯一对象,使用Mutable方法获取模型中特定类型的可变对象,使用TakeOwnership方法将指针的所有权交给模型,使用Create方法创建一个非单例对象并将其所有权交给模型,使用Register方法注册一个非拥有的类作为模型中的单例对象。这些方法提供了方便和灵活的方式来构建、操作和管理优化模型。
中文源码
namespace operations_research {
namespace sat {
/**
* 拥有与特定优化模型相关的所有内容的类。
*
* 实际上,这个类是一个完全通用的包装器,可以持有任何类型的约束、监视器、求解器,
* 并提供将它们连接在一起的机制。
*/
class Model {
public:
Model() {
}
~Model() {
// 删除的顺序似乎依赖于平台。
// 我们强制清理向量的顺序是相反的。
for (int i = cleanup_list_.size() - 1; i >= 0; --i) {
cleanup_list_[i].reset();
}
}
/**
* 当应用程序中有多个模型时,为了调试或记录,给它们命名是有意义的。
*/
explicit Model(std::string name) : name_(name) {
}
/**
* 这使得在客户端上有一个更好的API,并且允许两种形式:
* - ConstraintCreationFunction(constraint_args, &model);
* - model.Add(ConstraintCreationFunction(constraint_args));
*
* 第二种形式对客户端来说更好,而且还可以存储约束并以后添加它们。然而,创建约束的函数稍微复杂一些。
*
* \code
std::function<void(Model*)> ConstraintCreationFunction(constraint_args) {
return [=] (Model* model) {
... 相同的代码 ...
};
}
\endcode
*
* 对于需要的函数,我们还有一个带模板返回值的版本,例如
* \code
const BooleanVariable b = model.Add(NewBooleanVariable());
const IntegerVariable i = model.Add(NewWeightedSum(weights, variables));
\endcode
*/
template <typename T>
T Add(std::function<T(Model*)> f) {
return f(this);
}
/// 类似于Add(),但是这个是const的。
template <typename T>
T Get(std::function<T(const Model&)> f) const {
return f(*this);
}
/**
* 返回一个类型为T的对象,该对象对该模型是唯一的(类似于“本地”单例)。
* 如果需要,使用T(Model* model)构造函数创建一个已经存在的实例,
* 否则使用T()构造函数创建一个新实例。
*
* 这在依赖注入框架中的工作方式上有点像,并且可以非常容易地将组成求解器的所有类连接在一起。
* 例如,一个约束可以依赖于LiteralTrail或IntegerTrail或两者,
* 它可以依赖于Watcher类来注册自己以便在需要时调用等等。
*
* 重要提示:Model*构造函数之间不应形成循环,否则这将导致程序崩溃。
*/
template <typename T>
T* GetOrCreate() {
const size_t type_id = gtl::FastTypeId<T>();
auto find = singletons_.find(type_id);
if (find != singletons_.end()) {
return static_cast<T*>(find->second);
}
// 新元素。
// 计划(user):直接在singletons_中存储std::unique_ptr<>?
T* new_t = MyNew<T>(0);
singletons_[type_id] = new_t;
TakeOwnership(new_t);
return new_t;
}
/**
* 类似于GetOrCreate(),但是如果对象不存在,则不创建该对象。
*
* 返回对象的const版本。
*/
template <typename T>
const T* Get() const {
const auto& it = singletons_.find(gtl::FastTypeId<T>());
return it != singletons_.end() ? static_cast<const T*>(it->second)
: nullptr;
}
/**
* 与Get()相同,但返回对象的可变版本。
*/
template <typename T>
T* Mutable() const {
const auto& it = singletons_.find(gtl::FastTypeId<T>());
return it != singletons_.end() ? static_cast<T*>(it->second) : nullptr;
}
/**
* 将指针所有权交给此模型。
*
* 当模型销毁时,它将被销毁。
*/
template <typename T>
T* TakeOwnership(T* t) {
cleanup_list_.emplace_back(new Delete<T>(t));
return t;
}
/**
* 这返回一个由模型拥有且使用T(Model* model)构造函数创建的非单例对象,
* 如果不存在则使用T()构造函数。这只是new + TakeOwnership()的快捷方式。
*/
template <typename T>
T* Create() {
T* new_t = MyNew<T>(0);
TakeOwnership(new_t);
return new_t;
}
/**
* 注册一个模型中将是“单例”的非拥有类。
*
* 在已经注册的类上调用此方法是错误的。
*/
template <typename T>
void Register(T* non_owned_class) {
const size_t type_id = gtl::FastTypeId<T>();
CHECK(!singletons_.contains(type_id));
singletons_[type_id] = non_owned_class;
}
const std::string& Name() const {
return name_; }
private:
// 如果在decltype()中的类型存在,则只定义第一个MyNew()。
// 第二个MyNew()将始终被定义,但由于省略号的原因,它的优先级低于第一个MyNew()。
template <typename T>
decltype(T(static_cast<Model*>(nullptr)))* MyNew(int) {
return new T(this);
}
template <typename T>
T* MyNew(...) {
return new T();
}
const std::string name_;
// FastTypeId<T>到类型为T的“单例”的映射。
absl::flat_hash_map</*typeid*/ size_t, void*> singletons_;
struct DeleteInterface {
virtual ~DeleteInterface() = default;
};
template <typename T>
class Delete : public DeleteInterface {
public:
explicit Delete(T* t) : to_delete_(t) {
}
~Delete() override = default;
private:
std::unique_ptr<T> to_delete_;
};
// 要删除的项目列表。
//
// 计划(user):我认为我们不需要两层unique_ptr,但是我们对效率并不太在意,
// 并且这更容易工作。
std::vector<std::unique_ptr<DeleteInterface>> cleanup_list_;
DISALLOW_COPY_AND_ASSIGN(Model);
};
} // namespace sat
} // namespace operations_research
#endif // OR_TOOLS_SAT_MODEL_H_
2.SolveCpModel函数
功能
这段代码是一个求解CP模型的函数。首先,它初始化了一些计时器,并检查是否需要输出初始模型和覆盖参数。
接下来,它启用了日志记录组件,并根据参数设置是否将日志记录到标准输出和响应中。
然后,它创建了一个共享的响应管理器,并设置了响应的转储前缀。
之后,它添加了一些后处理器和响应后处理器,用于在求解完成后执行一些操作,如转储响应到文件和记录时间信息。
接着,它验证了参数的有效性,如果参数无效,则返回一个包含错误信息的响应。
然后,它从参数中初始化了时间限制。
接下来,它注册了SIGINT信号处理程序,以便在收到SIGINT信号时停止求解。
然后,它输出一些求解器的信息和参数。
接着,它更新了参数中的工作线程数。
然后,它检查是否需要打印随机数生成器的salt。
接下来,它验证了模型的有效性,如果模型无效,则返回一个包含错误信息的响应。
最后,如果模型是纯SAT问题,则调用了另一个函数来求解纯SAT模型并返回结果。
整体来说,这段代码主要是对CP模型进行求解的一系列准备工作,包括参数设置、验证、日志记录等,最后调用函数求解模型并返回结果。
步骤
SolveCpModel
函数是用于求解Constraint Programming模型的主要函数。它执行以下步骤:
- 检查是否需要将预处理后的模型转储为文本文件,并进行转储。
- 检查是否需要在预处理之后停止求解,或者已达到时间限制。如果是,则返回当前状态作为最终响应。
- 加载模型并初始化目标域和主积分。
- 如果指定了完整解提示,测试其可行性并加载。
- 根据参数设置,检测并添加对称性约束。
- 根据平台选择多线程求解或顺序求解。
- 如果是多线程求解,调用
SolveCpModelParallel
函数进行并行求解。 - 否则,使用顺序求解:
- 创建一个局部模型,并将时间限制、SatParameters、统计信息和响应管理器注册到该模型中。
- 加载模型和调试解。
- 进行快速求解或最小化L1距离(根据参数设置)。
- 调用
SolveLoadedCpModel
函数进行求解。 - 填充响应中的统计信息。
- 如果启用日志记录,顺序记录线性规划约束的统计信息。
- 如果是多线程求解,调用
- 如果启用日志记录,记录共享统计信息。
- 返回最终响应。
中文源码分析
CpSolverResponse SolveCpModel(const CpModelProto& model_proto, Model* model) {
auto* wall_timer = model->GetOrCreate<WallTimer>();
auto* user_timer = model->GetOrCreate<UserTimer>();
wall_timer->Start();
user_timer->Start();
#if !defined(__PORTABLE_PLATFORM__)
#endif // __PORTABLE_PLATFORM__
#if !defined(__PORTABLE_PLATFORM__)
// 是否输出初始模型?
if (absl::GetFlag(FLAGS_cp_model_dump_models)) {
const std::string file =
absl::StrCat(absl::GetFlag(FLAGS_cp_model_dump_prefix)