Google OR-Tools v9.12
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
markowitz.cc
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
15
16#include <algorithm>
17#include <cstdint>
18#include <cstdlib>
19#include <limits>
20#include <string>
21#include <vector>
22
23#include "absl/strings/str_format.h"
28
29namespace operations_research {
30namespace glop {
31
33 const CompactSparseMatrixView& basis_matrix, RowPermutation* row_perm,
34 ColumnPermutation* col_perm) {
35 SCOPED_TIME_STAT(&stats_);
36 Clear();
37 const RowIndex num_rows = basis_matrix.num_rows();
38 const ColIndex num_cols = basis_matrix.num_cols();
39 col_perm->assign(num_cols, kInvalidCol);
40 row_perm->assign(num_rows, kInvalidRow);
41
42 // Get the empty matrix corner case out of the way.
43 if (basis_matrix.IsEmpty()) return Status::OK();
44 basis_matrix_ = &basis_matrix;
45
46 // Initialize all the matrices.
47 lower_.Reset(num_rows, num_cols);
48 upper_.Reset(num_rows, num_cols);
49 permuted_lower_.Reset(num_cols);
50 permuted_upper_.Reset(num_cols);
51 permuted_lower_column_needs_solve_.assign(num_cols, false);
52 contains_only_singleton_columns_ = true;
53
54 // Start by moving the singleton columns to the front and by putting their
55 // non-zero coefficient on the diagonal. The general algorithm below would
56 // have the same effect, but this function is a lot faster.
57 int index = 0;
58 ExtractSingletonColumns(basis_matrix, row_perm, col_perm, &index);
59 ExtractResidualSingletonColumns(basis_matrix, row_perm, col_perm, &index);
60 int stats_num_pivots_without_fill_in = index;
61 int stats_degree_two_pivot_columns = 0;
62
63 // Initialize residual_matrix_non_zero_ with the submatrix left after we
64 // removed the singleton and residual singleton columns.
65 residual_matrix_non_zero_.InitializeFromMatrixSubset(
66 basis_matrix, row_perm->const_view(), col_perm->const_view(),
67 &singleton_column_, &singleton_row_);
68
69 // Perform Gaussian elimination.
70 const int end_index = std::min(num_rows.value(), num_cols.value());
71 const Fractional singularity_threshold =
72 parameters_.markowitz_singularity_threshold();
73 while (index < end_index) {
74 Fractional pivot_coefficient = 0.0;
75 RowIndex pivot_row = kInvalidRow;
76 ColIndex pivot_col = kInvalidCol;
77
78 // TODO(user): If we don't need L and U, we can abort when the residual
79 // matrix becomes dense (i.e. when its density factor is above a certain
80 // threshold). The residual size is 'end_index - index' and the
81 // density can either be computed exactly or estimated from min_markowitz.
82 const int64_t min_markowitz = FindPivot(*row_perm, *col_perm, &pivot_row,
83 &pivot_col, &pivot_coefficient);
84
85 // Singular matrix? No pivot will be selected if a column has no entries. If
86 // a column has some entries, then we are sure that a pivot will be selected
87 // but its magnitude can be really close to zero. In both cases, we
88 // report the singularity of the matrix.
89 if (pivot_row == kInvalidRow || pivot_col == kInvalidCol ||
90 std::abs(pivot_coefficient) <= singularity_threshold) {
91 const std::string error_message = absl::StrFormat(
92 "The matrix is singular! pivot = %E", pivot_coefficient);
93 VLOG(1) << "ERROR_LU: " << error_message;
94 return Status(Status::ERROR_LU, error_message);
95 }
96 DCHECK_EQ((*row_perm)[pivot_row], kInvalidRow);
97 DCHECK_EQ((*col_perm)[pivot_col], kInvalidCol);
98
99 // Update residual_matrix_non_zero_.
100 // TODO(user): This step can be skipped, once a fully dense matrix is
101 // obtained. But note that permuted_lower_column_needs_solve_ needs to be
102 // updated.
103 const int pivot_col_degree = residual_matrix_non_zero_.ColDegree(pivot_col);
104 const int pivot_row_degree = residual_matrix_non_zero_.RowDegree(pivot_row);
105 residual_matrix_non_zero_.DeleteRowAndColumn(pivot_row, pivot_col);
106 if (min_markowitz == 0) {
107 ++stats_num_pivots_without_fill_in;
108 if (pivot_col_degree == 1) {
109 RemoveRowFromResidualMatrix(pivot_row, pivot_col);
110 } else {
111 DCHECK_EQ(pivot_row_degree, 1);
112 RemoveColumnFromResidualMatrix(pivot_row, pivot_col);
113 }
114 } else {
115 // TODO(user): Note that in some rare cases, because of numerical
116 // cancellation, the column degree may actually be smaller than
117 // pivot_col_degree. Exploit that better?
119 if (pivot_col_degree == 2) { ++stats_degree_two_pivot_columns; });
120 UpdateResidualMatrix(pivot_row, pivot_col);
121 }
122
123 if (contains_only_singleton_columns_) {
124 DCHECK(permuted_upper_.column(pivot_col).IsEmpty());
125 lower_.AddDiagonalOnlyColumn(1.0);
126 upper_.AddTriangularColumn(basis_matrix.column(pivot_col), pivot_row);
127 } else {
128 lower_.AddAndNormalizeTriangularColumn(permuted_lower_.column(pivot_col),
129 pivot_row, pivot_coefficient);
130 permuted_lower_.ClearAndReleaseColumn(pivot_col);
131
132 upper_.AddTriangularColumnWithGivenDiagonalEntry(
133 permuted_upper_.column(pivot_col), pivot_row, pivot_coefficient);
134 permuted_upper_.ClearAndReleaseColumn(pivot_col);
135 }
136
137 // Update the permutations.
138 (*col_perm)[pivot_col] = ColIndex(index);
139 (*row_perm)[pivot_row] = RowIndex(index);
140 ++index;
141 }
142
143 // To get a better deterministic time, we add a factor that depend on the
144 // final number of entries in the result.
145 num_fp_operations_ += 10 * lower_.num_entries().value();
146 num_fp_operations_ += 10 * upper_.num_entries().value();
147
148 stats_.pivots_without_fill_in_ratio.Add(
149 1.0 * stats_num_pivots_without_fill_in / num_rows.value());
150 stats_.degree_two_pivot_columns.Add(1.0 * stats_degree_two_pivot_columns /
151 num_rows.value());
152 return Status::OK();
153}
154
156 RowPermutation* row_perm,
157 ColumnPermutation* col_perm,
158 TriangularMatrix* lower, TriangularMatrix* upper) {
159 // The two first swaps allow to use less memory since this way upper_
160 // and lower_ will always stay empty at the end of this function.
161 lower_.Swap(lower);
162 upper_.Swap(upper);
164 ComputeRowAndColumnPermutation(basis_matrix, row_perm, col_perm));
165 SCOPED_TIME_STAT(&stats_);
166 lower_.ApplyRowPermutationToNonDiagonalEntries(*row_perm);
167 upper_.ApplyRowPermutationToNonDiagonalEntries(*row_perm);
168 lower_.Swap(lower);
169 upper_.Swap(upper);
170 DCHECK(lower->IsLowerTriangular());
171 DCHECK(upper->IsUpperTriangular());
172 return Status::OK();
173}
174
176 SCOPED_TIME_STAT(&stats_);
177 permuted_lower_.Clear();
178 permuted_upper_.Clear();
179 residual_matrix_non_zero_.Clear();
180 examined_col_.clear();
181 num_fp_operations_ = 0;
182 is_col_by_degree_initialized_ = false;
183}
184
185void Markowitz::ExtractSingletonColumns(
186 const CompactSparseMatrixView& basis_matrix, RowPermutation* row_perm,
187 ColumnPermutation* col_perm, int* index) {
188 SCOPED_TIME_STAT(&stats_);
189 tmp_singleton_entries_.clear();
190 const ColIndex num_cols = basis_matrix.num_cols();
191 for (ColIndex col(0); col < num_cols; ++col) {
192 const ColumnView column = basis_matrix.column(col);
193 if (column.num_entries().value() == 1) {
194 const RowIndex row = column.GetFirstRow();
195
196 // We temporary mark row perm (it will be filled below).
197 // If there is a tie, we will choose the lower column.
198 if ((*row_perm)[row] != kInvalidRow) continue;
199 (*row_perm)[row] = 0;
200
201 tmp_singleton_entries_.push_back(
202 MatrixEntry(row, col, column.GetFirstCoefficient()));
203 }
204 }
205
206 // Sorting the entries by row indices allows the row_permutation to be closer
207 // to identity which seems like a good idea.
208 std::sort(tmp_singleton_entries_.begin(), tmp_singleton_entries_.end());
209 for (const MatrixEntry e : tmp_singleton_entries_) {
210 (*col_perm)[e.col] = ColIndex(*index);
211 (*row_perm)[e.row] = RowIndex(*index);
212 lower_.AddDiagonalOnlyColumn(1.0);
213 upper_.AddDiagonalOnlyColumn(e.coefficient);
214 ++(*index);
215 }
216 stats_.basis_singleton_column_ratio.Add(static_cast<double>(*index) /
217 basis_matrix.num_rows().value());
218}
219
220bool Markowitz::IsResidualSingletonColumn(
222 RowIndex* row) {
223 int residual_degree = 0;
224 for (const auto e : column) {
225 if (row_perm[e.row()] != kInvalidRow) continue;
226 ++residual_degree;
227 if (residual_degree > 1) return false;
228 *row = e.row();
229 }
230 return residual_degree == 1;
231}
232
233void Markowitz::ExtractResidualSingletonColumns(
234 const CompactSparseMatrixView& basis_matrix, RowPermutation* row_perm,
235 ColumnPermutation* col_perm, int* index) {
236 SCOPED_TIME_STAT(&stats_);
237 const ColIndex num_cols = basis_matrix.num_cols();
238 RowIndex row = kInvalidRow;
239 for (ColIndex col(0); col < num_cols; ++col) {
240 if ((*col_perm)[col] != kInvalidCol) continue;
241 const ColumnView column = basis_matrix.column(col);
242 if (!IsResidualSingletonColumn(column, row_perm->const_view(), &row)) {
243 continue;
244 }
245 (*col_perm)[col] = ColIndex(*index);
246 (*row_perm)[row] = RowIndex(*index);
247 lower_.AddDiagonalOnlyColumn(1.0);
248 upper_.AddTriangularColumn(column, row);
249 ++(*index);
250 }
251 stats_.basis_residual_singleton_column_ratio.Add(
252 static_cast<double>(*index) / basis_matrix.num_rows().value());
253}
254
255const SparseColumn& Markowitz::ComputeColumn(const RowPermutation& row_perm,
256 ColIndex col) {
257 SCOPED_TIME_STAT(&stats_);
258 // Is this the first time ComputeColumn() sees this column? This is a bit
259 // tricky because just one of the tests is not sufficient in case the matrix
260 // is degenerate.
261 const bool first_time = permuted_lower_.column(col).IsEmpty() &&
262 permuted_upper_.column(col).IsEmpty();
263
264 // If !permuted_lower_column_needs_solve_[col] then the result of the
265 // PermutedLowerSparseSolve() below is already stored in
266 // permuted_lower_.column(col) and we just need to split this column. Note
267 // that this is just an optimization and the code would work if we just
268 // assumed permuted_lower_column_needs_solve_[col] to be always true.
269 SparseColumn* lower_column = permuted_lower_.mutable_column(col);
270 if (permuted_lower_column_needs_solve_[col]) {
271 // Solve a sparse triangular system. If the column 'col' of permuted_lower_
272 // was never computed before by ComputeColumn(), we use the column 'col' of
273 // the matrix to factorize.
274 const ColumnView& input =
275 first_time ? basis_matrix_->column(col) : ColumnView(*lower_column);
276 lower_.PermutedLowerSparseSolve(input, row_perm, lower_column,
277 permuted_upper_.mutable_column(col));
278 permuted_lower_column_needs_solve_[col] = false;
279 num_fp_operations_ +=
280 lower_.NumFpOperationsInLastPermutedLowerSparseSolve();
281 return *lower_column;
282 }
283
284 // All the symbolic non-zeros are always present in lower. So if this test is
285 // true, we can conclude that there is no entries from upper that need to be
286 // moved by a cardinality argument.
287 if (lower_column->num_entries() == residual_matrix_non_zero_.ColDegree(col)) {
288 return *lower_column;
289 }
290
291 // In this case, we just need to "split" the lower column. We copy from the
292 // appropriate ColumnView in basis_matrix_.
293 // TODO(user): add PopulateFromColumnView if it is useful elsewhere.
294 if (first_time) {
295 const EntryIndex num_entries = basis_matrix_->column(col).num_entries();
296 num_fp_operations_ += num_entries.value();
297 lower_column->Reserve(num_entries);
298 for (const auto e : basis_matrix_->column(col)) {
299 lower_column->SetCoefficient(e.row(), e.coefficient());
300 }
301 }
302 num_fp_operations_ += lower_column->num_entries().value();
303 lower_column->MoveTaggedEntriesTo(row_perm,
304 permuted_upper_.mutable_column(col));
305 return *lower_column;
306}
307
308int64_t Markowitz::FindPivot(const RowPermutation& row_perm,
309 const ColumnPermutation& col_perm,
310 RowIndex* pivot_row, ColIndex* pivot_col,
311 Fractional* pivot_coefficient) {
312 SCOPED_TIME_STAT(&stats_);
313
314 // Fast track for singleton columns.
315 while (!singleton_column_.empty()) {
316 const ColIndex col = singleton_column_.back();
317 singleton_column_.pop_back();
318 DCHECK_EQ(kInvalidCol, col_perm[col]);
319
320 // This can only happen if the matrix is singular. Continuing will cause
321 // the algorithm to detect the singularity at the end when we stop before
322 // the end.
323 //
324 // TODO(user): We could detect the singularity at this point, but that
325 // may make the code more complex.
326 if (residual_matrix_non_zero_.ColDegree(col) != 1) continue;
327
328 // ComputeColumn() is not used as long as only singleton columns of the
329 // residual matrix are used. See the other condition in
330 // ComputeRowAndColumnPermutation().
331 if (contains_only_singleton_columns_) {
332 *pivot_col = col;
333 for (const SparseColumn::Entry e : basis_matrix_->column(col)) {
334 if (row_perm[e.row()] == kInvalidRow) {
335 *pivot_row = e.row();
336 *pivot_coefficient = e.coefficient();
337 break;
338 }
339 }
340 return 0;
341 }
342 const SparseColumn& column = ComputeColumn(row_perm, col);
343 if (column.IsEmpty()) continue;
344 *pivot_col = col;
345 *pivot_row = column.GetFirstRow();
346 *pivot_coefficient = column.GetFirstCoefficient();
347 return 0;
348 }
349 contains_only_singleton_columns_ = false;
350
351 // Fast track for singleton rows. Note that this is actually more than a fast
352 // track because of the Zlatev heuristic. Such rows may not be processed as
353 // soon as possible otherwise, resulting in more fill-in.
354 while (!singleton_row_.empty()) {
355 const RowIndex row = singleton_row_.back();
356 singleton_row_.pop_back();
357
358 // A singleton row could have been processed when processing a singleton
359 // column. Skip if this is the case.
360 if (row_perm[row] != kInvalidRow) continue;
361
362 // This shows that the matrix is singular, see comment above for the same
363 // case when processing singleton columns.
364 if (residual_matrix_non_zero_.RowDegree(row) != 1) continue;
365 const ColIndex col =
366 residual_matrix_non_zero_.GetFirstNonDeletedColumnFromRow(row);
367 if (col == kInvalidCol) continue;
368 const SparseColumn& column = ComputeColumn(row_perm, col);
369 if (column.IsEmpty()) continue;
370
371 *pivot_col = col;
372 *pivot_row = row;
373 *pivot_coefficient = column.LookUpCoefficient(row);
374 return 0;
375 }
376
377 // col_by_degree_ is not needed before we reach this point. Exploit this with
378 // a lazy initialization.
379 if (!is_col_by_degree_initialized_) {
380 is_col_by_degree_initialized_ = true;
381 const ColIndex num_cols = col_perm.size();
382 col_by_degree_.Reset(row_perm.size().value(), num_cols);
383 for (ColIndex col(0); col < num_cols; ++col) {
384 if (col_perm[col] != kInvalidCol) continue;
385 const int degree = residual_matrix_non_zero_.ColDegree(col);
386 DCHECK_NE(degree, 1);
387 UpdateDegree(col, degree);
388 }
389 }
390
391 // Note(user): we use int64_t since this is a product of two ints, moreover
392 // the ints should be relatively small, so that should be fine for a while.
393 int64_t min_markowitz_number = std::numeric_limits<int64_t>::max();
394 examined_col_.clear();
395 const int num_columns_to_examine = parameters_.markowitz_zlatev_parameter();
396 const Fractional threshold = parameters_.lu_factorization_pivot_threshold();
397 while (examined_col_.size() < num_columns_to_examine) {
398 const ColIndex col = col_by_degree_.Pop();
399 if (col == kInvalidCol) break;
400 if (col_perm[col] != kInvalidCol) continue;
401 const int col_degree = residual_matrix_non_zero_.ColDegree(col);
402 examined_col_.push_back(col);
403
404 // Because of the two singleton special cases at the beginning of this
405 // function and because we process columns by increasing degree, we can
406 // derive a lower bound on the best markowitz number we can get by exploring
407 // this column. If we cannot beat this number, we can stop here.
408 //
409 // Note(user): we still process extra column if we can meet the lower bound
410 // to eventually have a better pivot.
411 //
412 // Todo(user): keep the minimum row degree to have a better bound?
413 const int64_t markowitz_lower_bound = col_degree - 1;
414 if (min_markowitz_number < markowitz_lower_bound) break;
415
416 // TODO(user): col_degree (which is the same as column.num_entries()) is
417 // actually an upper bound on the number of non-zeros since there may be
418 // numerical cancellations. Exploit this here? Note that it is already used
419 // when we update the non_zero pattern of the residual matrix.
420 const SparseColumn& column = ComputeColumn(row_perm, col);
421 DCHECK_EQ(column.num_entries(), col_degree);
422
423 Fractional max_magnitude = 0.0;
424 for (const SparseColumn::Entry e : column) {
425 max_magnitude = std::max(max_magnitude, std::abs(e.coefficient()));
426 }
427 if (max_magnitude == 0.0) {
428 // All symbolic non-zero entries have been cancelled!
429 // The matrix is singular, but we continue with the other columns.
430 examined_col_.pop_back();
431 continue;
432 }
433
434 const Fractional skip_threshold = threshold * max_magnitude;
435 for (const SparseColumn::Entry e : column) {
436 const Fractional magnitude = std::abs(e.coefficient());
437 if (magnitude < skip_threshold) continue;
438
439 const int row_degree = residual_matrix_non_zero_.RowDegree(e.row());
440 const int64_t markowitz_number = (col_degree - 1) * (row_degree - 1);
441 DCHECK_NE(markowitz_number, 0);
442 if (markowitz_number < min_markowitz_number ||
443 ((markowitz_number == min_markowitz_number) &&
444 magnitude > std::abs(*pivot_coefficient))) {
445 min_markowitz_number = markowitz_number;
446 *pivot_col = col;
447 *pivot_row = e.row();
448 *pivot_coefficient = e.coefficient();
449
450 // Note(user): We could abort early here if the markowitz_lower_bound is
451 // reached, but finishing to loop over this column is fast and may lead
452 // to a pivot with a greater magnitude (i.e. a more robust
453 // factorization).
454 }
455 }
456 DCHECK_NE(min_markowitz_number, 0);
457 DCHECK_GE(min_markowitz_number, markowitz_lower_bound);
458 }
459
460 // Push back the columns that we just looked at in the queue since they
461 // are candidates for the next pivot.
462 //
463 // TODO(user): Do that after having updated the matrix? Rationale:
464 // - col_by_degree_ is LIFO, so that may save work in ComputeColumn() by
465 // calling it again on the same columns.
466 // - Maybe the earliest low-degree columns have a better precision? This
467 // actually depends on the number of operations so is not really true.
468 // - Maybe picking the column randomly from the ones with lowest degree would
469 // help having more diversity from one factorization to the next. This is
470 // for the case we do implement this TODO.
471 for (const ColIndex col : examined_col_) {
472 if (col != *pivot_col) {
473 const int degree = residual_matrix_non_zero_.ColDegree(col);
474 col_by_degree_.PushOrAdjust(col, degree);
475 }
476 }
477 return min_markowitz_number;
478}
479
480void Markowitz::UpdateDegree(ColIndex col, int degree) {
481 DCHECK(is_col_by_degree_initialized_);
482
483 // Separating the degree one columns work because we always select such
484 // a column first and pivoting by such columns does not affect the degree of
485 // any other singleton columns (except if the matrix is not inversible).
486 //
487 // Note that using this optimization does change the order in which the
488 // degree one columns are taken compared to pushing them in the queue.
489 if (degree == 1) {
490 // Note that there is no need to remove this column from col_by_degree_
491 // because it will be processed before col_by_degree_.Pop() is called and
492 // then just be ignored.
493 singleton_column_.push_back(col);
494 } else {
495 col_by_degree_.PushOrAdjust(col, degree);
496 }
497}
498
499void Markowitz::RemoveRowFromResidualMatrix(RowIndex pivot_row,
500 ColIndex pivot_col) {
501 SCOPED_TIME_STAT(&stats_);
502 // Note that instead of calling:
503 // residual_matrix_non_zero_.RemoveDeletedColumnsFromRow(pivot_row);
504 // it is a bit faster to test each position with IsColumnDeleted() since we
505 // will not need the pivot row anymore.
506 if (is_col_by_degree_initialized_) {
507 for (const ColIndex col : residual_matrix_non_zero_.RowNonZero(pivot_row)) {
508 if (residual_matrix_non_zero_.IsColumnDeleted(col)) continue;
509 UpdateDegree(col, residual_matrix_non_zero_.DecreaseColDegree(col));
510 }
511 } else {
512 for (const ColIndex col : residual_matrix_non_zero_.RowNonZero(pivot_row)) {
513 if (residual_matrix_non_zero_.IsColumnDeleted(col)) continue;
514 if (residual_matrix_non_zero_.DecreaseColDegree(col) == 1) {
515 singleton_column_.push_back(col);
516 }
517 }
518 }
519}
520
521void Markowitz::RemoveColumnFromResidualMatrix(RowIndex pivot_row,
522 ColIndex pivot_col) {
523 SCOPED_TIME_STAT(&stats_);
524 // The entries of the pivot column are exactly the symbolic non-zeros of the
525 // residual matrix, since we didn't remove the entries with a coefficient of
526 // zero during PermutedLowerSparseSolve().
527 //
528 // Note that it is okay to decrease the degree of a previous pivot row since
529 // it was set to 0 and will never trigger this test. Even if it triggers it,
530 // we just ignore such singleton rows in FindPivot().
531 for (const SparseColumn::Entry e : permuted_lower_.column(pivot_col)) {
532 const RowIndex row = e.row();
533 if (residual_matrix_non_zero_.DecreaseRowDegree(row) == 1) {
534 singleton_row_.push_back(row);
535 }
536 }
537}
538
539void Markowitz::UpdateResidualMatrix(RowIndex pivot_row, ColIndex pivot_col) {
540 SCOPED_TIME_STAT(&stats_);
541 const SparseColumn& pivot_column = permuted_lower_.column(pivot_col);
542 residual_matrix_non_zero_.Update(pivot_row, pivot_col, pivot_column);
543 for (const ColIndex col : residual_matrix_non_zero_.RowNonZero(pivot_row)) {
544 DCHECK_NE(col, pivot_col);
545 UpdateDegree(col, residual_matrix_non_zero_.ColDegree(col));
546 permuted_lower_column_needs_solve_[col] = true;
547 }
548 RemoveColumnFromResidualMatrix(pivot_row, pivot_col);
549}
550
552 return DeterministicTimeForFpOperations(num_fp_operations_);
553}
554
556 row_degree_.clear();
557 col_degree_.clear();
558 row_non_zero_.clear();
559 deleted_columns_.clear();
560 bool_scratchpad_.clear();
561 num_non_deleted_columns_ = 0;
562}
563
564void MatrixNonZeroPattern::Reset(RowIndex num_rows, ColIndex num_cols) {
565 row_degree_.AssignToZero(num_rows);
566 col_degree_.AssignToZero(num_cols);
567 row_non_zero_.clear();
568 row_non_zero_.resize(num_rows.value());
569 deleted_columns_.assign(num_cols, false);
570 bool_scratchpad_.assign(num_cols, false);
571 num_non_deleted_columns_ = num_cols;
572}
573
575 const CompactSparseMatrixView& basis_matrix,
578 std::vector<ColIndex>* singleton_columns,
579 std::vector<RowIndex>* singleton_rows) {
580 const ColIndex num_cols = basis_matrix.num_cols();
581 const RowIndex num_rows = basis_matrix.num_rows();
582
583 // Reset the matrix and initialize the vectors to the correct sizes.
584 Reset(num_rows, num_cols);
585 singleton_columns->clear();
586 singleton_rows->clear();
587
588 // Compute the number of entries in each row.
589 for (ColIndex col(0); col < num_cols; ++col) {
590 if (col_perm[col] != kInvalidCol) {
591 deleted_columns_[col] = true;
592 --num_non_deleted_columns_;
593 continue;
594 }
595 for (const SparseColumn::Entry e : basis_matrix.column(col)) {
596 ++row_degree_[e.row()];
597 }
598 }
599
600 // Reserve the row_non_zero_ vector sizes.
601 for (RowIndex row(0); row < num_rows; ++row) {
602 if (row_perm[row] == kInvalidRow) {
603 row_non_zero_[row].reserve(row_degree_[row]);
604 if (row_degree_[row] == 1) singleton_rows->push_back(row);
605 } else {
606 // This is needed because in the row degree computation above, we do not
607 // test for row_perm[row] == kInvalidRow because it is a bit faster.
608 row_degree_[row] = 0;
609 }
610 }
611
612 // Initialize row_non_zero_.
613 for (ColIndex col(0); col < num_cols; ++col) {
614 if (col_perm[col] != kInvalidCol) continue;
615 int32_t col_degree = 0;
616 for (const SparseColumn::Entry e : basis_matrix.column(col)) {
617 const RowIndex row = e.row();
618 if (row_perm[row] == kInvalidRow) {
619 ++col_degree;
620 row_non_zero_[row].push_back(col);
621 }
622 }
623 col_degree_[col] = col_degree;
624 if (col_degree == 1) singleton_columns->push_back(col);
625 }
626}
627
628void MatrixNonZeroPattern::AddEntry(RowIndex row, ColIndex col) {
629 ++row_degree_[row];
630 ++col_degree_[col];
631 row_non_zero_[row].push_back(col);
632}
633
635 return --col_degree_[col];
636}
637
639 return --row_degree_[row];
640}
641
643 ColIndex pivot_col) {
644 DCHECK(!deleted_columns_[pivot_col]);
645 deleted_columns_[pivot_col] = true;
646 --num_non_deleted_columns_;
647
648 // We do that to optimize RemoveColumnFromResidualMatrix().
649 row_degree_[pivot_row] = 0;
650}
651
653 return deleted_columns_[col];
654}
655
657 auto& ref = row_non_zero_[row];
658 int new_index = 0;
659 const int end = ref.size();
660 for (int i = 0; i < end; ++i) {
661 const ColIndex col = ref[i];
662 if (!deleted_columns_[col]) {
663 ref[new_index] = col;
664 ++new_index;
665 }
666 }
667 ref.resize(new_index);
668}
669
671 RowIndex row) const {
672 for (const ColIndex col : RowNonZero(row)) {
673 if (!IsColumnDeleted(col)) return col;
674 }
675 return kInvalidCol;
676}
677
678void MatrixNonZeroPattern::Update(RowIndex pivot_row, ColIndex pivot_col,
679 const SparseColumn& column) {
680 // Since DeleteRowAndColumn() must be called just before this function,
681 // the pivot column has been marked as deleted but degrees have not been
682 // updated yet. Hence the +1.
683 DCHECK(deleted_columns_[pivot_col]);
684 const int max_row_degree = num_non_deleted_columns_.value() + 1;
685
687 for (const ColIndex col : row_non_zero_[pivot_row]) {
689 bool_scratchpad_[col] = false;
690 }
691
692 // We only need to merge the row for the position with a coefficient different
693 // from 0.0. Note that the column must contain all the symbolic non-zeros for
694 // the row degree to be updated correctly. Note also that decreasing the row
695 // degrees due to the deletion of pivot_col will happen outside this function.
696 for (const SparseColumn::Entry e : column) {
697 const RowIndex row = e.row();
698 if (row == pivot_row) continue;
699
700 // If the row is fully dense, there is nothing to do (the merge below will
701 // not change anything). This is a small price to pay for a huge gain when
702 // the matrix becomes dense.
703 if (e.coefficient() == 0.0 || row_degree_[row] == max_row_degree) continue;
704 DCHECK_LT(row_degree_[row], max_row_degree);
705
706 // We only clean row_non_zero_[row] if there are more than 4 entries to
707 // delete. Note(user): the 4 is somewhat arbitrary, but gives good results
708 // on the Netlib (23/04/2013). Note that calling
709 // RemoveDeletedColumnsFromRow() is not mandatory and does not change the LU
710 // decomposition, so we could call it all the time or never and the
711 // algorithm would still work.
712 const int kDeletionThreshold = 4;
713 if (row_non_zero_[row].size() > row_degree_[row] + kDeletionThreshold) {
715 }
716 // TODO(user): Special case if row_non_zero_[pivot_row].size() == 1?
717 if (/* DISABLES CODE */ (true)) {
718 MergeInto(pivot_row, row);
719 } else {
720 // This is currently not used, but kept as an alternative algorithm to
721 // investigate. The performance is really similar, but the final L.U is
722 // different. Note that when this is used, there is no need to modify
723 // bool_scratchpad_ at the beginning of this function.
724 //
725 // TODO(user): Add unit tests before using this.
726 MergeIntoSorted(pivot_row, row);
727 }
728 }
729}
730
731void MatrixNonZeroPattern::MergeInto(RowIndex pivot_row, RowIndex row) {
732 // Note that bool_scratchpad_ must be already false on the positions in
733 // row_non_zero_[pivot_row].
734 for (const ColIndex col : row_non_zero_[row]) {
735 bool_scratchpad_[col] = true;
736 }
737
738 auto& non_zero = row_non_zero_[row];
739 const int old_size = non_zero.size();
740 for (const ColIndex col : row_non_zero_[pivot_row]) {
741 if (bool_scratchpad_[col]) {
742 bool_scratchpad_[col] = false;
743 } else {
744 non_zero.push_back(col);
745 ++col_degree_[col];
746 }
747 }
748 row_degree_[row] += non_zero.size() - old_size;
749}
750
751namespace {
752
753// Given two sorted vectors (the second one is the initial value of out), merges
754// them and outputs the sorted result in out. The merge is stable and an element
755// of input_a will appear before the identical elements of the second input.
756template <typename V, typename W>
757void MergeSortedVectors(const V& input_a, W* out) {
758 if (input_a.empty()) return;
759 const auto& input_b = *out;
760 int index_a = input_a.size() - 1;
761 int index_b = input_b.size() - 1;
762 int index_out = input_a.size() + input_b.size();
763 out->resize(index_out);
764 while (index_a >= 0) {
765 if (index_b < 0) {
766 while (index_a >= 0) {
767 --index_out;
768 (*out)[index_out] = input_a[index_a];
769 --index_a;
770 }
771 return;
772 }
773 --index_out;
774 if (input_a[index_a] > input_b[index_b]) {
775 (*out)[index_out] = input_a[index_a];
776 --index_a;
777 } else {
778 (*out)[index_out] = input_b[index_b];
779 --index_b;
780 }
781 }
782}
783
784} // namespace
785
786// The algorithm first computes into col_scratchpad_ the entries in pivot_row
787// that are not in the row (i.e. the fill-in). It then updates the non-zero
788// pattern using this temporary vector.
789void MatrixNonZeroPattern::MergeIntoSorted(RowIndex pivot_row, RowIndex row) {
790 // We want to add the entries of the input not already in the output.
791 const auto& input = row_non_zero_[pivot_row];
792 const auto& output = row_non_zero_[row];
793
794 // These two resizes are because of the set_difference() output iterator api.
795 col_scratchpad_.resize(input.size());
796 col_scratchpad_.resize(std::set_difference(input.begin(), input.end(),
797 output.begin(), output.end(),
798 col_scratchpad_.begin()) -
799 col_scratchpad_.begin());
800
801 // Add the fill-in to the pattern.
802 for (const ColIndex col : col_scratchpad_) {
803 ++col_degree_[col];
804 }
805 row_degree_[row] += col_scratchpad_.size();
806 MergeSortedVectors(col_scratchpad_, &row_non_zero_[row]);
807}
808
809void ColumnPriorityQueue::Reset(int max_degree, ColIndex num_cols) {
810 degree_.assign(num_cols, 0);
811 col_by_degree_.assign(max_degree + 1, kInvalidCol);
812 min_degree_ = max_degree + 1;
813
814 // These are not used as long as the degree is zero.
815 prev_.resize(num_cols, kInvalidCol);
816 next_.resize(num_cols, kInvalidCol);
817}
818
819void ColumnPriorityQueue::Remove(ColIndex col, int32_t old_degree) {
820 DCHECK_NE(old_degree, 0);
821
822 const ColIndex old_next = next_[col];
823 const ColIndex old_prev = prev_[col];
824
825 // Remove.
826 if (old_next != -1) prev_[old_next] = old_prev;
827 if (old_prev == -1) {
828 DCHECK_EQ(col_by_degree_[old_degree], col);
829 col_by_degree_[old_degree] = old_next;
830 } else {
831 next_[old_prev] = old_next;
832 }
833
834 // Mark as removed.
835 degree_[col] = 0;
836}
837
838void ColumnPriorityQueue::Insert(ColIndex col, int32_t degree) {
839 DCHECK_EQ(degree_[col], 0);
840 DCHECK_NE(degree, 0);
841
842 const ColIndex new_next = col_by_degree_[degree];
843 next_[col] = new_next;
844 if (new_next != -1) {
845 prev_[new_next] = col;
846 }
847
848 col_by_degree_[degree] = col;
849 prev_[col] = kInvalidCol;
850 degree_[col] = degree;
851
852 min_degree_ = std::min(min_degree_, degree);
853}
854
855void ColumnPriorityQueue::PushOrAdjust(ColIndex col, int32_t degree) {
856 DCHECK_GE(degree, 0);
857 DCHECK_LT(degree, col_by_degree_.size());
858 DCHECK_GE(col, 0);
859 DCHECK_LT(col, degree_.size());
860
861 const int32_t old_degree = degree_[col];
862 if (degree != old_degree) {
863 if (old_degree != 0) {
864 Remove(col, old_degree);
865 }
866 if (degree != 0) {
867 Insert(col, degree);
868 }
869 }
870}
871
873 DCHECK_GE(min_degree_, 0);
874 DCHECK_LE(min_degree_, col_by_degree_.size());
875 ColIndex result = kInvalidCol;
876 const int limit = col_by_degree_.size();
877 while (true) {
878 if (min_degree_ == limit) return kInvalidCol;
879 result = col_by_degree_[min_degree_];
880 if (result != kInvalidCol) break;
881 min_degree_++;
882 }
883 Remove(result, min_degree_);
884 return result;
885}
886
888 mapping_.assign(num_cols.value(), -1);
889 free_columns_.clear();
890 columns_.clear();
891}
892
894 ColIndex col) const {
895 if (mapping_[col] == -1) return empty_column_;
896 return columns_[mapping_[col]];
897}
898
900 ColIndex col) {
901 if (mapping_[col] != -1) return &columns_[mapping_[col]];
902 int new_col_index;
903 if (free_columns_.empty()) {
904 new_col_index = columns_.size();
905 columns_.push_back(SparseColumn());
906 } else {
907 new_col_index = free_columns_.back();
908 free_columns_.pop_back();
909 }
910 mapping_[col] = new_col_index;
911 return &columns_[new_col_index];
912}
913
915 DCHECK_NE(mapping_[col], -1);
916 free_columns_.push_back(mapping_[col]);
917 columns_[mapping_[col]].Clear();
918 mapping_[col] = -1;
919}
920
922 mapping_.clear();
923 free_columns_.clear();
924 columns_.clear();
925}
926
927} // namespace glop
928} // namespace operations_research
void PushOrAdjust(ColIndex col, int32_t degree)
Definition markowitz.cc:855
void Reset(int32_t max_degree, ColIndex num_cols)
Definition markowitz.cc:809
ColumnView column(ColIndex col) const
Definition sparse.h:580
bool IsEmpty() const
Same behavior as the SparseMatrix functions above.
Definition sparse.h:577
void Clear()
Releases the memory used by this class.
Definition markowitz.cc:175
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
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
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
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
StrictITISpan< IndexType, const IndexType > const_view() const
Definition permutation.h:97
void assign(IndexType size, IndexType value)
Definition permutation.h:66
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
static Status OK()
Improves readability but identical to 0-arg constructor.
Definition status.h:55
@ ERROR_LU
The LU factorization of the current basis couldn't be computed.
Definition status.h:34
void AddDiagonalOnlyColumn(Fractional diagonal_value)
Definition sparse.cc:694
void resize(size_type new_size)
constexpr ColIndex kInvalidCol(-1)
Permutation< ColIndex > ColumnPermutation
Permutation< RowIndex > RowPermutation
constexpr RowIndex kInvalidRow(-1)
static double DeterministicTimeForFpOperations(int64_t n)
Definition lp_types.h:433
In SWIG mode, we don't want anything besides these top-level includes.
util_intops::StrongVector< ColumnEntryIndex, ElementIndex > SparseColumn
static int input(yyscan_t yyscanner)
#define IF_STATS_ENABLED(instructions)
Definition stats.h:417
#define SCOPED_TIME_STAT(stats)
Definition stats.h:418
#define GLOP_RETURN_IF_ERROR(function_call)
Macro to simplify error propagation between function returning Status.
Definition status.h:71
Holds a triplet (row, col, coefficient).