/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/* */
/* 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_PRESOLVERS_SINGLETON_STUFFING_HPP_
#define _PAPILO_PRESOLVERS_SINGLETON_STUFFING_HPP_
#include "papilo/core/PresolveMethod.hpp"
#include "papilo/core/Problem.hpp"
#include "papilo/core/ProblemUpdate.hpp"
#include "papilo/misc/Num.hpp"
#include "papilo/misc/fmt.hpp"
namespace papilo
{
template
class SingletonStuffing : public PresolveMethod
{
public:
SingletonStuffing() : PresolveMethod()
{
this->setName( "stuffing" );
this->setTiming( PresolverTiming::kMedium );
}
virtual PresolveStatus
execute( const Problem& problem,
const ProblemUpdate& problemUpdate, const Num& num,
Reductions& reductions, const Timer& timer ) override;
};
#ifdef PAPILO_USE_EXTERN_TEMPLATES
extern template class SingletonStuffing;
extern template class SingletonStuffing;
extern template class SingletonStuffing;
#endif
template
PresolveStatus
SingletonStuffing::execute( const Problem& problem,
const ProblemUpdate& problemUpdate,
const Num& num,
Reductions& reductions, const Timer& timer )
{
const auto& domains = problem.getVariableDomains();
const auto& lower_bounds = domains.lower_bounds;
const auto& upper_bounds = domains.upper_bounds;
const auto& cflags = domains.flags;
const auto& activities = problem.getRowActivities();
const auto& singletonCols = problemUpdate.getSingletonCols();
const auto& constMatrix = problem.getConstraintMatrix();
const auto& lhs_values = constMatrix.getLeftHandSides();
const auto& rhs_values = constMatrix.getRightHandSides();
const auto& rflags = constMatrix.getRowFlags();
const auto& nrows = constMatrix.getNRows();
const auto& colsize = constMatrix.getColSizes();
const auto& rowsize = constMatrix.getRowSizes();
const auto& obj = problem.getObjective().coefficients;
PresolveStatus result = PresolveStatus::kUnchanged;
Vec rowsWithPenaltySingletons;
Vec penaltyVarCount( nrows );
auto handleEquation = [&]( int col, bool lbimplied, bool ubimplied,
const REAL& val, int row, bool impliedeq,
const REAL& side ) {
if( !impliedeq && rowsize[row] <= 1 )
return;
result = PresolveStatus::kReduced;
TransactionGuard tg{ reductions };
reductions.lockColBounds( col );
reductions.lockRow( row );
// if the equation is only implied, first change its side before doing
// the substitution
if( impliedeq )
{
if( rflags[row].test( RowFlag::kLhsInf ) )
{
assert( !rflags[row].test( RowFlag::kRhsInf ) );
reductions.changeRowLHS( row, side );
}
else
{
assert( rflags[row].test( RowFlag::kRhsInf ) );
reductions.changeRowRHS( row, side );
}
if( rowsize[row] <= 1 )
return;
}
// substitute the variable in the objective
reductions.substituteColInObjective( col, row );
// now check if the equation is redundant or needs to be modified
if( lbimplied && ubimplied )
{
// implied free -> just remove the equation completely
reductions.markRowRedundant( row );
}
else
{
assert( lbimplied || !cflags[col].test( ColFlag::kLbInf ) );
assert( ubimplied || !cflags[col].test( ColFlag::kUbInf ) );
// implied free only for one bound -> modify equation to be an
// inequality and remove the columns coefficient
reductions.changeMatrixEntry( row, col, 0 );
if( val < 0 )
{
// set the Bound first to infinity to avoid infeasiblty during bounds changes
if( lbimplied )
reductions.changeRowLHSInf( row );
if( ubimplied )
reductions.changeRowRHSInf( row );
if( !lbimplied && lower_bounds[col] != 0 )
reductions.changeRowLHS( row, side - lower_bounds[col] * val );
if(!ubimplied && upper_bounds[col] != 0 )
reductions.changeRowRHS( row, side - upper_bounds[col] * val );
}
else
{
// set the Bound first to infinity to avoid infeasiblty during bounds changes
if( lbimplied )
reductions.changeRowRHSInf( row );
if( ubimplied )
reductions.changeRowLHSInf( row );
if( !lbimplied && lower_bounds[col] != 0 )
reductions.changeRowRHS( row, side - lower_bounds[col] * val );
if( !ubimplied && upper_bounds[col] != 0 )
reductions.changeRowLHS( row, side - upper_bounds[col] * val );
}
}
};
for( int col : singletonCols )
{
assert( colsize[col] == 1 );
assert( constMatrix.getColumnCoefficients( col ).getLength() == 1 );
int row = constMatrix.getColumnCoefficients( col ).getIndices()[0];
const REAL& val = constMatrix.getColumnCoefficients( col ).getValues()[0];
assert( !constMatrix.isRowRedundant( row ) );
REAL lhs = lhs_values[row];
REAL rhs = rhs_values[row];
if( rflags[row].test( RowFlag::kEquation ) )
{
assert( !rflags[row].test( RowFlag::kLhsInf, RowFlag::kRhsInf ) );
// Found singleton column within an equation:
// Check if it is implied free on one bound. In that case the
// variable is substituted and the constraint stays as an inequality
// constraint. Otherwise it is equaivalent to implied free variable
// substitution.
if( rowsize[row] <= 1 )
continue;
bool lbimplied =
row_implies_LB( num, lhs, rhs, rflags[row],
activities[row], val, lower_bounds[col],
upper_bounds[col], cflags[col] );
if( !lbimplied && !problemUpdate.getPresolveOptions().removeslackvars )
continue;
bool ubimplied =
row_implies_UB( num, lhs, rhs, rflags[row],
activities[row], val, lower_bounds[col],
upper_bounds[col], cflags[col] );
if( !ubimplied &&
( !problemUpdate.getPresolveOptions().removeslackvars ||
( !lbimplied && obj[col] != 0 ) ) )
continue;
if( cflags[col].test( ColFlag::kIntegral ) )
{
bool unsuitableForSubstitution = false;
auto rowvec = constMatrix.getRowCoefficients( row );
const int* rowinds = rowvec.getIndices();
const REAL* rowvals = rowvec.getValues();
for( int i = 0; i != rowvec.getLength(); ++i )
{
if( rowinds[i] == col )
continue;
if( !cflags[rowinds[i]].test( ColFlag::kIntegral ) )
{
unsuitableForSubstitution = true;
break;
}
if( !num.isIntegral( rowvals[i] / val ) )
{
unsuitableForSubstitution = true;
break;
}
}
if( unsuitableForSubstitution )
continue;
}
handleEquation( col, lbimplied, ubimplied, val, row, false, rhs );
continue;
}
switch( problemUpdate.getPresolveOptions().dualreds )
{
case 0:
// no dual reductions allowed
continue;
case 1:
// only weak dual reductions allowed
if( obj[col] == 0 )
continue;
}
int nuplocks = 0;
int ndownlocks = 0;
count_locks( val, rflags[row], ndownlocks, nuplocks );
// ranged row -> not a singleton. @TODO: check if ranged row can be
// converted to an equation with dual infer technique below
if( nuplocks != 0 && ndownlocks != 0 )
continue;
if( ndownlocks == 0 && obj[col] >= 0 )
{
// dual fix to lower bound
if( cflags[col].test( ColFlag::kLbInf ) )
{
if( obj[col] != 0 )
return PresolveStatus::kUnbndOrInfeas;
continue;
}
TransactionGuard tg{ reductions };
reductions.lockCol( col );
reductions.fixCol( col, lower_bounds[col] );
result = PresolveStatus::kReduced;
continue;
}
if( nuplocks == 0 && obj[col] <= 0 )
{
// dual fix to upper bound
if( cflags[col].test( ColFlag::kUbInf ) )
{
if( obj[col] != 0 )
return PresolveStatus::kUnbndOrInfeas;
continue;
}
TransactionGuard tg{ reductions };
reductions.lockCol( col );
reductions.fixCol( col, upper_bounds[col] );
result = PresolveStatus::kReduced;
continue;
}
assert( ( obj[col] > 0 && ndownlocks == 1 && nuplocks == 0 ) ||
( obj[col] < 0 && ndownlocks == 0 && nuplocks == 1 ) );
// We are in the penalty case, check if the row can be converted to an
// euqation due to complementary slackness:
// This column is a singleton row in the dual space and therefore
// directly gives a bound on the dual variable associated with this
// row. If the bound implies that this dual variable cannot be zero,
// the row must hold with equality and this singleton can be handled as
// in the equation case above. Do not remember row for stuffing if
// successful
// remember that row contains at least one penalty singleton and should
// be considered for the singleton stuffing step
// for continuous columns we have a singleton row in the dual which
// directly implies a bound for the dualvariable of the primal row.
// If the dual bound implies the primal row to be an equation we can
// substitute the singleton variable as above in the equation case
if( !cflags[col].test( ColFlag::kIntegral ) )
{
bool duallbinf = true;
bool dualubinf = true;
assert( val != 0 );
REAL duallb = obj[col] / val;
REAL dualub = duallb;
bool lbimplied =
row_implies_LB( num, lhs, rhs, rflags[row],
activities[row], val, lower_bounds[col],
upper_bounds[col], cflags[col] );
bool ubimplied =
row_implies_UB( num, lhs, rhs, rflags[row],
activities[row], val, lower_bounds[col],
upper_bounds[col], cflags[col] );
if( lbimplied && ubimplied )
{
duallbinf = false;
dualubinf = false;
}
else if( lbimplied )
{
if( val > 0 )
duallbinf = false;
else
dualubinf = false;
}
else if( ubimplied )
{
if( val > 0 )
dualubinf = false;
else
duallbinf = false;
}
if( !duallbinf && num.isFeasGT( duallb, 0 ) )
{
bool removevar = true;
assert( !rflags[row].test( RowFlag::kLhsInf ) );
assert( rflags[row].test( RowFlag::kRhsInf ) || ! num.isEq(rhs, lhs) );
// check again if row implies more bounds with new right hand
// side
if( !lbimplied )
{
lbimplied = row_implies_LB(
num, lhs, lhs, RowFlag::kEquation,
activities[row], val, lower_bounds[col], upper_bounds[col],
cflags[col] );
if( !lbimplied &&
!problemUpdate.getPresolveOptions().removeslackvars )
removevar = false;
}
if( removevar && !ubimplied )
{
ubimplied = row_implies_UB(
num, lhs, lhs, RowFlag::kEquation,
activities[row], val, lower_bounds[col], upper_bounds[col],
cflags[col] );
if( !ubimplied &&
( !problemUpdate.getPresolveOptions().removeslackvars ||
( !lbimplied && obj[col] != 0 ) ) )
removevar = false;
}
if( removevar )
{
handleEquation( col, lbimplied, ubimplied, val, row, true, lhs );
}
else
{
// if the variable should not be removed, then just apply the
// dual reduction and change the constraint into an equation
result = PresolveStatus::kReduced;
TransactionGuard tg{ reductions };
reductions.lockColBounds( col );
reductions.lockRow( row );
reductions.changeRowRHS( row, lhs );
}
}
else if( !dualubinf && num.isFeasLT( dualub, 0 ) )
{
bool removevar = true;
assert( !rflags[row].test( RowFlag::kRhsInf ) );
assert( rflags[row].test( RowFlag::kLhsInf ) || rhs != lhs );
// check again if row implies more bounds with new left hand side
if( !lbimplied )
{
lbimplied = row_implies_LB(
num, rhs, rhs, RowFlag::kEquation,
activities[row], val, lower_bounds[col], upper_bounds[col],
cflags[col] );
if( !lbimplied &&
!problemUpdate.getPresolveOptions().removeslackvars )
removevar = false;
}
if( removevar && !ubimplied )
{
ubimplied = row_implies_UB(
num, rhs, rhs, RowFlag::kEquation,
activities[row], val, lower_bounds[col], upper_bounds[col],
cflags[col] );
if( !ubimplied &&
( !problemUpdate.getPresolveOptions().removeslackvars ||
( !lbimplied && obj[col] != 0 ) ) )
removevar = false;
}
if( removevar )
{
handleEquation( col, lbimplied, ubimplied, val, row, true, rhs );
}
else
{
// if the variable should not be removed, then just apply the
// dual reduction and change the constraint into an equation
result = PresolveStatus::kReduced;
TransactionGuard tg{ reductions };
reductions.lockColBounds( col );
reductions.lockRow( row );
reductions.changeRowLHS( row, rhs );
}
}
else
{
switch( penaltyVarCount[row] )
{
case 0:
penaltyVarCount[row] = 1;
break;
case 1:
penaltyVarCount[row] = 2;
rowsWithPenaltySingletons.push_back( row );
}
}
}
}
if( problemUpdate.getPresolveOptions().dualreds < 2 )
return result;
Vec> penaltyvars;
for( int row : rowsWithPenaltySingletons )
{
assert( rflags[row].test( RowFlag::kLhsInf ) ||
rflags[row].test( RowFlag::kRhsInf ) );
assert( !rflags[row].test( RowFlag::kLhsInf ) ||
!rflags[row].test( RowFlag::kRhsInf ) );
int scale;
REAL rhs;
if( rflags[row].test( RowFlag::kRhsInf ) )
{
rhs = -lhs_values[row];
scale = -1;
}
else
{
rhs = rhs_values[row];
scale = 1;
}
auto rowvec = constMatrix.getRowCoefficients( row );
const int* rowinds = rowvec.getIndices();
const REAL* rowvals = rowvec.getValues();
const int len = rowvec.getLength();
// TODO do singleton stuffing
bool suitable = true;
for( int i = 0; i != len; ++i )
{
int col = rowinds[i];
REAL coeff = scale * rowvals[i];
// if the column is a singleton and not integral it could be a
// penalty variable
if( colsize[col] == 1 && !cflags[col].test( ColFlag::kIntegral ) )
{
// for penalty variables adjust the right hand side as if it
// where complemented whith the cheapest bound
if( coeff > 0 && obj[col] < 0 &&
!cflags[col].test( ColFlag::kUbUseless ) )
{
rhs -= coeff * upper_bounds[col];
penaltyvars.emplace_back( col, std::move( coeff ) );
continue;
}
if( coeff < 0 && obj[col] > 0 &&
!cflags[col].test( ColFlag::kLbUseless ) )
{
rhs -= coeff * lower_bounds[col];
penaltyvars.emplace_back( col, std::move( coeff ) );
continue;
}
// handle the dual fix case here again to adjust the right hand
// side with the stronger values
if( coeff > 0 && obj[col] >= 0 &&
!cflags[col].test( ColFlag::kLbInf ) )
{
rhs -= coeff * lower_bounds[col];
continue;
}
if( coeff < 0 && obj[col] <= 0 &&
!cflags[col].test( ColFlag::kUbInf ) )
{
rhs -= coeff * upper_bounds[col];
continue;
}
}
// for non-singletons or singletons that where not handled in the
// case above treat them like every other variable i.e. subtract
// them with their largest contribution from the right hand side
if( coeff > 0 )
{
if( cflags[col].test( ColFlag::kUbUseless ) )
{
suitable = false;
break;
}
rhs -= coeff * upper_bounds[col];
}
else
{
if( cflags[col].test( ColFlag::kLbUseless ) )
{
suitable = false;
break;
}
rhs -= coeff * lower_bounds[col];
}
}
if( !suitable )
{
penaltyvars.clear();
continue;
}
pdqsort( penaltyvars.begin(), penaltyvars.end(),
[&]( const std::pair& c1,
const std::pair& c2 ) {
return ( obj[c1.first] / c1.second ) >
( obj[c2.first] / c2.second );
} );
std::size_t k = 0;
while( k < penaltyvars.size() && num.isFeasLT( rhs, 0 ) )
{
int col = penaltyvars[k].first;
const REAL& coeff = penaltyvars[k].second;
// for non-singletons or singletons that where not handled in the
// case above treat them like every other variable i.e. subtract
// them with their largest contribution from the right hand side
if( coeff > 0 )
{
if( cflags[col].test( ColFlag::kLbUseless ) )
{
suitable = false;
break;
}
rhs -= coeff * ( lower_bounds[col] - upper_bounds[col] );
}
else
{
if( cflags[col].test( ColFlag::kUbUseless ) )
{
suitable = false;
break;
}
rhs -= coeff * ( upper_bounds[col] - lower_bounds[col] );
}
++k;
}
if( suitable && k != penaltyvars.size() )
{
assert( num.isFeasGE( rhs, 0 ) );
while( k < penaltyvars.size() )
{
int col = penaltyvars[k].first;
const REAL& coeff = penaltyvars[k].second;
if( coeff < 0 )
reductions.fixCol( col, lower_bounds[col] );
else
reductions.fixCol( col, upper_bounds[col] );
++k;
}
}
penaltyvars.clear();
}
return result;
}
} // namespace papilo
#endif