Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
matrix_scaler.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 <algorithm>
17#include <cmath>
18#include <cstdlib>
19#include <memory>
20#include <string>
21#include <vector>
22
23#include "absl/log/check.h"
24#include "absl/strings/str_format.h"
27#include "ortools/glop/parameters.pb.h"
29#include "ortools/glop/status.h"
37
38namespace operations_research {
39namespace glop {
40
42 : matrix_(nullptr), row_scales_(), col_scales_() {}
43
45 DCHECK(matrix != nullptr);
46 matrix_ = matrix;
47 row_scales_.resize(matrix_->num_rows(), 1.0);
48 col_scales_.resize(matrix_->num_cols(), 1.0);
49}
50
52 matrix_ = nullptr;
53 row_scales_.clear();
54 col_scales_.clear();
55}
56
58 DCHECK_GE(row, 0);
59 return row < row_scales_.size() ? row_scales_[row] : 1.0;
60}
61
63 DCHECK_GE(col, 0);
64 return col < col_scales_.size() ? col_scales_[col] : 1.0;
65}
66
70
74
75std::string SparseMatrixScaler::DebugInformationString() const {
76 // Note that some computations are redundant with the computations made in
77 // some callees, but we do not care as this function is supposed to be called
78 // with FLAGS_v set to 1.
79 DCHECK(!row_scales_.empty());
80 DCHECK(!col_scales_.empty());
81 Fractional max_magnitude;
82 Fractional min_magnitude;
83 matrix_->ComputeMinAndMaxMagnitudes(&min_magnitude, &max_magnitude);
84 const Fractional dynamic_range = max_magnitude / min_magnitude;
85 std::string output = absl::StrFormat(
86 "Min magnitude = %g, max magnitude = %g\n"
87 "Dynamic range = %g\n"
88 "Variance = %g\n"
89 "Minimum row scale = %g, maximum row scale = %g\n"
90 "Minimum col scale = %g, maximum col scale = %g\n",
91 min_magnitude, max_magnitude, dynamic_range,
92 VarianceOfAbsoluteValueOfNonZeros(),
93 *std::min_element(row_scales_.begin(), row_scales_.end()),
94 *std::max_element(row_scales_.begin(), row_scales_.end()),
95 *std::min_element(col_scales_.begin(), col_scales_.end()),
96 *std::max_element(col_scales_.begin(), col_scales_.end()));
97 return output;
98}
99
100void SparseMatrixScaler::Scale(GlopParameters::ScalingAlgorithm method) {
101 // This is an implementation of the algorithm described in
102 // Benichou, M., Gauthier, J-M., Hentges, G., and Ribiere, G.,
103 // "The efficient solution of large-scale linear programming problems —
104 // some algorithmic techniques and computational results,"
105 // Mathematical Programming 13(3) (December 1977).
106 // http://www.springerlink.com/content/j3367676856m0064/
107 DCHECK(matrix_ != nullptr);
108 Fractional max_magnitude;
109 Fractional min_magnitude;
110 matrix_->ComputeMinAndMaxMagnitudes(&min_magnitude, &max_magnitude);
111 if (min_magnitude == 0.0) {
112 DCHECK_EQ(0.0, max_magnitude);
113 return; // Null matrix: nothing to do.
114 }
115 VLOG(1) << "Before scaling:\n" << DebugInformationString();
116 if (method == GlopParameters::LINEAR_PROGRAM) {
117 Status lp_status = LPScale();
118 // Revert to the default scaling method if there is an error with the LP.
119 if (lp_status.ok()) {
120 return;
121 } else {
122 VLOG(1) << "Error with LP scaling: " << lp_status.error_message();
123 }
124 }
125 // TODO(user): Decide precisely for which value of dynamic range we should cut
126 // off geometric scaling.
127 const Fractional dynamic_range = max_magnitude / min_magnitude;
128 const Fractional kMaxDynamicRangeForGeometricScaling = 1e20;
129 if (dynamic_range < kMaxDynamicRangeForGeometricScaling) {
130 const int kScalingIterations = 4;
131 const Fractional kVarianceThreshold(10.0);
132 for (int iteration = 0; iteration < kScalingIterations; ++iteration) {
133 const RowIndex num_rows_scaled = ScaleRowsGeometrically();
134 const ColIndex num_cols_scaled = ScaleColumnsGeometrically();
135 const Fractional variance = VarianceOfAbsoluteValueOfNonZeros();
136 VLOG(1) << "Geometric scaling iteration " << iteration
137 << ". Rows scaled = " << num_rows_scaled
138 << ", columns scaled = " << num_cols_scaled << "\n";
139 VLOG(1) << DebugInformationString();
140 if (variance < kVarianceThreshold ||
141 (num_cols_scaled == 0 && num_rows_scaled == 0)) {
142 break;
143 }
144 }
145 }
146 const RowIndex rows_equilibrated = EquilibrateRows();
147 const ColIndex cols_equilibrated = EquilibrateColumns();
148 VLOG(1) << "Equilibration step: Rows scaled = " << rows_equilibrated
149 << ", columns scaled = " << cols_equilibrated << "\n";
150 VLOG(1) << DebugInformationString();
151}
152
153namespace {
154template <class I>
155void ScaleVector(const util_intops::StrongVector<I, Fractional>& scale, bool up,
157 RETURN_IF_NULL(vector_to_scale);
158 const I size(std::min(scale.size(), vector_to_scale->size()));
159 if (up) {
160 for (I i(0); i < size; ++i) {
161 (*vector_to_scale)[i] *= scale[i];
162 }
163 } else {
164 for (I i(0); i < size; ++i) {
165 (*vector_to_scale)[i] /= scale[i];
166 }
167 }
168}
169
170template <typename InputIndexType>
171ColIndex CreateOrGetScaleIndex(
172 InputIndexType num, LinearProgram* lp,
174 if ((*scale_var_indices)[num] == -1) {
175 (*scale_var_indices)[num] = lp->CreateNewVariable();
176 }
177 return (*scale_var_indices)[num];
178}
179} // anonymous namespace
180
181void SparseMatrixScaler::ScaleRowVector(bool up, DenseRow* row_vector) const {
182 DCHECK(row_vector != nullptr);
183 ScaleVector(col_scales_, up, row_vector);
184}
185
187 DenseColumn* column_vector) const {
188 DCHECK(column_vector != nullptr);
189 ScaleVector(row_scales_, up, column_vector);
190}
191
192Fractional SparseMatrixScaler::VarianceOfAbsoluteValueOfNonZeros() const {
193 DCHECK(matrix_ != nullptr);
194 Fractional sigma_square(0.0);
195 Fractional sigma_abs(0.0);
196 double n = 0.0; // n is used in a calculation involving doubles.
197 const ColIndex num_cols = matrix_->num_cols();
198 for (ColIndex col(0); col < num_cols; ++col) {
199 for (const SparseColumn::Entry e : matrix_->column(col)) {
200 const Fractional magnitude = fabs(e.coefficient());
201 sigma_square += magnitude * magnitude;
202 sigma_abs += magnitude;
203 ++n;
204 }
205 }
206 if (n == 0.0) return 0.0;
207
208 // Since we know all the population (the non-zeros) and we are not using a
209 // sample, the variance is defined as below.
210 // For an explanation, see:
211 // http://en.wikipedia.org/wiki/Variance
212 // #Population_variance_and_sample_variance
213 return (sigma_square - sigma_abs * sigma_abs / n) / n;
214}
215
216// For geometric scaling, we compute the maximum and minimum magnitudes
217// of non-zeros in a row (resp. column). Let us denote these numbers as
218// max and min. We then scale the row (resp. column) by dividing the
219// coefficients by sqrt(min * max).
220
221RowIndex SparseMatrixScaler::ScaleRowsGeometrically() {
222 DCHECK(matrix_ != nullptr);
223 DenseColumn max_in_row(matrix_->num_rows(), 0.0);
224 DenseColumn min_in_row(matrix_->num_rows(), kInfinity);
225 const ColIndex num_cols = matrix_->num_cols();
226 for (ColIndex col(0); col < num_cols; ++col) {
227 for (const SparseColumn::Entry e : matrix_->column(col)) {
228 const Fractional magnitude = fabs(e.coefficient());
229 const RowIndex row = e.row();
230 if (magnitude != 0.0) {
231 max_in_row[row] = std::max(max_in_row[row], magnitude);
232 min_in_row[row] = std::min(min_in_row[row], magnitude);
233 }
234 }
235 }
236 const RowIndex num_rows = matrix_->num_rows();
237 DenseColumn scaling_factor(num_rows, 0.0);
238 for (RowIndex row(0); row < num_rows; ++row) {
239 if (max_in_row[row] == 0.0) {
240 scaling_factor[row] = 1.0;
241 } else {
242 DCHECK_NE(kInfinity, min_in_row[row]);
243 scaling_factor[row] = std::sqrt(max_in_row[row] * min_in_row[row]);
244 }
245 }
246 return ScaleMatrixRows(scaling_factor);
247}
248
249ColIndex SparseMatrixScaler::ScaleColumnsGeometrically() {
250 DCHECK(matrix_ != nullptr);
251 ColIndex num_cols_scaled(0);
252 const ColIndex num_cols = matrix_->num_cols();
253 for (ColIndex col(0); col < num_cols; ++col) {
254 Fractional max_in_col(0.0);
255 Fractional min_in_col(kInfinity);
256 for (const SparseColumn::Entry e : matrix_->column(col)) {
257 const Fractional magnitude = fabs(e.coefficient());
258 if (magnitude != 0.0) {
259 max_in_col = std::max(max_in_col, magnitude);
260 min_in_col = std::min(min_in_col, magnitude);
261 }
262 }
263 if (max_in_col != 0.0) {
264 const Fractional factor(std::sqrt(ToDouble(max_in_col * min_in_col)));
265 ScaleMatrixColumn(col, factor);
266 num_cols_scaled++;
267 }
268 }
269 return num_cols_scaled;
270}
271
272// For equilibration, we compute the maximum magnitude of non-zeros
273// in a row (resp. column), and then scale the row (resp. column) by dividing
274// the coefficients this maximum magnitude.
275// This brings the largest coefficient in a row equal to 1.0.
276
277RowIndex SparseMatrixScaler::EquilibrateRows() {
278 DCHECK(matrix_ != nullptr);
279 const RowIndex num_rows = matrix_->num_rows();
280 DenseColumn max_magnitudes(num_rows, 0.0);
281 const ColIndex num_cols = matrix_->num_cols();
282 for (ColIndex col(0); col < num_cols; ++col) {
283 for (const SparseColumn::Entry e : matrix_->column(col)) {
284 const Fractional magnitude = fabs(e.coefficient());
285 if (magnitude != 0.0) {
286 const RowIndex row = e.row();
287 max_magnitudes[row] = std::max(max_magnitudes[row], magnitude);
288 }
289 }
290 }
291 for (RowIndex row(0); row < num_rows; ++row) {
292 if (max_magnitudes[row] == 0.0) {
293 max_magnitudes[row] = 1.0;
294 }
295 }
296 return ScaleMatrixRows(max_magnitudes);
297}
298
299ColIndex SparseMatrixScaler::EquilibrateColumns() {
300 DCHECK(matrix_ != nullptr);
301 ColIndex num_cols_scaled(0);
302 const ColIndex num_cols = matrix_->num_cols();
303 for (ColIndex col(0); col < num_cols; ++col) {
304 const Fractional max_magnitude = InfinityNorm(matrix_->column(col));
305 if (max_magnitude != 0.0 && max_magnitude != 1.0) {
306 ScaleMatrixColumn(col, max_magnitude);
307 num_cols_scaled++;
308 }
309 }
310 return num_cols_scaled;
311}
312
313RowIndex SparseMatrixScaler::ScaleMatrixRows(const DenseColumn& factors) {
314 // Matrix rows are scaled by dividing their coefficients by factors[row].
315 DCHECK(matrix_ != nullptr);
316 const RowIndex num_rows = matrix_->num_rows();
317 DCHECK_EQ(num_rows, factors.size());
318 RowIndex num_rows_scaled(0);
319 for (RowIndex row(0); row < num_rows; ++row) {
320 const Fractional factor = factors[row];
321 DCHECK_NE(0.0, factor);
322 if (factor != 1.0) {
323 ++num_rows_scaled;
324 row_scales_[row] *= factor;
325 }
326 }
327
328 const ColIndex num_cols = matrix_->num_cols();
329 for (ColIndex col(0); col < num_cols; ++col) {
330 matrix_->mutable_column(col)->ComponentWiseDivide(factors);
331 }
332
333 return num_rows_scaled;
334}
335
336void SparseMatrixScaler::ScaleMatrixColumn(ColIndex col, Fractional factor) {
337 // A column is scaled by dividing by factor.
338 DCHECK(matrix_ != nullptr);
339 DCHECK_NE(0.0, factor);
340 col_scales_[col] *= factor;
341 matrix_->mutable_column(col)->DivideByConstant(factor);
342}
343
344Status SparseMatrixScaler::LPScale() {
345 DCHECK(matrix_ != nullptr);
346
347 auto linear_program = std::make_unique<LinearProgram>();
348 GlopParameters params;
349 auto simplex = std::make_unique<RevisedSimplex>();
350 simplex->SetParameters(params);
351
352 // Begin linear program construction.
353 // Beta represents the largest distance from zero among the constraint pairs.
354 // It resembles a slack variable because the 'objective' of each constraint is
355 // to cancel out the log "w" of the original nonzero |a_ij| (a.k.a. |a_rc|).
356 // Approaching 0 by addition in log space is the same as approaching 1 by
357 // multiplication in linear space. Hence, each variable's log magnitude is
358 // subtracted from the log row scale and log column scale. If the sum is
359 // positive, the positive constraint is trivially satisfied, but the negative
360 // constraint will determine the minimum necessary value of beta for that
361 // variable and scaling factors, and vice versa.
362 // For an MxN matrix, the resulting scaling LP has M+N+1 variables and
363 // O(M*N) constraints (2*M*N at maximum). As a result, using this LP to scale
364 // another linear program, will typically increase the time to
365 // optimization by a factor of 4, and has increased the time of some benchmark
366 // LPs by up to 10.
367
368 // Indices to variables in the LinearProgram populated by
369 // GenerateLinearProgram.
372 row_scale_var_indices.resize(RowToIntIndex(matrix_->num_rows()), kInvalidCol);
373 col_scale_var_indices.resize(ColToIntIndex(matrix_->num_cols()), kInvalidCol);
374 const ColIndex beta = linear_program->CreateNewVariable();
375 linear_program->SetVariableBounds(beta, -kInfinity, kInfinity);
376 // Default objective is to minimize.
377 linear_program->SetObjectiveCoefficient(beta, 1);
378 matrix_->CleanUp();
379 const ColIndex num_cols = matrix_->num_cols();
380 for (ColIndex col(0); col < num_cols; ++col) {
381 // This is the variable representing the log of the scale factor for col.
382 const ColIndex column_scale = CreateOrGetScaleIndex<ColIndex>(
383 col, linear_program.get(), &col_scale_var_indices);
384 linear_program->SetVariableBounds(column_scale, -kInfinity, kInfinity);
385 for (const SparseColumn::Entry e : matrix_->column(col)) {
386 const Fractional log_magnitude = log2(std::abs(e.coefficient()));
387 const RowIndex row = e.row();
388
389 // This is the variable representing the log of the scale factor for row.
390 const ColIndex row_scale = CreateOrGetScaleIndex<RowIndex>(
391 row, linear_program.get(), &row_scale_var_indices);
392
393 linear_program->SetVariableBounds(row_scale, -kInfinity, kInfinity);
394 // clang-format off
395 // This is derived from the formulation in
396 // min β
397 // Subject to:
398 // ∀ c∈C, v∈V, p_{c,v} ≠ 0.0, w_{c,v} + s^{var}_v + s^{comb}_c + β ≥ 0.0
399 // ∀ c∈C, v∈V, p_{c,v} ≠ 0.0, w_{c,v} + s^{var}_v + s^{comb}_c ≤ β
400 // If a variable is integer, its scale factor is zero.
401 // clang-format on
402
403 // Start with the constraint w_cv + s_c + s_v + beta >= 0.
404 const RowIndex positive_constraint =
405 linear_program->CreateNewConstraint();
406 // Subtract the constant w_cv from both sides.
407 linear_program->SetConstraintBounds(positive_constraint, -log_magnitude,
408 kInfinity);
409 // +s_c, meaning (log) scale of the constraint C, pointed by row_scale.
410 linear_program->SetCoefficient(positive_constraint, row_scale, 1);
411 // +s_v, meaning (log) scale of the variable V, pointed by column_scale.
412 linear_program->SetCoefficient(positive_constraint, column_scale, 1);
413 // +beta
414 linear_program->SetCoefficient(positive_constraint, beta, 1);
415
416 // Construct the constraint w_cv + s_c + s_v <= beta.
417 const RowIndex negative_constraint =
418 linear_program->CreateNewConstraint();
419 // Subtract w (and beta) from both sides.
420 linear_program->SetConstraintBounds(negative_constraint, -kInfinity,
421 -log_magnitude);
422 // +s_c, meaning (log) scale of the constraint C, pointed by row_scale.
423 linear_program->SetCoefficient(negative_constraint, row_scale, 1);
424 // +s_v, meaning (log) scale of the variable V, pointed by column_scale.
425 linear_program->SetCoefficient(negative_constraint, column_scale, 1);
426 // -beta
427 linear_program->SetCoefficient(negative_constraint, beta, -1);
428 }
429 }
430 // End linear program construction.
431
432 linear_program->AddSlackVariablesWhereNecessary(false);
433 const Status simplex_status =
434 simplex->Solve(*linear_program, TimeLimit::Infinite().get());
435 if (!simplex_status.ok()) {
436 return simplex_status;
437 } else {
438 // Now the solution variables can be interpreted and translated from log
439 // space.
440 // For each row scale, unlog it and scale the constraints and constraint
441 // bounds.
442 const ColIndex num_cols = matrix_->num_cols();
443 for (ColIndex col(0); col < num_cols; ++col) {
444 const Fractional column_scale =
445 exp2(-simplex->GetVariableValue(CreateOrGetScaleIndex<ColIndex>(
446 col, linear_program.get(), &col_scale_var_indices)));
447 ScaleMatrixColumn(col, column_scale);
448 }
449 const RowIndex num_rows = matrix_->num_rows();
450 DenseColumn row_scale(num_rows, 0.0);
451 for (RowIndex row(0); row < num_rows; ++row) {
452 row_scale[row] =
453 exp2(-simplex->GetVariableValue(CreateOrGetScaleIndex<RowIndex>(
454 row, linear_program.get(), &row_scale_var_indices)));
455 }
456 ScaleMatrixRows(row_scale);
457 return Status::OK();
458 }
459}
460
461} // namespace glop
462} // namespace operations_research
IntegerValue size
static std::unique_ptr< TimeLimit > Infinite()
Definition time_limit.h:119
void ScaleColumnVector(bool up, DenseColumn *column_vector) const
Fractional RowScalingFactor(RowIndex row) const
void ScaleRowVector(bool up, DenseRow *row_vector) const
Fractional ColScalingFactor(ColIndex col) const
Fractional RowUnscalingFactor(RowIndex row) const
void Scale(GlopParameters::ScalingAlgorithm method)
Scales the matrix.
Fractional ColUnscalingFactor(ColIndex col) const
void ComputeMinAndMaxMagnitudes(Fractional *min_magnitude, Fractional *max_magnitude) const
Definition sparse.cc:378
const SparseColumn & column(ColIndex col) const
Access the underlying sparse columns.
Definition sparse.h:194
SparseColumn * mutable_column(ColIndex col)
Definition sparse.h:195
RowIndex num_rows() const
Return the matrix dimension.
Definition sparse.h:190
void ComponentWiseDivide(const DenseVector &factors)
void DivideByConstant(Fractional factor)
static Status OK()
Improves readability but identical to 0-arg constructor.
Definition status.h:55
const std::string & error_message() const
Definition status.h:59
void resize(size_type new_size)
ColIndex col
Definition markowitz.cc:187
RowIndex row
Definition markowitz.cc:186
constexpr ColIndex kInvalidCol(-1)
Index ColToIntIndex(ColIndex col)
Get the integer index corresponding to the col.
Definition lp_types.h:60
Fractional InfinityNorm(const DenseColumn &v)
Returns the maximum of the coefficients of 'v'.
Definition lp_utils.cc:151
StrictITIVector< RowIndex, Fractional > DenseColumn
Column-vector types. Column-vector types are indexed by a row index.
Definition lp_types.h:382
Index RowToIntIndex(RowIndex row)
Get the integer index corresponding to the row.
Definition lp_types.h:63
static double ToDouble(double f)
Definition lp_types.h:74
In SWIG mode, we don't want anything besides these top-level includes.
#define RETURN_IF_NULL(x)