/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* */
/* This file is part of the program and library */
/* PaPILO --- Parallel Presolve for Integer and Linear Optimization */
/* */
/* Copyright (C) 2020-2022 Konrad-Zuse-Zentrum */
/* fuer Informationstechnik Berlin */
/* */
/* This program is free software: you can redistribute it and/or modify */
/* it under the terms of the GNU Lesser General Public License as published */
/* by the Free Software Foundation, either version 3 of the License, or */
/* (at your option) any later version. */
/* */
/* This program is distributed in the hope that it will be useful, */
/* but WITHOUT ANY WARRANTY; without even the implied warranty of */
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the */
/* GNU Lesser General Public License for more details. */
/* */
/* You should have received a copy of the GNU Lesser General Public License */
/* along with this program. If not, see . */
/* */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef _PAPILO_PARALLEL_COL_DETECTION_HPP_
#define _PAPILO_PARALLEL_COL_DETECTION_HPP_
#include "papilo/core/PresolveMethod.hpp"
#include "papilo/core/Problem.hpp"
#include "papilo/core/ProblemUpdate.hpp"
#include "papilo/misc/Hash.hpp"
#ifdef PAPILO_TBB
#include "papilo/misc/tbb.hpp"
#endif
#include "papilo/external/pdqsort/pdqsort.h"
namespace papilo
{
template
class ParallelColDetection : public PresolveMethod
{
struct SupportHashCompare
{
SupportHashCompare() = default;
static size_t
hash( const std::pair& row )
{
Hasher hasher( row.first );
const int* support = row.second;
for( int i = 0; i != row.first; ++i )
{
hasher.addValue( support[i] );
}
return hasher.getHash();
}
static bool
equal( const std::pair& row1,
const std::pair& row2 )
{
int length = row1.first;
if( length != row2.first )
return false;
return memcmp( static_cast( row1.second ),
static_cast( row2.second ),
length * sizeof( int ) ) == 0;
}
};
struct SupportHash
{
std::size_t
operator()( const std::pair& row ) const
{
return SupportHashCompare::hash( row );
}
};
struct SupportEqual
{
bool
operator()( const std::pair& row1,
const std::pair& row2 ) const
{
return SupportHashCompare::equal( row1, row2 );
}
};
void
findParallelCols( const Num& num, const int* bucket, int bucketSize,
const ConstraintMatrix& constMatrix,
const Vec& obj, const VariableDomains& domains,
Reductions& reductions );
void
computeColHashes( const ConstraintMatrix& constMatrix,
const Vec& obj, unsigned int* columnHashes );
void
computeSupportId( const ConstraintMatrix& constMatrix,
unsigned int* supportHashes );
public:
ParallelColDetection() : PresolveMethod()
{
this->setName( "parallelcols" );
this->setTiming( PresolverTiming::kMedium );
}
bool
initialize( const Problem& problem,
const PresolveOptions& presolveOptions ) override
{
if( presolveOptions.dualreds < 2 )
this->setEnabled( false );
return false;
}
PresolveStatus
execute( const Problem& problem,
const ProblemUpdate& problemUpdate, const Num& num,
Reductions& reductions, const Timer& timer ) override;
private:
int
determineBucketSize( int nColumns,
std::unique_ptr& supportid,
std::unique_ptr& coefficentHashes,
std::unique_ptr& column, int i );
bool
check_parallelity( const Num& num, const Vec& obj, int col1,
int length, const REAL* coefs1, int col2,
const REAL* coefs2 ) const;
bool
can_be_merged( const Num& num, const Vec& lbs,
const Vec& ubs, int col1, const REAL* coefs1,
const REAL* coefs2, const Vec& cflags ) const;
bool
determineOderingForZeroObj( REAL val1, REAL val2, int colpermCol1,
int colpermCol2 ) const;
};
#ifdef PAPILO_USE_EXTERN_TEMPLATES
extern template class ParallelColDetection;
extern template class ParallelColDetection;
extern template class ParallelColDetection;
#endif
template
void
ParallelColDetection::findParallelCols(
const Num& num, const int* bucket, int bucketSize,
const ConstraintMatrix& constMatrix, const Vec& obj,
const VariableDomains& domains, Reductions& reductions )
{
// TODO if bucketSize too large do gurobi trick
const Vec& cflags = domains.flags;
const Vec& lbs = domains.lower_bounds;
const Vec& ubs = domains.upper_bounds;
auto checkDomainsForHoles = [&]( int col1, int col2, const REAL& scale2 ) {
// test whether we need to check that the domain of the merged
// column has no holes
assert( cflags[col1].test( ColFlag::kIntegral ) );
assert( cflags[col2].test( ColFlag::kIntegral ) );
assert( !cflags[col1].test( ColFlag::kLbInf, ColFlag::kUbInf ) );
assert( !cflags[col2].test( ColFlag::kLbInf, ColFlag::kUbInf ) );
// compute the domains of the merged column
REAL mergeval = lbs[col2];
REAL mergeub = ubs[col2];
if( scale2 < 0 )
{
mergeval += scale2 * ubs[col1];
mergeub += scale2 * lbs[col1];
}
else
{
mergeval += scale2 * lbs[col1];
mergeub += scale2 * ubs[col1];
}
// scan domain of new variable for holes:
// test every value in domain of column 1 if it allows to
// find a value in the domain of column 2 to constitute the
// value the all values in the domain of the merged column.
// If no such value is found the columns cannot be merged
bool foundHole = false;
while( num.isLE( mergeval, mergeub ) )
{
// initialize col1val with the lower bound of column 1
REAL col1val = lbs[col1];
// test if col1val + scale2 * col2val = mergeval implies a
// value for col2val that is within the domain of column 2.
// If that is the case we can stop and increase mergeval by
// 1, otherwise we found a hole If that check failed for
// the current col1val we increase it by 1 the upper bound
// of column 1 is reached
foundHole = true;
while( num.isLE( col1val, ubs[col1] ) )
{
REAL col2val = mergeval - col1val * scale2;
if( num.isIntegral( col2val ) && num.isGE( col2val, lbs[col2] ) &&
num.isLE( col2val, ubs[col2] ) )
{
foundHole = false;
break;
}
col1val += 1;
}
// if a hole was found we can stop
if( foundHole )
break;
// test next value in domain
mergeval += 1;
}
return foundHole;
};
if( constMatrix.getColumnCoefficients( bucket[0] ).getLength() <= 1 )
return;
assert( !cflags[bucket[bucketSize - 1]].test( ColFlag::kInactive ) );
// only continuous columns
if( !cflags[bucket[bucketSize - 1]].test( ColFlag::kIntegral ) )
{
int col1 = bucket[0];
auto col1vec = constMatrix.getColumnCoefficients( col1 );
const int length = col1vec.getLength();
const REAL* coefs1 = col1vec.getValues();
for( int j = 1; j < bucketSize; ++j )
{
int col2 = bucket[j];
assert( !cflags[col2].test( ColFlag::kIntegral ) );
const REAL* coefs2 =
constMatrix.getColumnCoefficients( col2 ).getValues();
if( check_parallelity( num, obj, col1, length, coefs1, col2, coefs2 ) )
{
TransactionGuard tg{ reductions };
reductions.lockCol( col2 );
reductions.lockCol( col1 );
reductions.mark_parallel_cols( col2, col1 );
}
}
return;
}
// only integer columns
else if( cflags[bucket[0]].test( ColFlag::kIntegral ) )
{
// to avoid adding redundant tsxs in case of multiple parallel cols
// all tsxs only refer to the first col in the bucket IF it is possible
// to construct a tsxs between the first and the remaining ones
bool abort_after_first_loop = true;
int first_col_with_finite_bounds = -1;
for( int i = 0; i < bucketSize; i++ )
{
int col1 = bucket[i];
if( cflags[col1].test( ColFlag::kLbInf, ColFlag::kUbInf ) )
continue;
if( first_col_with_finite_bounds == -1 )
first_col_with_finite_bounds = i;
if( i == first_col_with_finite_bounds + 1 && abort_after_first_loop )
break;
auto col1vec = constMatrix.getColumnCoefficients( col1 );
const int length = col1vec.getLength();
const REAL* coefs1 = col1vec.getValues();
assert( cflags[col1].test( ColFlag::kIntegral ) );
for( int j = i + 1; j < bucketSize; ++j )
{
int col2 = bucket[j];
const REAL* coefs2 =
constMatrix.getColumnCoefficients( col2 ).getValues();
assert( cflags[col2].test( ColFlag::kIntegral ) );
assert( num.isLE( abs( coefs1[0] ), abs( coefs2[0] ) ) );
if( cflags[col2].test( ColFlag::kLbInf, ColFlag::kUbInf ) )
continue;
bool parallel = check_parallelity( num, obj, col1, length, coefs1,
col2, coefs2 );
if( parallel &&
!checkDomainsForHoles( col1, col2, coefs1[0] / coefs2[0] ) )
{
TransactionGuard tg{ reductions };
reductions.lockCol( col2 );
reductions.lockCol( col1 );
reductions.mark_parallel_cols( col2, col1 );
}
else
abort_after_first_loop = false;
}
}
return;
}
else
{
int col1 = bucket[0];
auto col1vec = constMatrix.getColumnCoefficients( col1 );
const int length = col1vec.getLength();
int continuous_cols;
assert( !cflags[col1].test( ColFlag::kIntegral ) );
{
const REAL* coefs1 = col1vec.getValues();
Vec> continuous_parallel_cols;
for( continuous_cols = 1; continuous_cols < bucketSize;
++continuous_cols )
{
int col2 = bucket[continuous_cols];
if( cflags[col2].test( ColFlag::kIntegral ) )
break;
const REAL* coefs2 =
constMatrix.getColumnCoefficients( col2 ).getValues();
if( check_parallelity( num, obj, col1, length, coefs1, col2,
coefs2 ) )
{
continuous_parallel_cols.emplace_back( col2, col1 );
}
}
// TODO: use the updated bounds of the cont parallel columns
int col2 = bucket[continuous_cols];
assert( cflags[col2].test( ColFlag::kIntegral ) );
const REAL* coefs2 =
constMatrix.getColumnCoefficients( col2 ).getValues();
if( ( can_be_merged( num, lbs, ubs, col1, coefs1, coefs2,
cflags ) ) &&
check_parallelity( num, obj, col1, length, coefs1, col2, coefs2 ) )
{
TransactionGuard tg{ reductions };
reductions.lockCol( col1 );
for( std::pair pair : continuous_parallel_cols )
reductions.lockCol( pair.first );
reductions.lockCol( col2 );
reductions.lockColBounds( col1 );
for( std::pair pair : continuous_parallel_cols )
reductions.mark_parallel_cols( pair.first, pair.second );
reductions.mark_parallel_cols( col1, col2 );
}
else
{
for( std::pair pair : continuous_parallel_cols )
{
TransactionGuard tg{ reductions };
reductions.lockCol( pair.first );
reductions.lockCol( pair.second );
reductions.mark_parallel_cols( pair.first, pair.second );
}
}
}
for( int int_cols = continuous_cols; int_cols < bucketSize; ++int_cols )
{
int int_col = bucket[int_cols];
auto int_col1vec = constMatrix.getColumnCoefficients( int_col );
assert( cflags[int_col].test( ColFlag::kIntegral ) );
assert( int_col1vec.getLength() == length );
const REAL* int_coefs1 = int_col1vec.getValues();
for( int j = int_cols + 1; j < bucketSize; ++j )
{
int col2 = bucket[j];
const REAL* coefs2 =
constMatrix.getColumnCoefficients( col2 ).getValues();
if( cflags[int_col].test( ColFlag::kLbInf, ColFlag::kUbInf ) ||
cflags[col2].test( ColFlag::kLbInf, ColFlag::kUbInf ) )
continue;
bool parallel = check_parallelity( num, obj, int_col, length,
int_coefs1, col2, coefs2 );
if( parallel && !checkDomainsForHoles(
int_col, col2, int_coefs1[0] / coefs2[0] ) )
{
TransactionGuard tg{ reductions };
reductions.lockCol( col2 );
reductions.lockCol( int_col );
reductions.mark_parallel_cols( col2, int_col );
}
}
}
return;
}
}
template
bool
ParallelColDetection::can_be_merged(
const Num& num, const Vec& lbs, const Vec& ubs, int col1,
const REAL* coefs1, const REAL* coefs2, const Vec& cflags ) const
{
return cflags[col1].test( ColFlag::kLbInf, ColFlag::kUbInf ) ||
!num.isLT(
abs( ( ubs[col1] - lbs[col1] ) * coefs1[0] / coefs2[0] ), 1 );
}
template
bool
ParallelColDetection::check_parallelity( const Num& num,
const Vec& obj, int col1,
int length, const REAL* coefs1,
int col2,
const REAL* coefs2 ) const
{
REAL scale = coefs1[0] / coefs2[0];
if( !num.isEq( obj[col1], obj[col2] * scale ) )
return false;
for( int k = 1; k < length; ++k )
if( !num.isEq( coefs1[k], coefs2[k] * scale ) )
return false;
return true;
}
template
void
ParallelColDetection::computeColHashes(
const ConstraintMatrix& constMatrix, const Vec& obj,
unsigned int* columnHashes )
{
#ifdef PAPILO_TBB
tbb::parallel_for(
tbb::blocked_range( 0, constMatrix.getNCols() ),
[&]( const tbb::blocked_range& r ) {
for( int i = r.begin(); i < r.end(); ++i )
#else
for( int i = 0; i< constMatrix.getNCols(); i++)
#endif
{
// compute hash-value for coefficients
auto columnCoefficients = constMatrix.getColumnCoefficients( i );
const REAL* values = columnCoefficients.getValues();
const int len = columnCoefficients.getLength();
Hasher hasher( len );
if( len > 1 )
{
// compute scale such that the first coefficient is
// positive 1/golden ratio. The choice of
// the constant is arbitrary and is used to make cases
// where two coefficients that are equal
// within epsilon get different values are
// more unlikely by choose some irrational number
REAL scale = REAL( 2.0 / ( 1.0 + sqrt( 5.0 ) ) ) / values[0];
// add scaled coefficients of other row
// entries to compute the hash
for( int j = 1; j != len; ++j )
{
hasher.addValue( Num::hashCode( values[j] * scale ) );
}
if( obj[i] != 0 )
hasher.addValue( Num::hashCode( obj[i] * scale ) );
}
columnHashes[i] = hasher.getHash();
}
#ifdef PAPILO_TBB
} );
#endif
}
template
void
ParallelColDetection::computeSupportId(
const ConstraintMatrix& constMatrix, unsigned int* supportHashes )
{
using SupportMap =
HashMap, int, SupportHash, SupportEqual>;
SupportMap supportMap(
static_cast( constMatrix.getNCols() * 1.1 ) );
for( int i = 0; i < constMatrix.getNCols(); ++i )
{
auto col = constMatrix.getColumnCoefficients( i );
int length = col.getLength();
const int* support = col.getIndices();
auto insResult =
supportMap.emplace( std::make_pair( length, support ), i );
if( insResult.second )
supportHashes[i] = i;
else // support already exists, use the previous support id
supportHashes[i] = insResult.first->second;
}
}
template
PresolveStatus
ParallelColDetection::execute( const Problem& problem,
const ProblemUpdate& problemUpdate,
const Num& num,
Reductions& reductions, const Timer& timer )
{
const auto& constMatrix = problem.getConstraintMatrix();
const auto& obj = problem.getObjective().coefficients;
const auto& cflags = problem.getColFlags();
const int ncols = constMatrix.getNCols();
const Vec& colperm = problemUpdate.getRandomColPerm();
PresolveStatus result = PresolveStatus::kUnchanged;
// get called less and less over time regardless of success since the
// presolver can be expensive otherwise
this->skipRounds( this->getNCalls() );
assert( ncols > 0 );
std::unique_ptr supportid{ new unsigned int[ncols] };
std::unique_ptr coefhash{ new unsigned int[ncols] };
std::unique_ptr col{ new int[ncols] };
#ifdef PAPILO_TBB
tbb::parallel_invoke(
[ncols, &col]() {
for( int i = 0; i < ncols; ++i )
col[i] = i;
},
[&constMatrix, &coefhash, &obj, this]() {
computeColHashes( constMatrix, obj, coefhash.get() );
},
[&constMatrix, &supportid, this]() {
computeSupportId( constMatrix, supportid.get() );
} );
#else
for( int i = 0; i < ncols; ++i )
col[i] = i;
computeColHashes( constMatrix, obj, coefhash.get() );
computeSupportId( constMatrix, supportid.get() );
#endif
pdqsort(
col.get(), col.get() + ncols,
[&]( int a, int b )
{
if( supportid[a] < supportid[b] ||
( supportid[a] == supportid[b] && coefhash[a] < coefhash[b] ) )
return true;
else if( !( supportid[a] == supportid[b] &&
coefhash[a] == coefhash[b] ) )
return false;
assert( supportid[a] == supportid[b] && coefhash[a] == coefhash[b] );
bool flag_a_integer = cflags[a].test( ColFlag::kIntegral );
bool flag_b_integer = cflags[b].test( ColFlag::kIntegral );
if( flag_a_integer != flag_b_integer )
return !flag_a_integer;
return // sort by scale factor
abs( obj[a] ) < abs( obj[b] ) ||
// sort by scale factor if obj is zero
( abs( obj[a] ) == abs( obj[b] ) && obj[a] == 0 &&
determineOderingForZeroObj(
constMatrix.getColumnCoefficients( a ).getValues()[0],
constMatrix.getColumnCoefficients( b ).getValues()[0],
colperm[a], colperm[b] ) ) ||
// sort by permutation
( abs( obj[a] ) == abs( obj[b] ) && obj[a] != 0 &&
colperm[a] < colperm[b] );
} );
for( int i = 0; i < ncols; )
{
int bucketSize =
determineBucketSize( ncols, supportid, coefhash, col, i );
// if more than one col is in the bucket find parallel cols
if( bucketSize > 1 )
{
findParallelCols( num, col.get() + i, bucketSize, constMatrix, obj,
problem.getVariableDomains(), reductions );
}
i = i + bucketSize;
}
if( reductions.getTransactions().size() > 0 )
result = PresolveStatus::kReduced;
return result;
}
template
int
ParallelColDetection::determineBucketSize(
int nColumns, std::unique_ptr& supportid,
std::unique_ptr& coefficentHashes,
std::unique_ptr& column, int i )
{
int j;
for( j = i + 1; j < nColumns; ++j )
{
if( coefficentHashes[column[i]] != coefficentHashes[column[j]] ||
supportid[column[i]] != supportid[column[j]] )
{
break;
}
}
assert( j > i );
return j - i;
}
template
bool
ParallelColDetection::determineOderingForZeroObj( REAL val1, REAL val2,
int colpermCol1, int colpermCol2 ) const
{
if(val1 == val2)
return colpermCol1 < colpermCol2;
return abs(val1) < abs(val2);
}
} // namespace papilo
#endif