/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*                                                                           */
/*               This file is part of the program and library                */
/*    PaPILO --- Parallel Presolve for Integer and Linear Optimization       */
/*                                                                           */
/* Copyright (C) 2020-2025 Zuse Institute Berlin (ZIB)                       */
/*                                                                           */
/* 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.                                            */
/*                                                                           */
/* You should have received a copy of the Apache-2.0 license                 */
/* along with PaPILO; see the file LICENSE. If not visit scipopt.org.        */
/*                                                                           */
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

#ifndef _PAPILO_PRESOLVERS_CONSTRAINT_PROPAGATION_HPP_
#define _PAPILO_PRESOLVERS_CONSTRAINT_PROPAGATION_HPP_

#include "papilo/Config.hpp"
#include "papilo/core/PresolveMethod.hpp"
#include "papilo/core/Problem.hpp"
#include "papilo/core/ProblemUpdate.hpp"
#include "papilo/core/SingleRow.hpp"

namespace papilo
{

template <typename REAL>
class ConstraintPropagation : public PresolveMethod<REAL>
{
 public:
   ConstraintPropagation() : PresolveMethod<REAL>()
   {
      this->setName( "propagation" );
      this->setTiming( PresolverTiming::kFast );
      this->setArgument(ArgumentType::kPropagation);
   }

