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