402 lines
14 KiB
C++
402 lines
14 KiB
C++
// Copyright 2010-2025 Google LLC
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
//
|
|
// This code loads flow-graph models (as Dimacs file or binary FlowModel proto)
|
|
// and solves them with the OR-tools flow algorithms.
|
|
//
|
|
// Note(user): only min-cost flow is supported at this point.
|
|
// TODO(user): move this DIMACS parser to its own class, like the ones in
|
|
// routing/. This change would improve searchability of the parser.
|
|
|
|
#include <algorithm>
|
|
#include <cstdint>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <functional>
|
|
#include <sstream>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "absl/base/log_severity.h"
|
|
#include "absl/flags/flag.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/log/globals.h"
|
|
#include "absl/log/log.h"
|
|
#include "absl/strings/match.h"
|
|
#include "absl/strings/str_format.h"
|
|
#include "absl/strings/string_view.h"
|
|
#include "ortools/base/filesystem.h"
|
|
#include "ortools/base/helpers.h"
|
|
#include "ortools/base/init_google.h"
|
|
#include "ortools/base/options.h"
|
|
#include "ortools/base/path.h"
|
|
#include "ortools/base/timer.h"
|
|
#include "ortools/graph/flow_graph.h"
|
|
#include "ortools/graph/flow_problem.pb.h"
|
|
#include "ortools/graph/generic_max_flow.h"
|
|
#include "ortools/graph/graph.h"
|
|
#include "ortools/graph/min_cost_flow.h"
|
|
#include "ortools/util/file_util.h"
|
|
#include "ortools/util/filelineiter.h"
|
|
#include "ortools/util/stats.h"
|
|
|
|
ABSL_FLAG(std::string, input, "", "Input file of the problem.");
|
|
ABSL_FLAG(std::string, output_dimacs, "", "Output problem as a dimacs file.");
|
|
ABSL_FLAG(std::string, output_proto, "", "Output problem as a flow proto.");
|
|
ABSL_FLAG(bool, use_flow_graph, true, "Use special kind of graph.");
|
|
ABSL_FLAG(bool, sort_heads, false, "Sort outgoing arcs by head.");
|
|
ABSL_FLAG(bool, detect_reverse_arcs, true, "Detect reverse arcs.");
|
|
|
|
namespace operations_research {
|
|
|
|
// See http://lpsolve.sourceforge.net/5.5/DIMACS_mcf.htm for the dimacs file
|
|
// format of a min cost flow problem.
|
|
//
|
|
// TODO(user): This currently only works for min cost flow problem.
|
|
void ConvertFlowModelToDimacs(const FlowModelProto& flow_model,
|
|
std::string* dimacs) {
|
|
CHECK_EQ(FlowModelProto::MIN_COST_FLOW, flow_model.problem_type());
|
|
dimacs->clear();
|
|
dimacs->append("c Min-Cost flow problem generated from a FlowModelProto.\n");
|
|
|
|
// We need to compute the num_nodes from the nodes appearing in the arcs.
|
|
int64_t num_nodes = 0;
|
|
for (int64_t i = 0; i < flow_model.arcs_size(); ++i) {
|
|
num_nodes = std::max(num_nodes, flow_model.arcs(i).tail() + 1);
|
|
num_nodes = std::max(num_nodes, flow_model.arcs(i).head() + 1);
|
|
}
|
|
|
|
// Problem size and type.
|
|
const int64_t num_arcs = flow_model.arcs_size();
|
|
dimacs->append("c\nc Problem line (problem_type, num nodes, num arcs).\n");
|
|
dimacs->append(absl::StrFormat("p min %d %d\n", num_nodes, num_arcs));
|
|
|
|
// Nodes.
|
|
dimacs->append("c\nc Node descriptor lines (id, supply/demand).\n");
|
|
for (int64_t i = 0; i < flow_model.nodes_size(); ++i) {
|
|
const int64_t id = flow_model.nodes(i).id() + 1;
|
|
const int64_t supply = flow_model.nodes(i).supply();
|
|
if (supply != 0) {
|
|
dimacs->append(absl::StrFormat("n %d %d\n", id, supply));
|
|
}
|
|
}
|
|
|
|
// Arcs.
|
|
dimacs->append(
|
|
"c\nc Arc descriptor Lines (tail, head, minflow, maxflow, cost).\n");
|
|
for (int64_t i = 0; i < flow_model.arcs_size(); ++i) {
|
|
const int64_t tail = flow_model.arcs(i).tail() + 1;
|
|
const int64_t head = flow_model.arcs(i).head() + 1;
|
|
const int64_t unit_cost = flow_model.arcs(i).unit_cost();
|
|
const int64_t capacity = flow_model.arcs(i).capacity();
|
|
dimacs->append(absl::StrFormat("a %d %d %d %d %d\n", tail, head, int64_t{0},
|
|
capacity, unit_cost));
|
|
}
|
|
dimacs->append("c\n");
|
|
}
|
|
|
|
// Note(user): Going from Dimacs to flow adds an extra copy, but for now we
|
|
// don't really care of the Dimacs file reading performance.
|
|
// Returns true if the file was converted correctly.
|
|
bool ConvertDimacsToFlowModel(absl::string_view file,
|
|
FlowModelProto* flow_model) {
|
|
flow_model->Clear();
|
|
FlowModelProto::ProblemType problem_type;
|
|
for (const std::string& line : FileLines(file)) {
|
|
if (line.empty()) continue;
|
|
if (line[0] == 'p') {
|
|
if (absl::StartsWith(line, "p min")) {
|
|
problem_type = FlowModelProto::MIN_COST_FLOW;
|
|
} else if (absl::StartsWith(line, "p max")) {
|
|
problem_type = FlowModelProto::MAX_FLOW;
|
|
} else {
|
|
LOG(ERROR) << "Unknown dimacs problem format.";
|
|
return false;
|
|
}
|
|
flow_model->set_problem_type(problem_type);
|
|
}
|
|
if (line[0] == 'n') {
|
|
int64_t id;
|
|
int64_t supply;
|
|
std::stringstream ss(line.substr(1));
|
|
switch (problem_type) {
|
|
case FlowModelProto::MIN_COST_FLOW:
|
|
ss >> id >> supply;
|
|
break;
|
|
case FlowModelProto::MAX_FLOW: {
|
|
std::string type;
|
|
ss >> id >> type;
|
|
supply = (type == "s" ? 1 : -1);
|
|
break;
|
|
}
|
|
default:
|
|
LOG(ERROR) << "Node line before the problem type definition.";
|
|
return false;
|
|
}
|
|
FlowNodeProto* node = flow_model->add_nodes();
|
|
node->set_id(id - 1);
|
|
node->set_supply(supply);
|
|
}
|
|
if (line[0] == 'a') {
|
|
int64_t tail;
|
|
int64_t head;
|
|
int64_t min_capacity = 0;
|
|
int64_t max_capacity = 0;
|
|
int64_t unit_cost = 0;
|
|
std::stringstream ss(line.substr(1));
|
|
switch (problem_type) {
|
|
case FlowModelProto::MIN_COST_FLOW:
|
|
ss >> tail >> head >> min_capacity >> max_capacity >> unit_cost;
|
|
break;
|
|
case FlowModelProto::MAX_FLOW:
|
|
ss >> tail >> head >> max_capacity;
|
|
break;
|
|
default:
|
|
LOG(ERROR) << "Arc line before the problem type definition.";
|
|
return false;
|
|
}
|
|
FlowArcProto* arc = flow_model->add_arcs();
|
|
arc->set_tail(tail - 1);
|
|
arc->set_head(head - 1);
|
|
arc->set_capacity(max_capacity);
|
|
arc->set_unit_cost(unit_cost);
|
|
if (min_capacity != 0) {
|
|
LOG(ERROR) << "We do not support minimum capacity.";
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Type of graph to use.
|
|
typedef util::ReverseArcStaticGraph<> Graph;
|
|
|
|
// Loads a FlowModelProto proto into the MinCostFlow class and solves it.
|
|
void SolveMinCostFlow(const FlowModelProto& flow_model, double* loading_time,
|
|
double* solving_time) {
|
|
WallTimer timer;
|
|
timer.Start();
|
|
|
|
// Compute the number of nodes.
|
|
int64_t num_nodes = 0;
|
|
for (int i = 0; i < flow_model.arcs_size(); ++i) {
|
|
num_nodes = std::max(num_nodes, flow_model.arcs(i).tail() + 1);
|
|
num_nodes = std::max(num_nodes, flow_model.arcs(i).head() + 1);
|
|
}
|
|
|
|
// Build the graph.
|
|
Graph graph(num_nodes, flow_model.arcs_size());
|
|
for (int i = 0; i < flow_model.arcs_size(); ++i) {
|
|
graph.AddArc(flow_model.arcs(i).tail(), flow_model.arcs(i).head());
|
|
}
|
|
std::vector<Graph::ArcIndex> permutation;
|
|
graph.Build(&permutation);
|
|
absl::PrintF("%d,", graph.num_nodes());
|
|
absl::PrintF("%d,", graph.num_arcs());
|
|
|
|
GenericMinCostFlow<Graph> min_cost_flow(&graph);
|
|
for (int i = 0; i < flow_model.arcs_size(); ++i) {
|
|
const Graph::ArcIndex image = i < permutation.size() ? permutation[i] : i;
|
|
min_cost_flow.SetArcUnitCost(image, flow_model.arcs(i).unit_cost());
|
|
min_cost_flow.SetArcCapacity(image, flow_model.arcs(i).capacity());
|
|
}
|
|
for (int i = 0; i < flow_model.nodes_size(); ++i) {
|
|
min_cost_flow.SetNodeSupply(flow_model.nodes(i).id(),
|
|
flow_model.nodes(i).supply());
|
|
}
|
|
|
|
*loading_time = timer.Get();
|
|
absl::PrintF("%f,", *loading_time);
|
|
fflush(stdout);
|
|
|
|
timer.Start();
|
|
CHECK(min_cost_flow.Solve());
|
|
CHECK_EQ(GenericMinCostFlow<Graph>::OPTIMAL, min_cost_flow.status());
|
|
*solving_time = timer.Get();
|
|
absl::PrintF("%f,", *solving_time);
|
|
absl::PrintF("%d", min_cost_flow.GetOptimalCost());
|
|
fflush(stdout);
|
|
}
|
|
|
|
// Loads a FlowModelProto proto into the MaxFlow class and solves it.
|
|
template <typename GraphType>
|
|
void SolveMaxFlow(
|
|
const FlowModelProto& flow_model, double* loading_time,
|
|
double* solving_time,
|
|
std::function<void(GraphType* graph)> configure_graph_options = nullptr) {
|
|
WallTimer timer;
|
|
timer.Start();
|
|
|
|
// Build the graph.
|
|
GraphType graph(flow_model.nodes_size(), flow_model.arcs_size());
|
|
for (int i = 0; i < flow_model.arcs_size(); ++i) {
|
|
graph.AddArc(flow_model.arcs(i).tail(), flow_model.arcs(i).head());
|
|
}
|
|
std::vector<typename GraphType::ArcIndex> permutation;
|
|
|
|
if (configure_graph_options != nullptr) {
|
|
configure_graph_options(&graph);
|
|
}
|
|
graph.Build(&permutation);
|
|
|
|
absl::PrintF("%d,", graph.num_nodes());
|
|
absl::PrintF("%d,", graph.num_arcs());
|
|
|
|
// Find source & sink.
|
|
typename GraphType::NodeIndex source = -1;
|
|
typename GraphType::NodeIndex sink = -1;
|
|
CHECK_EQ(2, flow_model.nodes_size());
|
|
for (int i = 0; i < flow_model.nodes_size(); ++i) {
|
|
if (flow_model.nodes(i).supply() > 0) {
|
|
source = flow_model.nodes(i).id();
|
|
}
|
|
if (flow_model.nodes(i).supply() < 0) {
|
|
sink = flow_model.nodes(i).id();
|
|
}
|
|
}
|
|
CHECK_NE(source, -1);
|
|
CHECK_NE(sink, -1);
|
|
|
|
// Create the max flow instance and set the arc capacities.
|
|
GenericMaxFlow<GraphType> max_flow(&graph, source, sink);
|
|
for (int i = 0; i < flow_model.arcs_size(); ++i) {
|
|
const typename GraphType::ArcIndex image =
|
|
i < permutation.size() ? permutation[i] : i;
|
|
max_flow.SetArcCapacity(image, flow_model.arcs(i).capacity());
|
|
}
|
|
|
|
*loading_time = timer.Get();
|
|
absl::PrintF("%f,", *loading_time);
|
|
fflush(stdout);
|
|
|
|
timer.Start();
|
|
CHECK(max_flow.Solve());
|
|
CHECK_EQ(GenericMaxFlow<GraphType>::OPTIMAL, max_flow.status());
|
|
*solving_time = timer.Get();
|
|
absl::PrintF("%f,", *solving_time);
|
|
absl::PrintF("%d", max_flow.GetOptimalFlow());
|
|
fflush(stdout);
|
|
}
|
|
|
|
} // namespace operations_research
|
|
|
|
using operations_research::FlowModelProto;
|
|
using operations_research::SolveMaxFlow;
|
|
using operations_research::SolveMinCostFlow;
|
|
using operations_research::TimeDistribution;
|
|
|
|
int main(int argc, char** argv) {
|
|
InitGoogle(
|
|
"Runs the OR-tools min-cost flow on a pattern of files given by --input. "
|
|
"The files must be in Dimacs text format or in binary FlowModelProto "
|
|
"format.",
|
|
&argc, &argv, true);
|
|
absl::SetStderrThreshold(absl::LogSeverityAtLeast::kInfo);
|
|
if (absl::GetFlag(FLAGS_input).empty()) {
|
|
LOG(FATAL) << "Please specify input pattern via --input=...";
|
|
}
|
|
std::vector<std::string> file_list;
|
|
file::Match(absl::GetFlag(FLAGS_input), &file_list, file::Defaults())
|
|
.IgnoreError();
|
|
|
|
TimeDistribution parsing_time_distribution("Parsing time summary");
|
|
TimeDistribution loading_time_distribution("Loading time summary");
|
|
TimeDistribution solving_time_distribution("Solving time summary");
|
|
|
|
absl::PrintF(
|
|
"file_name, parsing_time, num_nodes, num_arcs,loading_time, "
|
|
"solving_time, optimal_cost\n");
|
|
for (int i = 0; i < file_list.size(); ++i) {
|
|
const std::string file_name = file_list[i];
|
|
absl::PrintF("%s,", file::Basename(file_name));
|
|
fflush(stdout);
|
|
|
|
// Parse the input as a proto.
|
|
double parsing_time = 0;
|
|
operations_research::FlowModelProto proto;
|
|
if (absl::EndsWith(file_name, ".bin") ||
|
|
absl::EndsWith(file_name, ".bin.gz")) {
|
|
ScopedWallTime timer(&parsing_time);
|
|
if (absl::EndsWith(file_name, "gz")) {
|
|
std::string raw_data;
|
|
CHECK_OK(file::GetContents(file_name, &raw_data, file::Defaults()));
|
|
CHECK_OK(StringToProto(raw_data, &proto));
|
|
} else {
|
|
CHECK_OK(file::GetBinaryProto(file_name, &proto, file::Defaults()));
|
|
}
|
|
} else {
|
|
ScopedWallTime timer(&parsing_time);
|
|
if (!ConvertDimacsToFlowModel(file_name, &proto)) continue;
|
|
}
|
|
absl::PrintF("%f,", parsing_time);
|
|
fflush(stdout);
|
|
|
|
if (!absl::GetFlag(FLAGS_output_proto).empty()) {
|
|
LOG(INFO) << "Dumping binary proto to '"
|
|
<< absl::GetFlag(FLAGS_output_proto) << "'.";
|
|
CHECK_OK(file::SetBinaryProto(absl::GetFlag(FLAGS_output_proto), proto,
|
|
file::Defaults()));
|
|
}
|
|
|
|
// TODO(user): improve code to convert many files.
|
|
if (!absl::GetFlag(FLAGS_output_dimacs).empty()) {
|
|
LOG(INFO) << "Converting first input file to dimacs format.";
|
|
double time = 0;
|
|
{
|
|
ScopedWallTime timer(&time);
|
|
std::string dimacs;
|
|
ConvertFlowModelToDimacs(proto, &dimacs);
|
|
CHECK_OK(file::SetContents(absl::GetFlag(FLAGS_output_dimacs), dimacs,
|
|
file::Defaults()));
|
|
}
|
|
LOG(INFO) << "Done: " << time << "s.";
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
double loading_time = 0;
|
|
double solving_time = 0;
|
|
switch (proto.problem_type()) {
|
|
case FlowModelProto::MIN_COST_FLOW:
|
|
SolveMinCostFlow(proto, &loading_time, &solving_time);
|
|
break;
|
|
case FlowModelProto::MAX_FLOW:
|
|
if (absl::GetFlag(FLAGS_use_flow_graph)) {
|
|
SolveMaxFlow<util::FlowGraph<>>(
|
|
proto, &loading_time, &solving_time,
|
|
[](util::FlowGraph<>* graph) {
|
|
graph->SetDetectReverse(
|
|
absl::GetFlag(FLAGS_detect_reverse_arcs));
|
|
graph->SetSortByHead(absl::GetFlag(FLAGS_sort_heads));
|
|
});
|
|
} else {
|
|
SolveMaxFlow<util::ReverseArcStaticGraph<>>(proto, &loading_time,
|
|
&solving_time);
|
|
}
|
|
break;
|
|
default:
|
|
LOG(ERROR) << "Problem type not supported: " << proto.problem_type();
|
|
}
|
|
absl::PrintF("\n");
|
|
|
|
parsing_time_distribution.AddTimeInSec(parsing_time);
|
|
loading_time_distribution.AddTimeInSec(loading_time);
|
|
solving_time_distribution.AddTimeInSec(solving_time);
|
|
}
|
|
absl::PrintF("%s", parsing_time_distribution.StatString());
|
|
absl::PrintF("%s", loading_time_distribution.StatString());
|
|
absl::PrintF("%s", solving_time_distribution.StatString());
|
|
return EXIT_SUCCESS;
|
|
}
|