402 lines
17 KiB
C++
402 lines
17 KiB
C++
// Copyright 2010-2024 Google LLC
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#ifndef OR_TOOLS_GLOP_BASIS_REPRESENTATION_H_
|
|
#define OR_TOOLS_GLOP_BASIS_REPRESENTATION_H_
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "ortools/base/logging.h"
|
|
#include "ortools/glop/lu_factorization.h"
|
|
#include "ortools/glop/parameters.pb.h"
|
|
#include "ortools/glop/rank_one_update.h"
|
|
#include "ortools/glop/status.h"
|
|
#include "ortools/lp_data/lp_types.h"
|
|
#include "ortools/lp_data/permutation.h"
|
|
#include "ortools/lp_data/scattered_vector.h"
|
|
#include "ortools/lp_data/sparse.h"
|
|
#include "ortools/util/stats.h"
|
|
|
|
namespace operations_research {
|
|
namespace glop {
|
|
|
|
// An eta matrix E corresponds to the identity matrix except for one column e of
|
|
// index j. In particular, B.E is the matrix of the new basis obtained from B by
|
|
// replacing the j-th vector of B by B.e, note that this is exactly what happens
|
|
// during a "pivot" of the current basis in the simplex algorithm.
|
|
//
|
|
// E = [ 1 ... 0 e_0 0 ... 0
|
|
// ... ... ... ... ... ... ...
|
|
// 0 ... 1 e_{j-1} 0 ... 0
|
|
// 0 ... 0 e_j 0 ... 0
|
|
// 0 ... 0 e_{j+1} 1 ... 0
|
|
// ... ... ... ... ... ... ...
|
|
// 0 ... 0 e_{n-1} 0 ... 1 ]
|
|
//
|
|
// The inverse of the eta matrix is:
|
|
// E^{-1} = [ 1 ... 0 -e_0/e_j 0 ... 0
|
|
// ... ... ... ... ... ... ...
|
|
// 0 ... 1 -e_{j-1}/e_j 0 ... 0
|
|
// 0 ... 0 1/e_j 0 ... 0
|
|
// 0 ... 0 -e_{j+1}/e_j 1 ... 0
|
|
// ... ... ... ... ... ... ...
|
|
// 0 ... 0 -e_{n-1}/e_j 0 ... 1 ]
|
|
class EtaMatrix {
|
|
public:
|
|
EtaMatrix(ColIndex eta_col, const ScatteredColumn& direction);
|
|
|
|
// This type is neither copyable nor movable.
|
|
EtaMatrix(const EtaMatrix&) = delete;
|
|
EtaMatrix& operator=(const EtaMatrix&) = delete;
|
|
|
|
virtual ~EtaMatrix();
|
|
|
|
// Solves the system y.E = c, 'c' beeing the initial value of 'y'.
|
|
// Then y = c.E^{-1}, so y is equal to c except for
|
|
// y_j = (c_j - \sum_{i != j}{c_i * e_i}) / e_j.
|
|
void LeftSolve(DenseRow* y) const;
|
|
|
|
// Same as LeftSolve(), but 'pos' contains the non-zero positions of c. The
|
|
// order of the positions is not important, but there must be no duplicates.
|
|
// The values not in 'pos' are not used. If eta_col_ was not already in 'pos',
|
|
// it is added.
|
|
void SparseLeftSolve(DenseRow* y, ColIndexVector* pos) const;
|
|
|
|
// Solves the system E.d = a, 'a' beeing the initial value of 'd'.
|
|
// Then d = E^{-1}.a = [ a_0 - e_0 * a_j / e_j
|
|
// ...
|
|
// a_{j-1} - e_{j-1} * a_j / e_j
|
|
// a_j / e_j
|
|
// a_{j+1} - e_{j+1} * a_j / e_j
|
|
// ...
|
|
// a_{n-1} - e_{n-1} * a_j / e_j ]
|
|
void RightSolve(DenseColumn* d) const;
|
|
|
|
private:
|
|
// Internal RightSolve() and LeftSolve() implementations using either the
|
|
// dense or the sparse representation of the eta vector.
|
|
void LeftSolveWithDenseEta(DenseRow* y) const;
|
|
void LeftSolveWithSparseEta(DenseRow* y) const;
|
|
void RightSolveWithDenseEta(DenseColumn* d) const;
|
|
void RightSolveWithSparseEta(DenseColumn* d) const;
|
|
|
|
// If an eta vector density is smaller than this threshold, we use the
|
|
// sparse version of the Solve() functions rather than the dense version.
|
|
// TODO(user): Detect automatically a good parameter? 0.5 is a good value on
|
|
// the Netlib (I only did a few experiments though). Note that in the future
|
|
// we may not even keep the dense representation at all.
|
|
static const Fractional kSparseThreshold;
|
|
|
|
const ColIndex eta_col_;
|
|
const Fractional eta_col_coefficient_;
|
|
|
|
// Note that to optimize solves, the position eta_col_ is set to 0.0 and
|
|
// stored in eta_col_coefficient_ instead.
|
|
DenseColumn eta_coeff_;
|
|
SparseColumn sparse_eta_coeff_;
|
|
};
|
|
|
|
// An eta factorization corresponds to the product of k eta matrices,
|
|
// i.e. E = E_0.E_1. ... .E_{k-1}
|
|
// It is used to solve two systems:
|
|
// - E.d = a (where a is usually the entering column).
|
|
// - y.E = c (where c is usually the objective row).
|
|
class EtaFactorization {
|
|
public:
|
|
EtaFactorization();
|
|
|
|
// This type is neither copyable nor movable.
|
|
EtaFactorization(const EtaFactorization&) = delete;
|
|
EtaFactorization& operator=(const EtaFactorization&) = delete;
|
|
|
|
virtual ~EtaFactorization();
|
|
|
|
// Deletes all eta matrices.
|
|
void Clear();
|
|
|
|
// Updates the eta factorization, i.e. adds the new eta matrix defined by
|
|
// the leaving variable and the corresponding eta column.
|
|
void Update(ColIndex entering_col, RowIndex leaving_variable_row,
|
|
const ScatteredColumn& direction);
|
|
|
|
// Left solves all systems from right to left, i.e. y_i = y_{i+1}.(E_i)^{-1}
|
|
void LeftSolve(DenseRow* y) const;
|
|
|
|
// Same as LeftSolve(), but 'pos' contains the non-zero positions of c. The
|
|
// order of the positions is not important, but there must be no duplicates.
|
|
// The values not in 'pos' are not used. If eta_col_ was not already in 'pos',
|
|
// it is added.
|
|
void SparseLeftSolve(DenseRow* y, ColIndexVector* pos) const;
|
|
|
|
// Right solves all systems from left to right, i.e. E_i.d_{i+1} = d_i
|
|
void RightSolve(DenseColumn* d) const;
|
|
|
|
private:
|
|
std::vector<EtaMatrix*> eta_matrix_;
|
|
};
|
|
|
|
// A basis factorization is the product of an eta factorization and
|
|
// a L.U decomposition, i.e. B = L.U.E_0.E_1. ... .E_{k-1}
|
|
// It is used to solve two systems:
|
|
// - B.d = a where a is the entering column.
|
|
// - y.B = c where c is the objective row.
|
|
//
|
|
// To speed-up and improve stability the factorization is refactorized at least
|
|
// every 'refactorization_period' updates.
|
|
//
|
|
// This class does not take ownership of the underlying matrix and basis, and
|
|
// thus they must outlive this class (and keep the same address in memory).
|
|
class BasisFactorization {
|
|
public:
|
|
BasisFactorization(const CompactSparseMatrix* compact_matrix,
|
|
const RowToColMapping* basis);
|
|
|
|
// This type is neither copyable nor movable.
|
|
BasisFactorization(const BasisFactorization&) = delete;
|
|
BasisFactorization& operator=(const BasisFactorization&) = delete;
|
|
|
|
virtual ~BasisFactorization();
|
|
|
|
// Sets the parameters for this component.
|
|
void SetParameters(const GlopParameters& parameters) {
|
|
max_num_updates_ = parameters.basis_refactorization_period();
|
|
use_middle_product_form_update_ =
|
|
parameters.use_middle_product_form_update();
|
|
parameters_ = parameters;
|
|
lu_factorization_.SetParameters(parameters);
|
|
}
|
|
|
|
// Returns the column permutation used by the LU factorization.
|
|
// This call only makes sense if the basis was just refactorized.
|
|
const ColumnPermutation& GetColumnPermutation() const {
|
|
DCHECK(IsRefactorized());
|
|
return lu_factorization_.GetColumnPermutation();
|
|
}
|
|
|
|
// Sets the column permutation used by the LU factorization to the identity.
|
|
// Hense the Solve() results will be computed without this permutation.
|
|
// This call only makes sense if the basis was just refactorized.
|
|
void SetColumnPermutationToIdentity() {
|
|
DCHECK(IsRefactorized());
|
|
lu_factorization_.SetColumnPermutationToIdentity();
|
|
}
|
|
|
|
// Clears the factorization and resets it to an identity matrix of size given
|
|
// by matrix_.num_rows().
|
|
void Clear();
|
|
|
|
// Clears the factorization and initializes the class using the current
|
|
// matrix_ and basis_. This is fast if IsIdentityBasis() is true, otherwise
|
|
// it will trigger a refactorization and will return an error if the matrix
|
|
// could not be factorized.
|
|
ABSL_MUST_USE_RESULT Status Initialize();
|
|
|
|
// This mainly forward the call to LuFactorization::ComputeInitialBasis().
|
|
//
|
|
// Note that once this is called, one would need to call Initialize() to
|
|
// actually create the factorization. The only side effect of this is to
|
|
// update the deterministic time.
|
|
//
|
|
// TODO(user): This "double" factorization is a bit inefficient, and we should
|
|
// probably Initialize() right away the factorization with the new basis, but
|
|
// more code is needed for that. It is also not that easy also because we want
|
|
// to permute all the added slack first.
|
|
RowToColMapping ComputeInitialBasis(const std::vector<ColIndex>& candidates);
|
|
|
|
// Return the number of rows in the basis.
|
|
RowIndex GetNumberOfRows() const { return compact_matrix_.num_rows(); }
|
|
|
|
// Clears eta factorization and refactorizes LU.
|
|
// Nothing happens if this is called on an already refactorized basis.
|
|
// Returns an error if the matrix could not be factorized: i.e. not a basis.
|
|
ABSL_MUST_USE_RESULT Status Refactorize();
|
|
|
|
// Like Refactorize(), but do it even if IsRefactorized() is true.
|
|
// Call this if the underlying basis_ changed and Update() wasn't called.
|
|
ABSL_MUST_USE_RESULT Status ForceRefactorization();
|
|
|
|
// Returns true if the factorization was just recomputed.
|
|
bool IsRefactorized() const;
|
|
|
|
// Updates the factorization. The 'eta' column will be modified with a swap to
|
|
// avoid a copy (only if the standard eta update is used). Returns an error if
|
|
// the matrix could not be factorized: i.e. not a basis.
|
|
ABSL_MUST_USE_RESULT Status Update(ColIndex entering_col,
|
|
RowIndex leaving_variable_row,
|
|
const ScatteredColumn& direction);
|
|
|
|
// Left solves the system y.B = rhs, where y initially contains rhs.
|
|
void LeftSolve(ScatteredRow* y) const;
|
|
|
|
// Left solves the system y.B = e_j, where e_j has only 1 non-zero
|
|
// coefficient of value 1.0 at position 'j'.
|
|
void LeftSolveForUnitRow(ColIndex j, ScatteredRow* y) const;
|
|
|
|
// Same as LeftSolveForUnitRow() but does not update any internal data.
|
|
void TemporaryLeftSolveForUnitRow(ColIndex j, ScatteredRow* y) const;
|
|
|
|
// Right solves the system B.d = a where the input is the initial value of d.
|
|
void RightSolve(ScatteredColumn* d) const;
|
|
|
|
// Same as RightSolve() for matrix.column(col). This also exploits its
|
|
// sparsity.
|
|
void RightSolveForProblemColumn(ColIndex col, ScatteredColumn* d) const;
|
|
|
|
// Specialized version for ComputeTau() in DualEdgeNorms. This reuses an
|
|
// intermediate result of the last LeftSolveForUnitRow() in order to save a
|
|
// permutation if it is available. Note that the input 'a' should always be
|
|
// equal to the last result of LeftSolveForUnitRow() and will be used for a
|
|
// DCHECK() or if the intermediate result wasn't kept.
|
|
const DenseColumn& RightSolveForTau(const ScatteredColumn& a) const;
|
|
|
|
// Returns the norm of B^{-1}.a, this is a specific function because
|
|
// it is a bit faster and it avoids polluting the stats of RightSolve().
|
|
// It can be called only when IsRefactorized() is true.
|
|
Fractional RightSolveSquaredNorm(const ColumnView& a) const;
|
|
|
|
// Returns the norm of (B^T)^{-1}.e_row where e is an unit vector.
|
|
// This is a bit faster and avoids polluting the stats of LeftSolve().
|
|
// It can be called only when IsRefactorized() is true.
|
|
Fractional DualEdgeSquaredNorm(RowIndex row) const;
|
|
|
|
// Computes the condition number of B.
|
|
// For a given norm, this is the matrix norm times the norm of its inverse.
|
|
// A condition number greater than 1E7 will lead to precision problems.
|
|
Fractional ComputeOneNormConditionNumber() const;
|
|
Fractional ComputeInfinityNormConditionNumber() const;
|
|
Fractional ComputeInfinityNormConditionNumberUpperBound() const;
|
|
|
|
// Computes the 1-norm of B.
|
|
// The 1-norm |A| is defined as max_j sum_i |a_ij|
|
|
// http://en.wikipedia.org/wiki/Matrix_norm
|
|
Fractional ComputeOneNorm() const;
|
|
|
|
// Computes the infinity-norm of B.
|
|
// The infinity-norm |A| is defined as max_i sum_j |a_ij|
|
|
// http://en.wikipedia.org/wiki/Matrix_norm
|
|
Fractional ComputeInfinityNorm() const;
|
|
|
|
// Computes the 1-norm of the inverse of B.
|
|
// For this we iteratively solve B.x = e_j, where e_j is the jth unit vector.
|
|
// The result of this computation is the jth column of B^-1.
|
|
Fractional ComputeInverseOneNorm() const;
|
|
|
|
// Computes the infinity-norm of the inverse of B.
|
|
Fractional ComputeInverseInfinityNorm() const;
|
|
|
|
// Stats related function.
|
|
// Note that ResetStats() could be const, but until needed it is not to
|
|
// prevent anyone holding a const BasisFactorization& to call it.
|
|
std::string StatString() const {
|
|
return stats_.StatString() + lu_factorization_.StatString();
|
|
}
|
|
void ResetStats() { stats_.Reset(); }
|
|
|
|
// The deterministic time used by this class. It is incremented for each
|
|
// solve and each factorization.
|
|
double DeterministicTime() const;
|
|
|
|
// Returns the number of updates since last refactorization.
|
|
int NumUpdates() const { return num_updates_; }
|
|
|
|
private:
|
|
// Called by ForceRefactorization() or Refactorize() or Initialize().
|
|
Status ComputeFactorization();
|
|
|
|
// Return true if the submatrix of matrix_ given by basis_ is exactly the
|
|
// identity (without permutation).
|
|
bool IsIdentityBasis() const;
|
|
|
|
// Updates the factorization using the middle product form update.
|
|
// Qi Huangfu, J. A. Julian Hall, "Novel update techniques for the revised
|
|
// simplex method", 28 january 2013, Technical Report ERGO-13-0001
|
|
ABSL_MUST_USE_RESULT Status
|
|
MiddleProductFormUpdate(ColIndex entering_col, RowIndex leaving_variable_row);
|
|
|
|
// Increases the deterministic time for a solve operation with a vector having
|
|
// this number of non-zero entries (it can be an approximation).
|
|
void BumpDeterministicTimeForSolve(int num_entries) const;
|
|
|
|
// Stats about this class.
|
|
struct Stats : public StatsGroup {
|
|
Stats()
|
|
: StatsGroup("BasisFactorization"),
|
|
refactorization_interval("refactorization_interval", this) {}
|
|
IntegerDistribution refactorization_interval;
|
|
};
|
|
|
|
// Mutable because we track the running time of const method like
|
|
// RightSolve() and LeftSolve().
|
|
mutable Stats stats_;
|
|
GlopParameters parameters_;
|
|
|
|
// References to the basis subpart of the linear program matrix.
|
|
const CompactSparseMatrix& compact_matrix_;
|
|
const RowToColMapping& basis_;
|
|
|
|
// Middle form product update factorization and scratchpad_ used to construct
|
|
// new rank one matrices.
|
|
RankOneUpdateFactorization rank_one_factorization_;
|
|
mutable DenseColumn scratchpad_;
|
|
mutable std::vector<RowIndex> scratchpad_non_zeros_;
|
|
|
|
// This is used by RightSolveForTau(). It holds an intermediate result from
|
|
// the last LeftSolveForUnitRow() and also the final result of
|
|
// RightSolveForTau().
|
|
mutable ScatteredColumn tau_;
|
|
|
|
// Booleans controlling the interaction between LeftSolveForUnitRow() that may
|
|
// or may not keep its intermediate results for the optimized
|
|
// RightSolveForTau().
|
|
//
|
|
// tau_computation_can_be_optimized_ will be true iff LeftSolveForUnitRow()
|
|
// kept its intermediate result when it was called and the factorization
|
|
// didn't change since then. If it is true, then RightSolveForTau() can use
|
|
// this result for a faster computation.
|
|
//
|
|
// tau_is_computed_ is used as an heuristic by LeftSolveForUnitRow() to decide
|
|
// if it is worth keeping its intermediate result (which is sligthly slower).
|
|
// It is simply set to true by RightSolveForTau() and to false by
|
|
// LeftSolveForUnitRow(), this way the optimization will automatically switch
|
|
// itself on when switching from the primal simplex (where RightSolveForTau()
|
|
// is never called) to the dual where it is called after each
|
|
// LeftSolveForUnitRow(), and back off again in the other direction.
|
|
mutable bool tau_computation_can_be_optimized_;
|
|
mutable bool tau_is_computed_;
|
|
|
|
// Data structure to store partial solve results for the middle form product
|
|
// update. See LeftSolveForUnitRow() and RightSolveForProblemColumn(). We use
|
|
// two CompactSparseMatrix to have a better cache behavior when solving with
|
|
// the rank_one_factorization_.
|
|
mutable CompactSparseMatrix storage_;
|
|
mutable CompactSparseMatrix right_storage_;
|
|
mutable ColMapping left_pool_mapping_;
|
|
mutable ColMapping right_pool_mapping_;
|
|
|
|
bool use_middle_product_form_update_;
|
|
int max_num_updates_;
|
|
int num_updates_;
|
|
EtaFactorization eta_factorization_;
|
|
LuFactorization lu_factorization_;
|
|
|
|
// mutable because the Solve() functions are const but need to update this.
|
|
double last_factorization_deterministic_time_ = 0.0;
|
|
mutable double deterministic_time_;
|
|
};
|
|
|
|
} // namespace glop
|
|
} // namespace operations_research
|
|
|
|
#endif // OR_TOOLS_GLOP_BASIS_REPRESENTATION_H_
|