4444
4545#include < raft/core/nvtx.hpp>
4646
47+ #include < unordered_map>
48+
4749namespace cuopt ::mathematical_optimization::mip {
4850
4951template <typename i_t , typename f_t >
@@ -137,8 +139,10 @@ papilo::Problem<f_t> build_papilo_problem(const optimization_problem_t<i_t, f_t>
137139 }
138140 }
139141
140- for (size_t i = 0 ; i < h_var_types.size (); ++i) {
141- builder.setColIntegral (i, h_var_types[i] == var_t ::INTEGER );
142+ if (category == problem_category_t ::MIP ) {
143+ for (size_t i = 0 ; i < h_var_types.size (); ++i) {
144+ builder.setColIntegral (i, h_var_types[i] == var_t ::INTEGER );
145+ }
142146 }
143147
144148 if (!h_constr_lb.empty () && !h_constr_ub.empty ()) {
@@ -563,8 +567,6 @@ void set_presolve_methods(papilo::Presolve<f_t>& presolver,
563567 presolver.addPresolveMethod (uptr (new papilo::SimpleProbing<f_t >()));
564568 presolver.addPresolveMethod (uptr (new papilo::ParallelRowDetection<f_t >()));
565569 presolver.addPresolveMethod (uptr (new papilo::ParallelColDetection<f_t >()));
566-
567- presolver.addPresolveMethod (uptr (new papilo::SingletonStuffing<f_t >()));
568570 presolver.addPresolveMethod (uptr (new papilo::DualFix<f_t >()));
569571 presolver.addPresolveMethod (uptr (new papilo::SimplifyInequalities<f_t >()));
570572 presolver.addPresolveMethod (uptr (new papilo::CliqueMerging<f_t >()));
@@ -575,6 +577,11 @@ void set_presolve_methods(papilo::Presolve<f_t>& presolver,
575577 presolver.addPresolveMethod (uptr (new papilo::Probing<f_t >()));
576578
577579 if (!dual_postsolve) {
580+ // SingletonStuffing causes dual crushing failures on:
581+ // tr12-30, ns1208400, gmu-35-50, dws008-01, neos-1445765,
582+ // neos-5107597-kakapo, rocI-4-11, traininstance2, traininstance6,
583+ // radiationm18-12-05, rococoB10-011000, b1c1s1
584+ presolver.addPresolveMethod (uptr (new papilo::SingletonStuffing<f_t >()));
578585 presolver.addPresolveMethod (uptr (new papilo::DualInfer<f_t >()));
579586 presolver.addPresolveMethod (uptr (new papilo::SimpleSubstitution<f_t >()));
580587 presolver.addPresolveMethod (uptr (new papilo::Sparsify<f_t >()));
@@ -877,6 +884,251 @@ void third_party_presolve_t<i_t, f_t>::uncrush_primal_solution(
877884 full_primal = std::move (full_sol.primal );
878885}
879886
887+ template <typename i_t , typename f_t >
888+ void third_party_presolve_t <i_t , f_t >::crush_primal_solution(
889+ const std::vector<f_t >& original_primal, std::vector<f_t >& reduced_primal) const
890+ {
891+ cuopt_expects (presolver_ == cuopt::mathematical_optimization::presolver_t ::Papilo,
892+ error_type_t ::RuntimeError,
893+ " Primal crushing is only supported for PaPILO presolve" );
894+ cuopt_assert (papilo_post_solve_storage_ != nullptr , " No postsolve storage available" );
895+ std::vector<f_t > unused_y, unused_z;
896+ std::vector<f_t > empty_vals;
897+ std::vector<i_t > empty_indices, empty_offsets;
898+ crush_primal_dual_solution (original_primal,
899+ {},
900+ reduced_primal,
901+ unused_y,
902+ {},
903+ unused_z,
904+ empty_vals,
905+ empty_indices,
906+ empty_offsets);
907+ }
908+
909+ /* *
910+ * Crush an original-space primal+dual solution into the presolved (reduced) space.
911+ *
912+ * This is the forward counterpart of Papilo's Postsolve::undo(). It replays
913+ * each presolve reduction in forward order to transform variable/dual values,
914+ * then projects onto the surviving columns/rows via origcol/origrow_mapping.
915+ *
916+ * Only two reductions actually transform survivor coordinates:
917+ * kParallelCol — merges x[col1] into x[col2]; survivor rc is z[col2] if
918+ * nonzero, else z[col1] / scale (inverse of PaPILO postsolve)
919+ * kRowBoundChangeForcedByRow — conditionally transfers y[deleted_row] → y[kept_row]
920+ */
921+ template <typename i_t , typename f_t >
922+ void third_party_presolve_t <i_t , f_t >::crush_primal_dual_solution(
923+ const std::vector<f_t >& x_original,
924+ const std::vector<f_t >& y_original,
925+ std::vector<f_t >& x_reduced,
926+ std::vector<f_t >& y_reduced,
927+ const std::vector<f_t >& z_original,
928+ std::vector<f_t >& z_reduced,
929+ const std::vector<f_t >& A_values,
930+ const std::vector<i_t >& A_indices,
931+ const std::vector<i_t >& A_offsets) const
932+ {
933+ cuopt_expects (presolver_ == cuopt::mathematical_optimization::presolver_t ::Papilo,
934+ error_type_t ::RuntimeError,
935+ " Crushing is only supported for PaPILO presolve" );
936+ cuopt_assert (papilo_post_solve_storage_ != nullptr , " No postsolve storage available" );
937+
938+ const auto & storage = *papilo_post_solve_storage_;
939+ const auto & types = storage.types ;
940+ const auto & indices = storage.indices ;
941+ const auto & values = storage.values ;
942+ const auto & start = storage.start ;
943+ const auto & num = storage.num ;
944+
945+ cuopt_assert ((int )x_original.size () == (int )storage.nColsOriginal , " " );
946+
947+ const bool crush_dual = !y_original.empty ();
948+ if (crush_dual) { cuopt_assert ((int )y_original.size () == (int )storage.nRowsOriginal , " " ); }
949+
950+ const bool crush_rc = !z_original.empty () && crush_dual;
951+ if (crush_rc) { cuopt_assert ((int )z_original.size () == (int )storage.nColsOriginal , " " ); }
952+
953+ std::vector<f_t > x (x_original.begin (), x_original.end ());
954+ std::vector<f_t > y (y_original.begin (), y_original.end ());
955+ std::vector<f_t > z (z_original.begin (), z_original.end ());
956+
957+ // Track current coefficient values for entries modified by kCoefficientChange,
958+ // so repeated changes to the same (row, col) are handled correctly.
959+ std::unordered_map<i_t , f_t > coeff_current;
960+
961+ const i_t n_cols_original = (i_t )storage.nColsOriginal ;
962+
963+ auto coeff_key = [&](int row, int col) -> i_t { return (i_t )row * n_cols_original + (i_t )col; };
964+
965+ auto get_coeff = [&](int row, int col) -> f_t {
966+ auto it = coeff_current.find (coeff_key (row, col));
967+ if (it != coeff_current.end ()) return it->second ;
968+ for (i_t p = A_offsets[row]; p < A_offsets[row + 1 ]; ++p) {
969+ if (A_indices[p] == col) return A_values[p];
970+ }
971+ return 0 ;
972+ };
973+
974+ for (int i = 0 ; i < (int )types.size (); ++i) {
975+ int first = start[i];
976+
977+ switch (types[i]) {
978+ case ReductionType::kParallelCol : {
979+ // Storage layout: [orig_col1, flags1, orig_col2, flags2, -1]
980+ // [col1lb, col1ub, col2lb, col2ub, col2scale]
981+ int col1 = indices[first];
982+ int col2 = indices[first + 2 ];
983+ const f_t & scale = values[first + 4 ];
984+ x[col2] += scale * x[col1];
985+ if (crush_rc) {
986+ // Inverse of Postsolve::apply_parallel_col_to_original_solution reduced-cost split.
987+ if (num.isZero (z[col2]) && !num.isZero (z[col1])) {
988+ cuopt_assert (!num.isZero (scale), " parallel column scale must be nonzero" );
989+ z[col2] = z[col1] / scale;
990+ }
991+ }
992+ break ;
993+ }
994+
995+ case ReductionType::kRowBoundChangeForcedByRow : {
996+ if (!crush_dual) break ;
997+ cuopt_assert (i >= 1 && types[i - 1 ] == ReductionType::kReasonForRowBoundChangeForcedByRow ,
998+ " kRowBoundChangeForcedByRow must be preceded by its reason record" );
999+
1000+ bool is_lhs = indices[first] == 1 ;
1001+ int row = (int )values[first];
1002+
1003+ int reason_first = start[i - 1 ];
1004+ int deleted_row = indices[reason_first + 1 ];
1005+ f_t factor = values[reason_first];
1006+ cuopt_assert (factor != 0 , " parallel row factor must be nonzero" );
1007+
1008+ // Forward rule: if the deleted row carried dual signal that the
1009+ // reverse would have attributed to the kept row, transfer it back.
1010+ f_t candidate = y[deleted_row] / factor;
1011+ bool sign_ok = is_lhs ? num.isGT (candidate, (f_t )0 ) : num.isLT (candidate, (f_t )0 );
1012+
1013+ if (sign_ok) {
1014+ f_t y_old = y[row];
1015+ y[row] = candidate;
1016+ // Maintain z = c - A^T y: propagate the y change into reduced costs
1017+ if (crush_rc) {
1018+ f_t delta_y = candidate - y_old;
1019+ for (i_t p = A_offsets[row]; p < A_offsets[row + 1 ]; ++p) {
1020+ f_t a = get_coeff (row, A_indices[p]);
1021+ z[A_indices[p]] -= delta_y * a;
1022+ }
1023+ }
1024+ }
1025+ break ;
1026+ }
1027+
1028+ case ReductionType::kCoefficientChange : {
1029+ if (!crush_rc) break ;
1030+ int row = indices[first];
1031+ int col = indices[first + 1 ];
1032+ f_t a_new = values[first];
1033+ f_t a_old = get_coeff (row, col);
1034+ coeff_current[coeff_key (row, col)] = a_new;
1035+ z[col] += (a_old - a_new) * y[row];
1036+ break ;
1037+ }
1038+
1039+ case ReductionType::kSubstitutedColWithDual : {
1040+ // Singleton substitution: column j is expressed via equality row k as
1041+ // x_j = (rhs_k - Σ_{l≠j} a_kl·x_l) / a_kj
1042+ // This changes the objective for every column l in row k:
1043+ // c_red[l] = c_orig[l] - (c_j / a_kj) · a_kl
1044+ // Adjust z accordingly: Δz[l] = -(a_kl / a_kj)·z[j] - a_kl·y[k]
1045+ if (!crush_rc) break ;
1046+ int row_k = indices[first]; // equality row (original space)
1047+ int row_length = (int )values[first];
1048+ // Row coefficients start at first+3
1049+ int row_coef_start = first + 3 ;
1050+ // Substituted column index is after the row coefficients
1051+ int col_j = indices[row_coef_start + row_length];
1052+
1053+ // Find a_kj (coefficient of col j in row k)
1054+ f_t a_kj = 0 ;
1055+ for (int p = 0 ; p < row_length; ++p) {
1056+ if (indices[row_coef_start + p] == col_j) {
1057+ a_kj = values[row_coef_start + p];
1058+ break ;
1059+ }
1060+ }
1061+ if (a_kj == 0 ) break ; // shouldn't happen
1062+
1063+ f_t z_j = z[col_j];
1064+ f_t y_k = y[row_k];
1065+
1066+ // Adjust z for each surviving column l in the equality row (l ≠ j)
1067+ for (int p = 0 ; p < row_length; ++p) {
1068+ int col_l = indices[row_coef_start + p];
1069+ if (col_l == col_j) continue ;
1070+ f_t a_kl = values[row_coef_start + p];
1071+ z[col_l] -= (a_kl / a_kj) * z_j + a_kl * y_k;
1072+ }
1073+ break ;
1074+ }
1075+
1076+ case ReductionType::kFixedCol : // Handled via projection
1077+ case ReductionType::kSubstitutedCol : // Col is dropped
1078+ case ReductionType::kFixedInfCol : // Col is dropped
1079+ case ReductionType::kVarBoundChange : // Noop
1080+ case ReductionType::kRedundantRow : // Noop
1081+ case ReductionType::kRowBoundChange : // Noop
1082+ case ReductionType::kReasonForRowBoundChangeForcedByRow : // Metadata for above
1083+ case ReductionType::kSaveRow : // Metadata
1084+ case ReductionType::kReducedBoundsCost : // Noop
1085+ case ReductionType::kColumnDualValue : // Column reduced-cost only
1086+ case ReductionType::kRowDualValue : // Handled via projection
1087+ break ;
1088+ // no default: case to let the compiler yell at us if a new reduction is later introduced
1089+ }
1090+ }
1091+
1092+ const auto & col_map = storage.origcol_mapping ;
1093+ const auto & row_map = storage.origrow_mapping ;
1094+
1095+ // Cancel contributions from removed rows. The original-space z was
1096+ // computed as z = c - A^T y over ALL rows. The reduced-space stationarity
1097+ // only involves surviving rows, so we must add back the terms from removed
1098+ // rows: z[j] += y[i] * a_{i,j} for every removed row i with materially nonzero y[i].
1099+ if (crush_rc) {
1100+ std::vector<bool > row_survives ((int )storage.nRowsOriginal , false );
1101+ for (size_t k = 0 ; k < row_map.size (); ++k) {
1102+ row_survives[row_map[k]] = true ;
1103+ }
1104+ for (int i = 0 ; i < (int )storage.nRowsOriginal ; ++i) {
1105+ if (row_survives[i] || num.isZero (y[i])) continue ;
1106+ for (i_t p = A_offsets[i]; p < A_offsets[i + 1 ]; ++p) {
1107+ z[A_indices[p]] += y[i] * get_coeff (i, A_indices[p]);
1108+ }
1109+ }
1110+ }
1111+
1112+ x_reduced.resize (col_map.size ());
1113+ for (size_t k = 0 ; k < col_map.size (); ++k) {
1114+ x_reduced[k] = x[col_map[k]];
1115+ }
1116+
1117+ if (crush_dual) {
1118+ y_reduced.resize (row_map.size ());
1119+ for (size_t k = 0 ; k < row_map.size (); ++k) {
1120+ y_reduced[k] = y[row_map[k]];
1121+ }
1122+ }
1123+
1124+ if (crush_rc) {
1125+ z_reduced.resize (col_map.size ());
1126+ for (size_t k = 0 ; k < col_map.size (); ++k) {
1127+ z_reduced[k] = z[col_map[k]];
1128+ }
1129+ }
1130+ }
1131+
8801132template <typename i_t , typename f_t >
8811133third_party_presolve_t <i_t , f_t >::~third_party_presolve_t ()
8821134{
0 commit comments