Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
markowitz.h
Go to the documentation of this file.
1// Copyright 2010-2025 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
14// LU decomposition algorithm of a sparse matrix B with Markowitz pivot
15// selection strategy. The algorithm constructs a lower matrix L, upper matrix
16// U, row permutation P and a column permutation Q such that L.U = P.B.Q^{-1}.
17//
18// The current algorithm is a mix of ideas that can be found in the literature
19// and of some optimizations tailored for its use in a revised simplex algorithm
20// (like a fast processing of the singleton columns present in B). It constructs
21// L and U column by column from left to right.
22//
23// A key concept is the one of the residual matrix which is the bottom right
24// square submatrix that still needs to be factorized during the classical
25// Gaussian elimination. The algorithm maintains the non-zero pattern of its
26// rows and its row/column degrees.
27//
28// At each step, a number of columns equal to 'markowitz_zlatev_parameter' are
29// chosen as candidates from the residual matrix. They are the ones with minimal
30// residual column degree. They can be found easily because the columns of the
31// residual matrix are kept in a priority queue.
32//
33// We compute the numerical value of these residual columns like in a
34// left-looking algorithm by solving a sparse lower-triangular system with the
35// current L constructed so far. Note that this step is highly optimized for
36// sparsity and we reuse the computations done in the previous steps (if the
37// candidate column was already considered before). As a by-product, we also
38// get the corresponding column of U.
39//
40// Among the entries of these columns, a pivot is chosen such that the product:
41// (num_column_entries - 1) * (num_row_entries - 1)
42// is minimized. Only the pivots with a magnitude greater than
43// 'lu_factorization_pivot_threshold' times the maximum magnitude of the
44// corresponding residual column are considered for stability reasons.
45//
46// Once the pivot is chosen, the residual column divided by the pivot becomes a
47// column of L, and the non-zero pattern of the new residual submatrix is
48// updated by subtracting the outer product of this pivot column times the pivot
49// row. The product minimized above is thus an upper bound of the number of
50// fill-in created during a step.
51//
52// References:
53//
54// J. R. Gilbert and T. Peierls, "Sparse partial pivoting in time proportional
55// to arithmetic operations," SIAM J. Sci. Statist. Comput., 9 (1988): 862-874.
56//
57// I.S. Duff, A.M. Erisman and J.K. Reid, "Direct Methods for Sparse Matrices",
58// Clarendon, Oxford, UK, 1987, ISBN 0-19-853421-3,
59// http://www.amazon.com/dp/0198534213
60//
61// T.A. Davis, "Direct methods for Sparse Linear Systems", SIAM, Philadelphia,
62// 2006, ISBN-13: 978-0-898716-13, http://www.amazon.com/dp/0898716136
63//
64// TODO(user): Determine whether any of these would bring any benefit:
65// - S.C. Eisenstat and J.W.H. Liu, "The theory of elimination trees for
66// sparse unsymmetric matrices," SIAM J. Matrix Anal. Appl., 26:686-705,
67// January 2005
68// - S.C. Eisenstat and J.W.H. Liu. "Algorithmic aspects of elimination trees
69// for sparse unsymmetric matrices," SIAM J. Matrix Anal. Appl.,
70// 29:1363-1381, January 2008.
71// - http://perso.ens-lyon.fr/~bucar/papers/kauc.pdf
72
73#ifndef OR_TOOLS_GLOP_MARKOWITZ_H_
74#define OR_TOOLS_GLOP_MARKOWITZ_H_
75
76#include <cstdint>
77#include <queue>
78#include <string>
79#include <vector>
80
81#include "absl/container/inlined_vector.h"
84#include "ortools/glop/parameters.pb.h"
85#include "ortools/glop/status.h"
90#include "ortools/util/stats.h"
91
92namespace operations_research {
93namespace glop {
94
95// Holds the non-zero positions (by row) and column/row degree of the residual
96// matrix during the Gaussian elimination.
97//
98// During each step of Gaussian elimination, a row and a column will be
99// "removed" from the residual matrix. Note however that the row and column
100// indices of the non-removed part do not change, so the residual matrix at a
101// given step will only correspond to a subset of the initial indices.
103 public:
105
106 // This type is neither copyable nor movable.
109
110 // Releases the memory used by this class.
111 void Clear();
112
113 // Resets the pattern to the one of an empty square matrix of the given size.
114 void Reset(RowIndex num_rows, ColIndex num_cols);
115
116 // Resets the pattern to the one of the given matrix but only for the
117 // rows/columns whose given permutation is kInvalidRow or kInvalidCol.
118 // This also fills the singleton columns/rows with the corresponding entries.
120 const CompactSparseMatrixView& basis_matrix,
123 std::vector<ColIndex>* singleton_columns,
124 std::vector<RowIndex>* singleton_rows);
125
126 // Adds a non-zero entry to the matrix. There should be no duplicates.
127 void AddEntry(RowIndex row, ColIndex col);
128
129 // Marks the given pivot row and column as deleted.
130 // This is called at each step of the Gaussian elimination on the pivot.
131 void DeleteRowAndColumn(RowIndex pivot_row, ColIndex pivot_col);
132
133 // Decreases the degree of a row/column. This is the basic operation used to
134 // keep the correct degree after a call to DeleteRowAndColumn(). This is
135 // because row_non_zero_[row] is only lazily cleaned.
136 int32_t DecreaseRowDegree(RowIndex row);
137 int32_t DecreaseColDegree(ColIndex col);
138
139 // Returns true if the column has been deleted by DeleteRowAndColumn().
140 bool IsColumnDeleted(ColIndex col) const;
141
142 // Removes from the corresponding row_non_zero_[row] the columns that have
143 // been previously deleted by DeleteRowAndColumn().
144 void RemoveDeletedColumnsFromRow(RowIndex row);
145
146 // Returns the first non-deleted column index from this row or kInvalidCol if
147 // none can be found.
148 ColIndex GetFirstNonDeletedColumnFromRow(RowIndex row) const;
149
150 // Performs a generic Gaussian update of the residual matrix:
151 // - DeleteRowAndColumn() must already have been called.
152 // - The non-zero pattern is augmented (set union) by the one of the
153 // outer product of the pivot column and row.
154 //
155 // Important: as a small optimization, this function does not call
156 // DecreaseRowDegree() on the row in the pivot column. This has to be done by
157 // the client.
158 void Update(RowIndex pivot_row, ColIndex pivot_col,
159 const SparseColumn& column);
160
161 // Returns the degree (i.e. the number of non-zeros) of the given column.
162 // This is only valid for the column indices still in the residual matrix.
163 int32_t ColDegree(ColIndex col) const {
164 DCHECK(!deleted_columns_[col]);
165 return col_degree_[col];
166 }
167
168 // Returns the degree (i.e. the number of non-zeros) of the given row.
169 // This is only valid for the row indices still in the residual matrix.
170 int32_t RowDegree(RowIndex row) const { return row_degree_[row]; }
171
172 // Returns the set of non-zeros of the given row (unsorted).
173 // Call RemoveDeletedColumnsFromRow(row) to clean the row first.
174 // This is only valid for the row indices still in the residual matrix.
175 const absl::InlinedVector<ColIndex, 6>& RowNonZero(RowIndex row) const {
176 return row_non_zero_[row];
177 }
178
179 private:
180 // Augments the non-zero pattern of the given row by taking its union with the
181 // non-zero pattern of the given pivot_row.
182 void MergeInto(RowIndex pivot_row, RowIndex row);
183
184 // Different version of MergeInto() that works only if the non-zeros position
185 // of each row are sorted in increasing order. The output will also be sorted.
186 //
187 // TODO(user): This is currently not used but about the same speed as the
188 // non-sorted version. Investigate more.
189 void MergeIntoSorted(RowIndex pivot_row, RowIndex row);
190
191 // Using InlinedVector helps because we usually have many rows with just a few
192 // non-zeros. Note that on a 64 bits computer we get exactly 6 inlined int32_t
193 // elements without extra space, and the size of the inlined vector is 4 times
194 // 64 bits.
195 //
196 // TODO(user): We could be even more efficient since a size of int32_t is
197 // enough for us and we could store in common the inlined/not-inlined size.
199 row_non_zero_;
202 DenseBooleanRow deleted_columns_;
203 DenseBooleanRow bool_scratchpad_;
204 std::vector<ColIndex> col_scratchpad_;
205 ColIndex num_non_deleted_columns_;
206};
207
208// Adjustable priority queue of columns. Pop() returns a column with the
209// smallest degree first (degree = number of entries in the column).
210// Empty columns (i.e. with degree 0) are not stored in the queue.
212 public:
214
215 // This type is neither copyable nor movable.
218
219 // Clears the queue and prepares it to store up to num_cols column indices
220 // with a degree from 1 to max_degree included.
221 void Reset(int32_t max_degree, ColIndex num_cols);
222
223 // Changes the degree of a column and make sure it is in the queue. The degree
224 // must be non-negative (>= 0) and at most equal to the value of num_cols used
225 // in Reset(). A degree of zero will remove the column from the queue.
226 void PushOrAdjust(ColIndex col, int32_t degree);
227
228 // Removes the column index with higher priority from the queue and returns
229 // it. Returns kInvalidCol if the queue is empty.
230 ColIndex Pop();
231
232 private:
233 void Remove(ColIndex col, int32_t old_degree);
234 void Insert(ColIndex col, int32_t degree);
235
236 // A degree of zero means not present.
237 int32_t min_degree_;
239
240 // Pointer in the form of the prev/next column, kInvalidCol means "nil".
241 // We use double linked list for each degree, with col_by_degree_ pointing to
242 // the first element.
245 std::vector<ColIndex> col_by_degree_;
246};
247
248// Contains a set of columns indexed by ColIndex. This is like a SparseMatrix
249// but this class is optimized for the case where only a small subset of columns
250// is needed at the same time (like it is the case in our LU algorithm). It
251// reuses the memory of the columns that are no longer needed.
253 public:
255
256 // This type is neither copyable nor movable.
261
262 // Resets the repository to num_cols empty columns.
263 void Reset(ColIndex num_cols);
264
265 // Returns the column with given index.
266 const SparseColumn& column(ColIndex col) const;
267
268 // Gets the mutable column with given column index. The returned vector
269 // address is only valid until the next call to mutable_column().
270 SparseColumn* mutable_column(ColIndex col);
271
272 // Clears the column with given index and releases its memory to the common
273 // memory pool that is used to create new mutable_column() on demand.
274 void ClearAndReleaseColumn(ColIndex col);
275
276 // Reverts this class to its initial state. This releases the memory of the
277 // columns that were used but not the memory of this class member (this should
278 // be fine).
279 void Clear();
280
281 private:
282 // mutable_column(col) is stored in columns_[mapping_[col]].
283 // The columns_ that can be reused have their index stored in free_columns_.
284 const SparseColumn empty_column_;
286 std::vector<int> free_columns_;
287 std::vector<SparseColumn> columns_;
288};
289
290// The class that computes either the actual L.U decomposition, or the
291// permutation P and Q such that P.B.Q^{-1} will have a sparse L.U
292// decomposition.
294 public:
295 Markowitz() = default;
296
297 // This type is neither copyable nor movable.
298 Markowitz(const Markowitz&) = delete;
299 Markowitz& operator=(const Markowitz&) = delete;
300
301 // Computes the full factorization with P, Q, L and U.
302 //
303 // If the matrix is singular, the returned status will indicate it and the
304 // permutation (col_perm) will contain a maximum non-singular set of columns
305 // of the matrix. Moreover, by adding singleton columns with a one at the rows
306 // such that 'row_perm[row] == kInvalidRow', then the matrix will be
307 // non-singular.
308 ABSL_MUST_USE_RESULT Status
309 ComputeLU(const CompactSparseMatrixView& basis_matrix,
310 RowPermutation* row_perm, ColumnPermutation* col_perm,
311 TriangularMatrix* lower, TriangularMatrix* upper);
312
313 // Only computes P and Q^{-1}, L and U can be computed later from these
314 // permutations using another algorithm (for instance left-looking L.U). This
315 // may be faster than computing the full L and U at the same time but the
316 // current implementation is not optimized for this.
317 //
318 // It behaves the same as ComputeLU() for singular matrices.
319 //
320 // This function also works with a non-square matrix. It will return a set of
321 // independent columns of maximum size. If all the given columns are
322 // independent, the returned Status will be OK.
323 ABSL_MUST_USE_RESULT Status ComputeRowAndColumnPermutation(
324 const CompactSparseMatrixView& basis_matrix, RowPermutation* row_perm,
325 ColumnPermutation* col_perm);
326
327 // Releases the memory used by this class.
328 void Clear();
329
330 // Returns an estimate of the time spent in the last factorization.
332
333 // Returns a string containing the statistics for this class.
334 std::string StatString() const { return stats_.StatString(); }
335
336 // Sets the current parameters.
337 void SetParameters(const GlopParameters& parameters) {
338 parameters_ = parameters;
339 }
340
341 private:
342 // Statistics about this class.
343 struct Stats : public StatsGroup {
344 Stats()
345 : StatsGroup("Markowitz"),
346 basis_singleton_column_ratio("basis_singleton_column_ratio", this),
347 basis_residual_singleton_column_ratio(
348 "basis_residual_singleton_column_ratio", this),
349 pivots_without_fill_in_ratio("pivots_without_fill_in_ratio", this),
350 degree_two_pivot_columns("degree_two_pivot_columns", this) {}
351 RatioDistribution basis_singleton_column_ratio;
352 RatioDistribution basis_residual_singleton_column_ratio;
353 RatioDistribution pivots_without_fill_in_ratio;
354 RatioDistribution degree_two_pivot_columns;
355 };
356 Stats stats_;
357
358 // Fast track for singleton columns of the matrix. Fills a part of the row and
359 // column permutation that move these columns in order to form an identity
360 // sub-matrix on the upper left.
361 //
362 // Note(user): Linear programming bases usually have a reasonable percentage
363 // of slack columns in them, so this gives a big speedup.
364 void ExtractSingletonColumns(const CompactSparseMatrixView& basis_matrix,
365 RowPermutation* row_perm,
366 ColumnPermutation* col_perm, int* index);
367
368 // Fast track for columns that form a triangular matrix. This does not find
369 // all of them, but because the column are ordered in the same way they were
370 // ordered at the end of the previous factorization, this is likely to find
371 // quite a few.
372 //
373 // The main gain here is that it avoids taking these columns into account in
374 // InitializeResidualMatrix() and later in RemoveRowFromResidualMatrix().
375 void ExtractResidualSingletonColumns(
376 const CompactSparseMatrixView& basis_matrix, RowPermutation* row_perm,
377 ColumnPermutation* col_perm, int* index);
378
379 // Helper function for determining if a column is a residual singleton column.
380 // If it is, RowIndex* row contains the index of the single residual edge.
381 bool IsResidualSingletonColumn(
382 const ColumnView& column,
383 StrictITISpan<RowIndex, const RowIndex> row_perm, RowIndex* row);
384
385 // Returns the column of the current residual matrix with an index 'col' in
386 // the initial matrix. We compute it by solving a linear system with the
387 // current lower_ and the last computed column 'col' of a previous residual
388 // matrix. This uses the same algorithm as a left-looking factorization (see
389 // lu_factorization.h for more details).
390 const SparseColumn& ComputeColumn(const RowPermutation& row_perm,
391 ColIndex col);
392
393 // Finds an entry in the residual matrix with a low Markowitz score and a high
394 // enough magnitude. Returns its Markowitz score and updates the given
395 // pointers.
396 //
397 // We use the strategy of Zlatev, "On some pivotal strategies in Gaussian
398 // elimination by sparse technique" (1980). SIAM J. Numer. Anal. 17 18-30. It
399 // consists of looking for the best pivot in only a few columns (usually 3
400 // or 4) amongst the ones which have the lowest number of entries.
401 //
402 // Amongst the pivots with a minimum Markowitz number, we choose the one
403 // with highest magnitude. This doesn't apply to pivots with a 0 Markowitz
404 // number because all such pivots will have to be taken at some point anyway.
405 int64_t FindPivot(const RowPermutation& row_perm,
406 const ColumnPermutation& col_perm, RowIndex* pivot_row,
407 ColIndex* pivot_col, Fractional* pivot_coefficient);
408
409 // Updates the degree of a given column in the internal structure of the
410 // class.
411 void UpdateDegree(ColIndex col, int degree);
412
413 // Removes all the coefficients in the residual matrix that are on the given
414 // row or column. In both cases, the pivot row or column is ignored.
415 void RemoveRowFromResidualMatrix(RowIndex pivot_row, ColIndex pivot_col);
416 void RemoveColumnFromResidualMatrix(RowIndex pivot_row, ColIndex pivot_col);
417
418 // Updates the residual matrix given the pivot position. This is needed if the
419 // pivot row and pivot column both have more than one entry. Otherwise, the
420 // residual matrix can be updated more efficiently by calling one of the
421 // Remove...() functions above.
422 void UpdateResidualMatrix(RowIndex pivot_row, ColIndex pivot_col);
423
424 // Temporary memory.
425 struct MatrixEntry {
426 RowIndex row;
427 ColIndex col;
428 Fractional coefficient;
429 MatrixEntry() = default;
430 MatrixEntry(RowIndex r, ColIndex c, Fractional coeff)
431 : row(r), col(c), coefficient(coeff) {}
432 bool operator<(const MatrixEntry& o) const { return row < o.row; }
433 };
434 std::vector<MatrixEntry> tmp_singleton_entries_;
435
436 // Pointer to the matrix to factorize.
437 CompactSparseMatrixView const* basis_matrix_;
438
439 // These matrices are transformed during the algorithm into the final L and U
440 // matrices modulo some row and column permutations. Note that the columns of
441 // these matrices stay in the initial order.
442 SparseMatrixWithReusableColumnMemory permuted_lower_;
443 SparseMatrixWithReusableColumnMemory permuted_upper_;
444
445 // These matrices will hold the final L and U. The are created columns by
446 // columns from left to right, and at the end, their rows are permuted by
447 // ComputeLU() to become triangular.
448 TriangularMatrix lower_;
449 TriangularMatrix upper_;
450
451 // The columns of permuted_lower_ for which we do need a call to
452 // PermutedLowerSparseSolve(). This speeds up ComputeColumn().
453 DenseBooleanRow permuted_lower_column_needs_solve_;
454
455 // Contains the non-zero positions of the current residual matrix (the
456 // lower-right square matrix that gets smaller by one row and column at each
457 // Gaussian elimination step).
458 MatrixNonZeroPattern residual_matrix_non_zero_;
459
460 // Data structure to access the columns by increasing degree.
461 ColumnPriorityQueue col_by_degree_;
462
463 // True as long as only singleton columns of the residual matrix are used.
464 bool contains_only_singleton_columns_;
465
466 // Boolean used to know when col_by_degree_ become useful.
467 bool is_col_by_degree_initialized_;
468
469 // FindPivot() needs to look at the first entries of col_by_degree_, it
470 // temporary put them here before pushing them back to col_by_degree_.
471 std::vector<ColIndex> examined_col_;
472
473 // Singleton column indices are kept here rather than in col_by_degree_ to
474 // optimize the algorithm: as long as this or singleton_row_ are not empty,
475 // col_by_degree_ do not need to be initialized nor updated.
476 std::vector<ColIndex> singleton_column_;
477
478 // List of singleton row indices.
479 std::vector<RowIndex> singleton_row_;
480
481 // Proto holding all the parameters of this algorithm.
482 GlopParameters parameters_;
483
484 // Number of floating point operations of the last factorization.
485 int64_t num_fp_operations_;
486};
487
488} // namespace glop
489} // namespace operations_research
490
491#endif // OR_TOOLS_GLOP_MARKOWITZ_H_
Statistic on the distribution of a sequence of ratios, displayed as %.
Definition stats.h:265
Base class to print a nice summary of a group of statistics.
Definition stats.h:128
ColumnPriorityQueue(const ColumnPriorityQueue &)=delete
This type is neither copyable nor movable.
void PushOrAdjust(ColIndex col, int32_t degree)
Definition markowitz.cc:855
ColumnPriorityQueue & operator=(const ColumnPriorityQueue &)=delete
void Reset(int32_t max_degree, ColIndex num_cols)
Definition markowitz.cc:809
Markowitz & operator=(const Markowitz &)=delete
void Clear()
Releases the memory used by this class.
Definition markowitz.cc:175
std::string StatString() const
Returns a string containing the statistics for this class.
Definition markowitz.h:334
ABSL_MUST_USE_RESULT Status ComputeRowAndColumnPermutation(const CompactSparseMatrixView &basis_matrix, RowPermutation *row_perm, ColumnPermutation *col_perm)
Definition markowitz.cc:32
double DeterministicTimeOfLastFactorization() const
Returns an estimate of the time spent in the last factorization.
Definition markowitz.cc:551
ABSL_MUST_USE_RESULT Status ComputeLU(const CompactSparseMatrixView &basis_matrix, RowPermutation *row_perm, ColumnPermutation *col_perm, TriangularMatrix *lower, TriangularMatrix *upper)
Definition markowitz.cc:155
Markowitz(const Markowitz &)=delete
This type is neither copyable nor movable.
void SetParameters(const GlopParameters &parameters)
Sets the current parameters.
Definition markowitz.h:337
void AddEntry(RowIndex row, ColIndex col)
Adds a non-zero entry to the matrix. There should be no duplicates.
Definition markowitz.cc:628
bool IsColumnDeleted(ColIndex col) const
Returns true if the column has been deleted by DeleteRowAndColumn().
Definition markowitz.cc:652
MatrixNonZeroPattern(const MatrixNonZeroPattern &)=delete
This type is neither copyable nor movable.
void InitializeFromMatrixSubset(const CompactSparseMatrixView &basis_matrix, StrictITISpan< RowIndex, const RowIndex > row_perm, StrictITISpan< ColIndex, const ColIndex > col_perm, std::vector< ColIndex > *singleton_columns, std::vector< RowIndex > *singleton_rows)
Definition markowitz.cc:574
ColIndex GetFirstNonDeletedColumnFromRow(RowIndex row) const
Definition markowitz.cc:670
const absl::InlinedVector< ColIndex, 6 > & RowNonZero(RowIndex row) const
Definition markowitz.h:175
MatrixNonZeroPattern & operator=(const MatrixNonZeroPattern &)=delete
void Reset(RowIndex num_rows, ColIndex num_cols)
Resets the pattern to the one of an empty square matrix of the given size.
Definition markowitz.cc:564
void DeleteRowAndColumn(RowIndex pivot_row, ColIndex pivot_col)
Definition markowitz.cc:642
void Update(RowIndex pivot_row, ColIndex pivot_col, const SparseColumn &column)
Definition markowitz.cc:678
void Clear()
Releases the memory used by this class.
Definition markowitz.cc:555
SparseMatrixWithReusableColumnMemory(const SparseMatrixWithReusableColumnMemory &)=delete
This type is neither copyable nor movable.
const SparseColumn & column(ColIndex col) const
Returns the column with given index.
Definition markowitz.cc:893
void Reset(ColIndex num_cols)
Resets the repository to num_cols empty columns.
Definition markowitz.cc:887
SparseMatrixWithReusableColumnMemory & operator=(const SparseMatrixWithReusableColumnMemory &)=delete
Permutation< ColIndex > ColumnPermutation
Permutation< RowIndex > RowPermutation
StrictITIVector< ColIndex, bool > DenseBooleanRow
Row of booleans.
Definition lp_types.h:354
In SWIG mode, we don't want anything besides these top-level includes.
util_intops::StrongVector< ColumnEntryIndex, ElementIndex > SparseColumn