   /// todo how to communicate about postsolve information
   PresolveStatus
   execute( const Problem<REAL>& problem,
            const ProblemUpdate<REAL>& problemUpdate,
            const Num<REAL>& num, Reductions<REAL>& reductions,
            const Timer& timer, int& reason_of_infeasibility) override;
};

#ifdef PAPILO_USE_EXTERN_TEMPLATES
extern template class ConstraintPropagation<double>;
extern template class ConstraintPropagation<Quad>;
extern template class ConstraintPropagation<Rational>;
#endif

template <typename REAL>
PresolveStatus
ConstraintPropagation<REAL>::execute( const Problem<REAL>& problem,
                                      const ProblemUpdate<REAL>& problemUpdate,
                                      const Num<REAL>& num, Reductions<REAL>& reductions,
                                      const Timer& timer, int& reason_of_infeasibility){
   const auto& domains = problem.getVariableDomains();
   const auto& activities = problem.getRowActivities();
   const auto& changedactivities = problemUpdate.getChangedActivities();
   const auto& consMatrix = problem.getConstraintMatrix();
   const auto& lhsValues = consMatrix.getLeftHandSides();
   const auto& rhsValues = consMatrix.getRightHandSides();
   const auto& rflags = consMatrix.getRowFlags();

   PresolveStatus result = PresolveStatus::kUnchanged;

   // for LP constraint propagation we might want to weaken the bounds by some
   // small amount above the feasibility tolerance
   const REAL weaken_bounds =
       problem.getNumIntegralCols() == 0
           ? REAL(problemUpdate.getPresolveOptions().weakenlpvarbounds) * num.getFeasTol()
           : REAL{ 0.0 };

   // calculating the basis for variable tightening (not fixings) may lead in
   // the postsolving step to a solution that is not in a vertex. In this case a
   // crossover would be required is too expensive performance wise.
   // Exception: for infinity bounds set a finite bound that is worse that the best possible bound
   const bool skip_variable_tightening =
       problem.getNumIntegralCols() == 0 &&
       problemUpdate.getPresolveOptions().calculate_basis_for_dual;

   const REAL bound_tightening_offset =
       REAL(problemUpdate.getPresolveOptions().get_variable_bound_tightening_offset());

#ifndef PAPILO_TBB
   assert( problemUpdate.getPresolveOptions().runs_sequential() );
#endif

   if( problemUpdate.getPresolveOptions().runs_sequential() ||
       !problemUpdate.getPresolveOptions().constraint_propagation_parallel )
   {
      auto add_boundchange = [&]( BoundChange boundChange, int col, REAL val, int row ) {
         // do not accept huge values as bounds
         if( num.isHugeVal( val ) )
            return;

         if( boundChange == BoundChange::kLower )
         {
            assert( domains.flags[col].test( ColFlag::kLbInf ) ||
                    val > domains.lower_bounds[col] );

            if( domains.flags[col].test( ColFlag::kIntegral,
                                         ColFlag::kImplInt ) )
               val = num.feasCeil( val );

            if( !domains.flags[col].test( ColFlag::kUbInf ) )
            {
               // compute distance of new lower bound to the upper bound
               REAL bnddist = domains.upper_bounds[col] - val;

               // bound exceeded by more then feastol means infeasible
               if( num.isFeasLT( bnddist, 0 ) )
               {
                  result = PresolveStatus::kInfeasible;
                  return;
               }

               // if the upper bound is reached, or reached within tolerances
               // and the change of feasibility is also within tolerances fix to
               // the upper bound
               if( bnddist <= 0
                   || ( num.isLE( bnddist, 0 ) && num.isFeasLE( consMatrix.getMaxFeasChange( col, bnddist ), 0 ) ) )
               {
                  reductions.fixCol( col, domains.upper_bounds[col], row );
                  result = PresolveStatus::kReduced;
                  return;
               }
            }

            val -= weaken_bounds;
            if( domains.flags[col].test( ColFlag::kLbInf )
                || num.isFeasGT( (val - domains.lower_bounds[col]) / 1000, 0 ) )
            {
               if(!skip_variable_tightening)
               {
                  reductions.changeColLB( col, val, row );
                  result = PresolveStatus::kReduced;
               }
               else if( domains.flags[col].test( ColFlag::kLbInf ) )
               {
                  // in case of skip_variable_tightening set bounds to an
                  // infinite value if possible
                  REAL offset =  bound_tightening_offset * abs( val );
                  if( offset < bound_tightening_offset )
                     offset = bound_tightening_offset;
                  reductions.changeColLB( col, val - offset, row );
                  result = PresolveStatus::kReduced;
               }
            }
         }
         else
         {
            assert( boundChange == BoundChange::kUpper );
            assert( domains.flags[col].test( ColFlag::kUbInf ) ||
                    val < domains.upper_bounds[col] );

            if( domains.flags[col].test( ColFlag::kIntegral,
                                         ColFlag::kImplInt ) )
               val = num.feasFloor( val );

            if( !domains.flags[col].test( ColFlag::kLbInf ) )
            {
               // compute distance of new upper bound to the lower bound
               REAL bnddist = val - domains.lower_bounds[col];

               // bound exceeded by more then feastol means infeasible
               if( num.isFeasLT( bnddist, 0 ) )
               {
                  result = PresolveStatus::kInfeasible;
                  return;
               }

               // if the lower bound is reached, or reached within tolerances
               // and the change of feasibility is also within tolerances fix to
               // the lower bound
               if( bnddist <= 0
                   || ( num.isLE( bnddist, 0 ) && num.isFeasLE( consMatrix.getMaxFeasChange( col, bnddist ), 0 ) ) )
               {
                  reductions.fixCol( col, domains.lower_bounds[col], row );
                  result = PresolveStatus::kReduced;
                  return;
               }
            }

            val += weaken_bounds;
            if( domains.flags[col].test( ColFlag::kUbInf )
                || num.isFeasLT( (val - domains.upper_bounds[col]) / 1000, 0 ) )
            {
               if(!skip_variable_tightening)
               {
                  reductions.changeColUB( col, val, row );
                  result = PresolveStatus::kReduced;
               }
               else if( domains.flags[col].test( ColFlag::kUbInf ) )
               {
                  // in case of skip_variable_tightening set bounds to an
                  // infinite value if possible
                  REAL offset =  bound_tightening_offset * abs( val );
                  if( offset < bound_tightening_offset )
                     offset = bound_tightening_offset;
                  reductions.changeColUB( col, val + offset, row );
                  result = PresolveStatus::kReduced;
               }
            }
         }
      };
      for( int row : changedactivities )
      {
         if( reductions.size() >= problemUpdate.getPresolveOptions().max_reduction_seq )
            break;
         auto rowvec = consMatrix.getRowCoefficients( row );

         // in sequential mode no intermediate trivial presolving is performed
         if( consMatrix.isRowRedundant( row ) )
            continue;

         switch( rowvec.getLength() )
         {
         case 0:
            if( ( !rflags[row].test( RowFlag::kLhsInf ) &&
                  num.isFeasGT( lhsValues[row], 0 ) ) ||
                ( !rflags[row].test( RowFlag::kRhsInf ) &&
                  num.isFeasLT( rhsValues[row], 0 ) ) )
               result = PresolveStatus::kInfeasible;
            else
               reductions.markRowRedundant( row );
            break;
         case 1:
            // do nothing, singleton row presolver handles this bound change
            break;
         default:
            propagate_row( num, row, rowvec.getValues(), rowvec.getIndices(),
                           rowvec.getLength(), activities[row], lhsValues[row],
                           rhsValues[row], rflags[row], domains.lower_bounds,
                           domains.upper_bounds, domains.flags,
                           add_boundchange );
         }

         if( result == PresolveStatus::kInfeasible )
            break;
      }
   }
#ifdef PAPILO_TBB
   else
   {
      Vec<Reductions<REAL>> stored_reductions( changedactivities.size() );
      bool infeasible = false;
      tbb::parallel_for(
          tbb::blocked_range<unsigned int>( 0, changedactivities.size() ),
          [&]( const tbb::blocked_range<unsigned int>& r ) {
             for( unsigned int j = r.begin(); j < r.end(); ++j )
             {
                PresolveStatus local_status = PresolveStatus::kUnchanged;

                auto add_boundchange = [&]( BoundChange boundChange, int col,
                                            REAL val, int row ) {
                   // do not accept huge values as bounds
                   if( num.isHugeVal( val ) )
                      return;

                   if( boundChange == BoundChange::kLower )
                   {
                      assert( domains.flags[col].test( ColFlag::kLbInf ) ||
                              val > domains.lower_bounds[col] );

                      if( domains.flags[col].test( ColFlag::kIntegral,
                                                   ColFlag::kImplInt ) )
                         val = num.feasCeil( val );

                      if( !domains.flags[col].test( ColFlag::kUbInf ) )
                      {
                         // compute distance of new lower bound to the upper
                         // bound
                         REAL bnddist = domains.upper_bounds[col] - val;

                         // bound exceeded by more then feastol means infeasible
                         if( num.isFeasLT( bnddist, 0 ) )
                         {
                            local_status = PresolveStatus::kInfeasible;
                            return;
                         }

                         // if the upper bound is reached, or reached within
                         // tolerances and the change of feasibility is also
                         // within tolerances fix to the upper bound
                         if( bnddist <= 0
                             || ( num.isLE( bnddist, 0 ) && num.isFeasLE( consMatrix.getMaxFeasChange( col, bnddist ), 0 ) ) )
                         {
                            stored_reductions[j].fixCol(
                                col, domains.upper_bounds[col], row );
                            local_status = PresolveStatus::kReduced;
                            return;
                         }
                      }

                      val -= weaken_bounds;
                      if( domains.flags[col].test( ColFlag::kLbInf )
                          || num.isFeasGT( (val - domains.lower_bounds[col]) / 1000, 0 ) )
                      {
                         if( !skip_variable_tightening )
                         {
                            stored_reductions[j].changeColLB( col, val, row );
                            local_status = PresolveStatus::kReduced;
                         }
                         // in case of skip_variable_tightening set bounds to an
                         // infinite value if possible
                         else if( domains.flags[col].test( ColFlag::kLbInf ) )
                         {
                            // in case of skip_variable_tightening set bounds to an
                            // infinite value if possible
                            REAL offset =  bound_tightening_offset * abs( val );
                            if( offset < bound_tightening_offset )
                               offset = bound_tightening_offset;
                            stored_reductions[j].changeColLB( col, val - offset, row );
                            result = PresolveStatus::kReduced;
                         }
                      }
                   }
                   else
                   {
                      assert( boundChange == BoundChange::kUpper );
                      assert( domains.flags[col].test( ColFlag::kUbInf ) ||
                              val < domains.upper_bounds[col] );

                      if( domains.flags[col].test( ColFlag::kIntegral,
                                                   ColFlag::kImplInt ) )
                         val = num.feasFloor( val );

                      if( !domains.flags[col].test( ColFlag::kLbInf ) )
                      {
                         // compute distance of new upper bound to the lower
                         // bound
                         REAL bnddist = val - domains.lower_bounds[col];

                         // bound exceeded by more then feastol means infeasible
                         if( num.isFeasLT( bnddist, 0 ) )
                         {
                            local_status = PresolveStatus::kInfeasible;
                            return;
                         }

                         // if the lower bound is reached, or reached within
                         // tolerances and the change of feasibility is also
                         // within tolerances fix to the lower bound
                         if( bnddist <= 0
                             || ( num.isLE( bnddist, 0 ) && num.isFeasLE( consMatrix.getMaxFeasChange( col, bnddist ), 0 ) ) )
                         {
                            stored_reductions[j].fixCol(
                                col, domains.lower_bounds[col], row );
                            local_status = PresolveStatus::kReduced;
                            return;
                         }
                      }

                      val += weaken_bounds;
                      if( domains.flags[col].test( ColFlag::kUbInf )
                          || num.isFeasLT( (val - domains.upper_bounds[col]) / 1000, 0 ) )
                      {
                         if( !skip_variable_tightening )
                         {
                            stored_reductions[j].changeColUB( col, val, row );
                            local_status = PresolveStatus::kReduced;
                         }
                         else if( domains.flags[col].test( ColFlag::kUbInf ) )
                         {
                            // in case of skip_variable_tightening set bounds to an
                            // infinite value if possible
                            REAL offset =  bound_tightening_offset * abs( val );
                            if( offset < bound_tightening_offset )
                               offset = bound_tightening_offset;
                            stored_reductions[j].changeColUB( col, val + offset, row );
                            result = PresolveStatus::kReduced;
                         }
                      }
                   }
                };

                int row = changedactivities[j];
                auto rowvec = consMatrix.getRowCoefficients( row );

                assert( !consMatrix.isRowRedundant( row ) );

                switch( rowvec.getLength() )
                {
                case 0:
                   if( ( !rflags[row].test( RowFlag::kLhsInf ) &&
                         num.isFeasGT( lhsValues[row], 0 ) ) ||
                       ( !rflags[row].test( RowFlag::kRhsInf ) &&
                         num.isFeasLT( rhsValues[row], 0 ) ) )
                      local_status = PresolveStatus::kInfeasible;
                   else
                      reductions.markRowRedundant( row );
                   break;
                case 1:
                   // do nothing, singleton row presolver handles this bound
                   // change
                   break;
                default:
                   propagate_row( num, row, rowvec.getValues(), rowvec.getIndices(),
                                  rowvec.getLength(), activities[row],
                                  lhsValues[row], rhsValues[row], rflags[row],
                                  domains.lower_bounds, domains.upper_bounds,
                                  domains.flags, add_boundchange );
                }
                assert( local_status == PresolveStatus::kReduced ||
                        local_status == PresolveStatus::kInfeasible ||
                        local_status == PresolveStatus::kUnchanged );
                if( local_status == PresolveStatus::kInfeasible )
                   infeasible = true;
                else if( local_status == PresolveStatus::kReduced )
                {
                   result = PresolveStatus::kReduced;
                }
             }
          } );
      if( infeasible )
         return PresolveStatus::kInfeasible;
      else if( result == PresolveStatus::kUnchanged )
         return PresolveStatus::kUnchanged;

      for( int i = 0; i < (int) stored_reductions.size(); ++i )
      {
         Reductions<REAL> reds = stored_reductions[i];
         if( reds.size() > 0 )
         {
            for( const auto& reduction : reds.getReductions() )
            {
               reductions.add_reduction( reduction.row, reduction.col,
                                         reduction.newval );
            }
         }
      }
   }
#endif

   return result;
}

} // namespace papilo

#endif
