Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
primal_edge_norms.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 <cmath>
18#include <cstdlib>
19
20#include "absl/log/check.h"
21#include "absl/log/log.h"
30#include "ortools/util/stats.h"
31
32namespace operations_research {
33namespace glop {
34
36 const VariablesInfo& variables_info,
37 const BasisFactorization& basis_factorization)
38 : compact_matrix_(compact_matrix),
39 variables_info_(variables_info),
40 basis_factorization_(basis_factorization),
41 stats_(),
42 recompute_edge_squared_norms_(true),
43 reset_devex_weights_(true),
44 edge_squared_norms_(),
45 matrix_column_norms_(),
46 devex_weights_(),
47 direction_left_inverse_(),
48 num_operations_(0) {}
49
51 SCOPED_TIME_STAT(&stats_);
52 matrix_column_norms_.clear();
53 recompute_edge_squared_norms_ = true;
54 reset_devex_weights_ = true;
55 for (bool* watcher : watchers_) *watcher = true;
56}
57
59 if (pricing_rule_ != GlopParameters ::STEEPEST_EDGE) return false;
60 return recompute_edge_squared_norms_;
61}
62
64 switch (pricing_rule_) {
70 return GetDevexWeights().const_view();
71 }
72 LOG(FATAL) << "Invalid pricing rule: " << pricing_rule_;
73}
74
76 if (recompute_edge_squared_norms_) ComputeEdgeSquaredNorms();
77 return edge_squared_norms_;
78}
79
81 if (reset_devex_weights_) ResetDevexWeights();
82 return devex_weights_;
83}
84
86 if (matrix_column_norms_.empty()) ComputeMatrixColumnNorms();
87 return matrix_column_norms_;
88}
89
91 ColIndex entering_col, const ScatteredColumn& direction) {
92 if (!recompute_edge_squared_norms_) {
93 SCOPED_TIME_STAT(&stats_);
94 // Recompute the squared norm of the edge used during this
95 // iteration, i.e. the entering edge.
96 const Fractional old_squared_norm = edge_squared_norms_[entering_col];
97 const Fractional precise_squared_norm = 1.0 + SquaredNorm(direction);
98 edge_squared_norms_[entering_col] = precise_squared_norm;
99
100 const Fractional precise_norm = sqrt(precise_squared_norm);
101 const Fractional estimated_edges_norm_accuracy =
102 (precise_norm - sqrt(old_squared_norm)) / precise_norm;
103 stats_.edges_norm_accuracy.Add(estimated_edges_norm_accuracy);
104 if (std::abs(estimated_edges_norm_accuracy) >
105 parameters_.recompute_edges_norm_threshold()) {
106 VLOG(1) << "Recomputing edge norms: " << sqrt(precise_squared_norm)
107 << " vs " << sqrt(old_squared_norm);
108 recompute_edge_squared_norms_ = true;
109 for (bool* watcher : watchers_) *watcher = true;
110 }
111
112 if (old_squared_norm < 0.25 * precise_squared_norm) {
113 VLOG(1) << "Imprecise norm, reprice. old=" << old_squared_norm
114 << " new=" << precise_squared_norm;
115 return false;
116 }
117 }
118 return true;
119}
120
122 ColIndex leaving_col,
123 RowIndex leaving_row,
124 const ScatteredColumn& direction,
125 UpdateRow* update_row) {
126 SCOPED_TIME_STAT(&stats_);
127 DCHECK_NE(entering_col, leaving_col);
128 if (!recompute_edge_squared_norms_) {
129 update_row->ComputeUpdateRow(leaving_row);
130 ComputeDirectionLeftInverse(entering_col, direction);
131 UpdateEdgeSquaredNorms(entering_col, leaving_col, leaving_row,
132 direction.values, *update_row);
133 }
134 if (!reset_devex_weights_) {
135 // Resets devex weights once in a while. If so, no need to update them
136 // before.
137 ++num_devex_updates_since_reset_;
138 if (num_devex_updates_since_reset_ >
139 parameters_.devex_weights_reset_period()) {
140 reset_devex_weights_ = true;
141 } else {
142 update_row->ComputeUpdateRow(leaving_row);
143 UpdateDevexWeights(entering_col, leaving_col, leaving_row,
144 direction.values, *update_row);
145 }
146 }
147}
148
149void PrimalEdgeNorms::ComputeMatrixColumnNorms() {
150 SCOPED_TIME_STAT(&stats_);
151 matrix_column_norms_.resize(compact_matrix_.num_cols(), 0.0);
152 for (ColIndex col(0); col < compact_matrix_.num_cols(); ++col) {
153 matrix_column_norms_[col] = SquaredNorm(compact_matrix_.column(col));
154 num_operations_ += compact_matrix_.column(col).num_entries().value();
155 }
156}
157
158void PrimalEdgeNorms::ComputeEdgeSquaredNorms() {
159 SCOPED_TIME_STAT(&stats_);
160
161 // time_limit_->LimitReached() can be costly sometimes, so we only do that
162 // if we feel this will be slow anyway.
163 const bool test_limit = (time_limit_ != nullptr) &&
164 basis_factorization_.NumberOfEntriesInLU() > 10'000;
165
166 // Since we will do a lot of inversions, it is better to be as efficient and
167 // precise as possible by refactorizing the basis.
168 DCHECK(basis_factorization_.IsRefactorized());
169 edge_squared_norms_.resize(compact_matrix_.num_cols(), 1.0);
170 for (const ColIndex col : variables_info_.GetIsRelevantBitRow()) {
171 // Note the +1.0 in the squared norm for the component of the edge on the
172 // 'entering_col'.
173 edge_squared_norms_[col] = 1.0 + basis_factorization_.RightSolveSquaredNorm(
174 compact_matrix_.column(col));
175
176 // This operation can be costly, and we abort if we are stuck here.
177 // Note that we still mark edges as "recomputed" otherwise we can runs into
178 // some DCHECK before we actually abort the solve.
179 if (test_limit && time_limit_->LimitReached()) break;
180 }
181
182 recompute_edge_squared_norms_ = false;
183}
184
185// TODO(user): It should be possible to reorganize the code and call this when
186// the value of direction is no longer needed. This will simplify the code and
187// avoid a copy here.
188void PrimalEdgeNorms::ComputeDirectionLeftInverse(
189 ColIndex entering_col, const ScatteredColumn& direction) {
190 SCOPED_TIME_STAT(&stats_);
191
192 // Initialize direction_left_inverse_ to direction. Note the special case when
193 // the non-zero vector is empty which means we don't know and need to use the
194 // dense version.
195 const ColIndex size = RowToColIndex(direction.values.size());
196 const double kThreshold = 0.05 * size.value();
197 if (!direction_left_inverse_.non_zeros.empty() &&
198 (direction_left_inverse_.non_zeros.size() + direction.non_zeros.size() <
199 2 * kThreshold)) {
200 ClearAndResizeVectorWithNonZeros(size, &direction_left_inverse_);
201 for (const auto e : direction) {
202 direction_left_inverse_[RowToColIndex(e.row())] = e.coefficient();
203 }
204 } else {
205 direction_left_inverse_.values = Transpose(direction.values);
206 direction_left_inverse_.non_zeros.clear();
207 }
208
209 if (direction.non_zeros.size() < kThreshold) {
210 direction_left_inverse_.non_zeros = TransposedView(direction).non_zeros;
211 }
212 basis_factorization_.LeftSolve(&direction_left_inverse_);
213
214 // TODO(user): Refactorize if estimated accuracy above a threshold.
215 IF_STATS_ENABLED(stats_.direction_left_inverse_accuracy.Add(
216 compact_matrix_.ColumnScalarProduct(entering_col,
217 direction_left_inverse_.values) -
218 SquaredNorm(direction.values)));
219 IF_STATS_ENABLED(stats_.direction_left_inverse_density.Add(
220 Density(direction_left_inverse_.values)));
221}
222
223// Let new_edge denote the edge of 'col' in the new basis. We want:
224// reduced_costs_[col] = ScalarProduct(new_edge, basic_objective_);
225// edge_squared_norms_[col] = SquaredNorm(new_edge);
226//
227// In order to compute this, we use the formulas:
228// new_leaving_edge = old_entering_edge / divisor.
229// new_edge = old_edge + update_coeff * new_leaving_edge.
230void PrimalEdgeNorms::UpdateEdgeSquaredNorms(ColIndex entering_col,
231 ColIndex leaving_col,
232 RowIndex leaving_row,
233 const DenseColumn& direction,
234 const UpdateRow& update_row) {
235 SCOPED_TIME_STAT(&stats_);
236
237 // 'pivot' is the value of the entering_edge at 'leaving_row'.
238 // The edge of the 'leaving_col' in the new basis is equal to
239 // entering_edge / 'pivot'.
240 const Fractional pivot = -direction[leaving_row];
241 DCHECK_NE(pivot, 0.0);
242
243 const ColIndex first_slack =
244 compact_matrix_.num_cols() - RowToColIndex(compact_matrix_.num_rows());
245
246 // Note that this should be precise because of the call to
247 // TestEnteringEdgeNormPrecision().
248 const Fractional entering_squared_norm = edge_squared_norms_[entering_col];
249 const Fractional leaving_squared_norm =
250 std::max(Fractional(1.0), entering_squared_norm / Square(pivot));
251
252 int stat_lower_bounded_norms = 0;
253 const Fractional factor = 2.0 / pivot;
254 const auto view = compact_matrix_.view();
255 auto output = edge_squared_norms_.view();
256 const auto direction_left_inverse =
257 direction_left_inverse_.values.const_view();
258 for (const ColIndex col : update_row.GetNonZeroPositions()) {
259 const Fractional coeff = update_row.GetCoefficient(col);
260 const Fractional scalar_product =
261 col >= first_slack
262 ? direction_left_inverse_[col - first_slack]
263 : view.ColumnScalarProduct(col, direction_left_inverse);
264 num_operations_ += view.ColumnNumEntries(col).value();
265
266 // Update the edge squared norm of this column. Note that the update
267 // formula used is important to maximize the precision. See an explanation
268 // in the dual context in Koberstein's PhD thesis, section 8.2.2.1.
269 output[col] +=
270 coeff * (coeff * leaving_squared_norm + factor * scalar_product);
271
272 // Make sure it doesn't go under a known lower bound (TODO(user): ref?).
273 // This way norms are always >= 1.0 .
274 // TODO(user): precompute 1 / Square(pivot) or 1 / pivot? it will be
275 // slightly faster, but may introduce numerical issues. More generally,
276 // this test is only needed in a few cases, so is it worth it?
277 const Fractional lower_bound = 1.0 + Square(coeff / pivot);
278 if (output[col] < lower_bound) {
279 output[col] = lower_bound;
280 ++stat_lower_bounded_norms;
281 }
282 }
283 output[leaving_col] = leaving_squared_norm;
284 stats_.lower_bounded_norms.Add(stat_lower_bounded_norms);
285}
286
287void PrimalEdgeNorms::UpdateDevexWeights(
288 ColIndex entering_col /* index q in the paper */,
289 ColIndex leaving_col /* index p in the paper */, RowIndex leaving_row,
290 const DenseColumn& direction, const UpdateRow& update_row) {
291 SCOPED_TIME_STAT(&stats_);
292
293 // Compared to steepest edge update, the DEVEX weight uses the largest of the
294 // norms of two vectors to approximate the norm of the sum.
295 const Fractional entering_norm = sqrt(PreciseSquaredNorm(direction));
296 const Fractional pivot_magnitude = std::abs(direction[leaving_row]);
297 const Fractional leaving_norm =
298 std::max(Fractional(1.0), entering_norm / pivot_magnitude);
299 for (const ColIndex col : update_row.GetNonZeroPositions()) {
300 const Fractional coeff = update_row.GetCoefficient(col);
301 const Fractional update_vector_norm = std::abs(coeff) * leaving_norm;
302 devex_weights_[col] =
303 std::max(devex_weights_[col], Square(update_vector_norm));
304 }
305 devex_weights_[leaving_col] = Square(leaving_norm);
306}
307
308void PrimalEdgeNorms::ResetDevexWeights() {
309 SCOPED_TIME_STAT(&stats_);
310 if (parameters_.initialize_devex_with_column_norms()) {
311 devex_weights_ = GetMatrixColumnNorms();
312 } else {
313 devex_weights_.assign(compact_matrix_.num_cols(), 1.0);
314 }
315 num_devex_updates_since_reset_ = 0;
316 reset_devex_weights_ = false;
317}
318
319} // namespace glop
320} // namespace operations_research
StrictITISpan< ColIndex, const Fractional > ConstView
Definition lp_types.h:291
Fractional RightSolveSquaredNorm(const ColumnView &a) const
ColumnView column(ColIndex col) const
Definition sparse.h:419
static constexpr PricingRule DEVEX
static constexpr PricingRule DANTZIG
static constexpr PricingRule STEEPEST_EDGE
void UpdateBeforeBasisPivot(ColIndex entering_col, ColIndex leaving_col, RowIndex leaving_row, const ScatteredColumn &direction, UpdateRow *update_row)
bool TestEnteringEdgeNormPrecision(ColIndex entering_col, const ScatteredColumn &direction)
PrimalEdgeNorms(const CompactSparseMatrix &compact_matrix, const VariablesInfo &variables_info, const BasisFactorization &basis_factorization)
void ComputeUpdateRow(RowIndex leaving_row)
Definition update_row.cc:87
const DenseBitRow & GetIsRelevantBitRow() const
double Density(const DenseRow &row)
Definition lp_utils.cc:176
Fractional Square(Fractional f)
Definition lp_utils.h:41
const DenseRow & Transpose(const DenseColumn &col)
Definition lp_utils.h:199
ColIndex RowToColIndex(RowIndex row)
Definition lp_types.h:54
void ClearAndResizeVectorWithNonZeros(IndexType size, ScatteredRowOrCol *v)
Definition lp_utils.h:290
Fractional PreciseSquaredNorm(const SparseColumn &v)
Definition lp_utils.cc:44
Fractional SquaredNorm(const SparseColumn &v)
Definition lp_utils.cc:36
StrictITIVector< RowIndex, Fractional > DenseColumn
Definition lp_types.h:380
StrictITIVector< ColIndex, Fractional > DenseRow
Definition lp_types.h:351
const ScatteredRow & TransposedView(const ScatteredColumn &c)
OR-Tools root namespace.
#define IF_STATS_ENABLED(instructions)
Definition stats.h:412
#define SCOPED_TIME_STAT(stats)
Definition stats.h:419
StrictITIVector< Index, Fractional > values