Google OR-Tools v9.11
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
matrix_utils.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 <vector>
21
22#include "absl/log/check.h"
23#include "ortools/base/hash.h"
27
28namespace operations_research {
29namespace glop {
30
31namespace {
32
33// Returns true iff the two given sparse columns are proportional. The two
34// sparse columns must be ordered by row and must not contain any zero entry.
35//
36// See the header comment on FindProportionalColumns() for the exact definition
37// of two proportional columns with a given tolerance.
38bool AreColumnsProportional(const SparseColumn& a, const SparseColumn& b,
39 Fractional tolerance) {
40 DCHECK(a.IsCleanedUp());
41 DCHECK(b.IsCleanedUp());
42 if (a.num_entries() != b.num_entries()) return false;
43 Fractional multiple = 0.0;
44 bool a_is_larger = true;
45 for (const EntryIndex i : a.AllEntryIndices()) {
46 if (a.EntryRow(i) != b.EntryRow(i)) return false;
47 const Fractional coeff_a = a.EntryCoefficient(i);
48 const Fractional coeff_b = b.EntryCoefficient(i);
49 if (multiple == 0.0) {
50 a_is_larger = std::abs(coeff_a) > std::abs(coeff_b);
51 multiple = a_is_larger ? coeff_a / coeff_b : coeff_b / coeff_a;
52 } else {
53 if (a_is_larger) {
54 if (std::abs(coeff_a / coeff_b - multiple) > tolerance) return false;
55 } else {
56 if (std::abs(coeff_b / coeff_a - multiple) > tolerance) return false;
57 }
58 }
59 }
60 return true;
61}
62
63// A column index together with its fingerprint. See ComputeFingerprint().
64struct ColumnFingerprint {
65 ColumnFingerprint(ColIndex _col, int64_t _hash, double _value)
66 : col(_col), hash(_hash), value(_value) {}
67 ColIndex col;
68 int64_t hash;
69 double value;
70
71 // This order has the property that if AreProportionalCandidates() is true for
72 // two given columns, then in a sorted list of columns
73 // AreProportionalCandidates() will be true for all the pairs of columns
74 // between the two given ones (included).
75 bool operator<(const ColumnFingerprint& other) const {
76 if (hash == other.hash) {
77 return value < other.value;
78 }
79 return hash < other.hash;
80 }
81};
82
83// Two columns can be proportional only if:
84// - Their non-zero pattern hashes are the same.
85// - Their double fingerprints are close to each other.
86bool AreProportionalCandidates(ColumnFingerprint a, ColumnFingerprint b,
87 Fractional tolerance) {
88 if (a.hash != b.hash) return false;
89 return std::abs(a.value - b.value) < tolerance;
90}
91
92// The fingerprint of a column has two parts:
93// - A hash value of the column non-zero pattern.
94// - A double value which should be the same for two proportional columns
95// modulo numerical errors.
96ColumnFingerprint ComputeFingerprint(ColIndex col, const SparseColumn& column) {
97 int64_t non_zero_pattern_hash = 0;
98 Fractional min_abs = std::numeric_limits<Fractional>::max();
99 Fractional max_abs = 0.0;
100 Fractional sum = 0.0;
101 for (const SparseColumn::Entry e : column) {
102 non_zero_pattern_hash =
103 util_hash::Hash(e.row().value(), non_zero_pattern_hash);
104 sum += e.coefficient();
105 min_abs = std::min(min_abs, std::abs(e.coefficient()));
106 max_abs = std::max(max_abs, std::abs(e.coefficient()));
107 }
108
109 // The two scaled values are in [0, 1].
110 // TODO(user): A better way to discriminate columns would be to take the
111 // scalar product with a constant but random vector scaled by max_abs.
112 DCHECK_NE(0.0, max_abs);
113 const double inverse_dynamic_range = min_abs / max_abs;
114 const double scaled_average =
115 std::abs(sum) /
116 (static_cast<double>(column.num_entries().value()) * max_abs);
117 return ColumnFingerprint(col, non_zero_pattern_hash,
118 inverse_dynamic_range + scaled_average);
119}
120
121} // namespace
122
124 Fractional tolerance) {
125 const ColIndex num_cols = matrix.num_cols();
126 ColMapping mapping(num_cols, kInvalidCol);
127
128 // Compute the fingerprint of each columns and sort them.
129 std::vector<ColumnFingerprint> fingerprints;
130 for (ColIndex col(0); col < num_cols; ++col) {
131 if (!matrix.column(col).IsEmpty()) {
132 fingerprints.push_back(ComputeFingerprint(col, matrix.column(col)));
133 }
134 }
135 std::sort(fingerprints.begin(), fingerprints.end());
136
137 // Find a representative of each proportional columns class. This only
138 // compares columns with a close-enough fingerprint.
139 for (int i = 0; i < fingerprints.size(); ++i) {
140 const ColIndex col_a = fingerprints[i].col;
141 if (mapping[col_a] != kInvalidCol) continue;
142 for (int j = i + 1; j < fingerprints.size(); ++j) {
143 const ColIndex col_b = fingerprints[j].col;
144 if (mapping[col_b] != kInvalidCol) continue;
145
146 // Note that we use the same tolerance for the fingerprints.
147 // TODO(user): Derive precise bounds on what this tolerance should be so
148 // that no proportional columns are missed.
149 if (!AreProportionalCandidates(fingerprints[i], fingerprints[j],
150 tolerance)) {
151 break;
152 }
153 if (AreColumnsProportional(matrix.column(col_a), matrix.column(col_b),
154 tolerance)) {
155 mapping[col_b] = col_a;
156 }
157 }
158 }
159
160 // Sort the mapping so that the representative of each class is the smallest
161 // column. To achieve this, the current representative is used as a pointer
162 // to the new one, a bit like in an union find algorithm.
163 for (ColIndex col(0); col < num_cols; ++col) {
164 if (mapping[col] == kInvalidCol) continue;
165 const ColIndex new_representative = mapping[mapping[col]];
166 if (new_representative != kInvalidCol) {
167 mapping[col] = new_representative;
168 } else {
169 if (mapping[col] > col) {
170 mapping[mapping[col]] = col;
171 mapping[col] = kInvalidCol;
172 }
173 }
174 }
175
176 return mapping;
177}
178
180 const SparseMatrix& matrix, Fractional tolerance) {
181 const ColIndex num_cols = matrix.num_cols();
182 ColMapping mapping(num_cols, kInvalidCol);
183 for (ColIndex col_a(0); col_a < num_cols; ++col_a) {
184 if (matrix.column(col_a).IsEmpty()) continue;
185 if (mapping[col_a] != kInvalidCol) continue;
186 for (ColIndex col_b(col_a + 1); col_b < num_cols; ++col_b) {
187 if (matrix.column(col_b).IsEmpty()) continue;
188 if (mapping[col_b] != kInvalidCol) continue;
189 if (AreColumnsProportional(matrix.column(col_a), matrix.column(col_b),
190 tolerance)) {
191 mapping[col_b] = col_a;
192 }
193 }
194 }
195 return mapping;
196}
197
198bool AreFirstColumnsAndRowsExactlyEquals(RowIndex num_rows, ColIndex num_cols,
199 const SparseMatrix& matrix_a,
200 const CompactSparseMatrix& matrix_b) {
201 // TODO(user): Also DCHECK() that matrix_b is ordered by rows.
202 DCHECK(matrix_a.IsCleanedUp());
203 if (num_rows > matrix_a.num_rows() || num_rows > matrix_b.num_rows() ||
204 num_cols > matrix_a.num_cols() || num_cols > matrix_b.num_cols()) {
205 return false;
206 }
207 for (ColIndex col(0); col < num_cols; ++col) {
208 const SparseColumn& col_a = matrix_a.column(col);
209 const ColumnView& col_b = matrix_b.column(col);
210 const EntryIndex end = std::min(col_a.num_entries(), col_b.num_entries());
211 if (end < col_a.num_entries() && col_a.EntryRow(end) < num_rows) {
212 return false;
213 }
214 if (end < col_b.num_entries() && col_b.EntryRow(end) < num_rows) {
215 return false;
216 }
217 for (EntryIndex i(0); i < end; ++i) {
218 if (col_a.EntryRow(i) != col_b.EntryRow(i)) {
219 if (col_a.EntryRow(i) < num_rows || col_b.EntryRow(i) < num_rows) {
220 return false;
221 } else {
222 break;
223 }
224 }
225 if (col_a.EntryCoefficient(i) != col_b.EntryCoefficient(i)) {
226 return false;
227 }
228 if (col_a.num_entries() > end && col_a.EntryRow(end) < num_rows) {
229 return false;
230 }
231 if (col_b.num_entries() > end && col_b.EntryRow(end) < num_rows) {
232 return false;
233 }
234 }
235 }
236 return true;
237}
238
240 DCHECK(matrix.IsCleanedUp());
241 if (matrix.num_rows().value() > matrix.num_cols().value()) return false;
242 const ColIndex first_identity_col =
243 matrix.num_cols() - RowToColIndex(matrix.num_rows());
244 for (ColIndex col = first_identity_col; col < matrix.num_cols(); ++col) {
245 const SparseColumn& column = matrix.column(col);
246 if (column.num_entries() != 1 ||
247 column.EntryCoefficient(EntryIndex(0)) != 1.0) {
248 return false;
249 }
250 }
251 return true;
252}
253
254} // namespace glop
255} // namespace operations_research
RowIndex EntryRow(EntryIndex i) const
Fractional EntryCoefficient(EntryIndex i) const
ColumnView column(ColIndex col) const
Definition sparse.h:416
Fractional EntryCoefficient(EntryIndex i) const
RowIndex EntryRow(EntryIndex i) const
Use a separate API to get the row and coefficient of entry #i.
const SparseColumn & column(ColIndex col) const
Access the underlying sparse columns.
Definition sparse.h:194
bool IsCleanedUp() const
Call IsCleanedUp() on all columns, useful for doing a DCHECK.
Definition sparse.cc:144
RowIndex num_rows() const
Return the matrix dimension.
Definition sparse.h:190
EntryIndex num_entries() const
Note this method can only be used when the vector has no duplicates.
bool IsEmpty() const
Returns true if the vector is empty.
int64_t b
Definition table.cc:45
int64_t a
Definition table.cc:44
int64_t value
ColIndex col
Definition markowitz.cc:187
int64_t hash
constexpr ColIndex kInvalidCol(-1)
ColMapping FindProportionalColumnsUsingSimpleAlgorithm(const SparseMatrix &matrix, Fractional tolerance)
bool IsRightMostSquareMatrixIdentity(const SparseMatrix &matrix)
Returns true iff the rightmost square matrix is an identity matrix.
ColIndex RowToColIndex(RowIndex row)
Get the ColIndex corresponding to the column # row.
Definition lp_types.h:54
bool AreFirstColumnsAndRowsExactlyEquals(RowIndex num_rows, ColIndex num_cols, const SparseMatrix &matrix_a, const CompactSparseMatrix &matrix_b)
ColMapping FindProportionalColumns(const SparseMatrix &matrix, Fractional tolerance)
In SWIG mode, we don't want anything besides these top-level includes.
util_intops::StrongVector< ColumnEntryIndex, ElementIndex, ElementAllocator > SparseColumn
uint64_t Hash(uint64_t num, uint64_t c)
Definition hash.h:73
int column
std::optional< int64_t > end