diff --git a/MSVisualStudio/v17/libCoinUtils/libCoinUtils.vcxproj b/MSVisualStudio/v17/libCoinUtils/libCoinUtils.vcxproj index 9d73af41..7cbf0819 100644 --- a/MSVisualStudio/v17/libCoinUtils/libCoinUtils.vcxproj +++ b/MSVisualStudio/v17/libCoinUtils/libCoinUtils.vcxproj @@ -163,6 +163,7 @@ + diff --git a/src/CoinBoundPropagation.cpp b/src/CoinBoundPropagation.cpp index 092162b5..20d1f679 100644 --- a/src/CoinBoundPropagation.cpp +++ b/src/CoinBoundPropagation.cpp @@ -103,6 +103,151 @@ static bool rowNeedsProcessing( } // namespace +// ── Combined scanner: binary knapsack pre-check + FBBT pass-1 data ─────────── +// +// Used instead of rowNeedsProcessing when the problem has non-binary variables. +// Scans all nonzeros without early exit (FBBT needs the complete scan) and +// simultaneously computes: +// - runKnapsack : whether the binary knapsack should process this iteration +// - fbbtUseful : whether FBBT pass 2 might tighten a non-binary bound +// - bEff : effective RHS for FBBT (mult * adjRhs, fixed vars discounted) +// - minAct : finite minimum activity (binary and finite non-binary vars) +// - nUnbounded : number of non-binary vars with infinite bound in constraint dir +// - unboundedK : row position of the single unbounded var (nUnbounded == 1) +// +// Binary variables are included in minAct so that FBBT arithmetic is correct +// (e.g. if a binary was fixed to 1 in this round its contribution is exact), +// but they are never tightened in the FBBT pass 2 — the knapsack handles them. +namespace { + +struct RowScanInfo { + bool runKnapsack; ///< binary knapsack should run this iteration + bool fbbtUseful; ///< FBBT can potentially tighten non-binary bounds + double bEff; ///< effective rhs for FBBT + double minAct; ///< finite minimum activity + int nUnbounded; ///< unbounded non-binary variable count (0 or 1) + int unboundedK; ///< row position of unbounded non-binary var (nUnbounded==1) +}; + +static RowScanInfo scanRow( + const int *idx, const double *coef, int nz, + double multiplier, double adjustedRhs, + const double *colLB, const double *colUB, const char *colType, + double primalTol, double infinity) +{ + using CT = CoinColumnType; + + RowScanInfo info; + info.runKnapsack = false; + info.fbbtUseful = true; + info.bEff = multiplier * adjustedRhs; + info.minAct = 0.0; + info.nUnbounded = 0; + info.unboundedK = -1; + + // Kahan compensated summation for minAct: prevents FP accumulation errors + // from making minAct overshoot its true mathematical value (which would + // produce over-tight FBBT bounds). Without this, scalar sequential FP + // summation on arm64 at -O1 can compute minAct slightly above the true + // value, causing newUB to fall below the reference optimal — an invalid + // bound that triggers wrong-optimal (the LP with this bound excludes the + // true optimal solution even though no individual cut row is invalid). + double minActComp = 0.0; + // Helper: add val into info.minAct using Kahan compensation. + auto minActAdd = [&](double val) { + double y = val - minActComp; + double t = info.minAct + y; + minActComp = (t - info.minAct) - y; + info.minAct = t; + }; + + // Binary check state — mirrors rowNeedsProcessing but without early exit. + double binaryRhs = multiplier * adjustedRhs; + double maxCoef = 0.0; + bool seenPositiveC = false; + bool binaryUnbounded = false; // a free non-binary makes binary fixing impossible + + for (int k = 0; k < nz; ++k) { + const int col = idx[k]; + const double c = coef[k] * multiplier; + if (c == 0.0) + continue; + + const double lb = colLB[col], ub = colUB[col]; + + if (lb == ub) { + // Fixed variable: discount from both binary rhs and FBBT bEff. + binaryRhs -= c * lb; + info.bEff -= c * lb; + continue; + } + + const char ct = colType[col]; + + if (ct == CT::SemiContinuous || ct == CT::SemiInteger) { + // Semi-continuous/semi-integer: skip FBBT for this row; + // let processRow handle (and abort) the semi variable. + info.fbbtUseful = false; + info.runKnapsack = true; + return info; + } + + if (ct == CT::Continuous || ct == CT::GeneralInteger) { + // Non-binary: update FBBT min activity and check for unbounded contribution. + if (c > 0.0) { + if (lb <= -infinity) { + binaryUnbounded = true; + if (++info.nUnbounded == 1) + info.unboundedK = k; + else + info.fbbtUseful = false; // nUnbounded >= 2 → skip FBBT + } else { + minActAdd(c * lb); + binaryRhs -= c * lb; // discount for binary check + } + } else { // c < 0 + if (ub >= infinity) { + binaryUnbounded = true; + if (++info.nUnbounded == 1) + info.unboundedK = k; + else + info.fbbtUseful = false; + } else { + minActAdd(c * ub); + binaryRhs -= c * ub; // discount for binary check + } + } + continue; + } + + // Binary variable: include in FBBT min activity (tightening handled by knapsack). + // c > 0: lb = 0 (free binary) → contributes 0; c < 0: ub = 1 → contributes c. + if (c > 0.0) + minActAdd(c * lb); + else + minActAdd(c * ub); + + // Binary check (mirrors rowNeedsProcessing without early exit). + if (c >= 0.0) { + seenPositiveC = true; + maxCoef = std::max(maxCoef, c); + } else { + binaryRhs += (-c); // complementation + maxCoef = std::max(maxCoef, -c); + } + } + + // Binary knapsack should run when: a free non-binary doesn't block fixings AND + // either the effective RHS is already negative (infeasibility) or the largest + // coefficient exceeds the RHS (some binary can be fixed). + if (!binaryUnbounded) + info.runKnapsack = (binaryRhs < -primalTol) || (maxCoef > binaryRhs + primalTol); + + return info; +} + +} // namespace + CoinBoundPropagation::CoinBoundPropagation( int numCols, const char *colType, @@ -114,12 +259,21 @@ CoinBoundPropagation::CoinBoundPropagation( const double *rowRange, double primalTolerance, double infinity, - int maxRowNz) + int maxRowNz, + bool collectCases, + bool nonBinaryFBBT, + const std::vector< bool > *dirtyRowsFBBT, + const double *cachedRowMinAct, + const double *cachedRowMaxAct, + const int *cachedRowNUnbLB, + const int *cachedRowNUnbUB, + const bool *rowHasBinary) : newBounds_() , infeasible_(false) , infeasibleRow_(-1) , infeasibleCol_(-1) , complete_(true) + , nContinuousTightened_(0) { // Mutable copies of column bounds updated as fixings are propagated. std::vector< double > mutableLB(colLB, colLB + numCols); @@ -144,6 +298,59 @@ CoinBoundPropagation::CoinBoundPropagation( double multipliers[2]; double rhsAdjustments[2]; + // Pre-check: if the problem has any non-binary variable AND non-binary FBBT + // is enabled, we take the FBBT path (scanRow instead of rowNeedsProcessing) + // so that activity arithmetic can tighten continuous/general-integer bounds. + bool hasNonBinary = false; + if (nonBinaryFBBT) { + // If the caller supplies a dirty-rows hint, it was built from a non-binary + // check on the problem — non-binary vars exist iff the dirty array is non-null. + if (dirtyRowsFBBT != nullptr) { + hasNonBinary = true; // caller wouldn't pass a dirty hint for pure-binary problems + } else { + for (int j = 0; j < numCols && !hasNonBinary; ++j) + if (colType[j] != CoinColumnType::Binary) + hasNonBinary = true; + } + } + + // Per-column touch flag: set when FBBT tightens a non-binary bound. + // Only allocated when needed; avoids overhead for pure-binary problems. + std::vector< bool > fbbtTouched; + if (hasNonBinary) + fbbtTouched.assign(static_cast< size_t >(numCols), false); + + // Per-row flag: does this row contain at least one non-binary variable? + // When the caller supplies dirtyRowsFBBT, it already encodes "is non-binary + // AND was touched last round", so we skip the O(nnz) scan and use the hint + // directly. When no hint is given (first round / binary-only fallback), we + // compute it ourselves. + std::vector< bool > rowHasNonBinary; + if (hasNonBinary && dirtyRowsFBBT == nullptr) { + rowHasNonBinary.assign(nRows, false); + for (size_t r = 0; r < nRows; ++r) { + const CoinBigIndex rs = start[r]; + const int len = length[r]; + for (int k = 0; k < len; ++k) { + if (colType[idxs[rs + k]] != CoinColumnType::Binary) { + rowHasNonBinary[r] = true; + break; + } + } + } + } + + // Helper: should row r use the full scanRow() + FBBT path? + // true → scan row for FBBT activity arithmetic + // false → use cheap rowNeedsProcessing() for knapsack only + auto rowNeedsFBBT = [&](size_t r) -> bool { + if (!hasNonBinary) + return false; + if (dirtyRowsFBBT != nullptr) + return (*dirtyRowsFBBT)[r]; + return rowHasNonBinary[r]; + }; + #ifdef COIN_BT_STATS rowStats_.reserve(nRows); #endif @@ -177,68 +384,123 @@ CoinBoundPropagation::CoinBoundPropagation( bool rowSkipped = (numIter == 0); for (int it = 0; it < numIter; ++it) { - // Mathematical pre-check: skip this iteration if no fixing is possible. - // This is always sound — it never suppresses a real fixing or infeasibility. - if (!rowNeedsProcessing(rowIdxs, rowCoefs, rowLength, - multipliers[it], rhsAdjustments[it], - mColLB, mColUB, colType, - primalTolerance, infinity)) { - rowSkipped = true; - continue; + bool doKnapsack; + RowScanInfo scan; // only valid when row has non-binary vars + bool usedCachedScan = false; + + if (rowNeedsFBBT(idxRow)) { + // Cached fast path: when the caller provides per-row activity caches + // and this row has no binary variables, skip the O(L) scanRow() and + // compute the slack in O(1) from the pre-computed cached values. + if (cachedRowMinAct != nullptr && rowHasBinary != nullptr + && !rowHasBinary[idxRow] + && multipliers[it] > 0) { + // Cached fast path for pure non-binary ≤ constraints only. + // For ≥ constraints (mult < 0) we fall through to scanRow so that + // in-round Gauss-Seidel updates are included — the Jacobi cached + // maxAct is often stale for the ≥ direction, giving very weak bounds. + const int nUnb = cachedRowNUnbLB[idxRow]; + if (nUnb == 0) { + // All variable lower bounds finite: O(1) slack for ≤ direction. + scan.bEff = rhsAdjustments[it]; // mult == 1, so mult*adj == adj + scan.minAct = cachedRowMinAct[idxRow]; + scan.nUnbounded = 0; + scan.fbbtUseful = true; + scan.runKnapsack = false; + usedCachedScan = true; + } + } + if (!usedCachedScan) { + scan = scanRow(rowIdxs, rowCoefs, static_cast< int >(rowLength), + multipliers[it], rhsAdjustments[it], + mColLB, mColUB, colType, primalTolerance, infinity); + } + doKnapsack = scan.runKnapsack; + if (!doKnapsack && !scan.fbbtUseful) { + rowSkipped = true; + continue; + } + } else { + // Fast path: row has only binary vars, or is clean this round. + doKnapsack = rowNeedsProcessing(rowIdxs, rowCoefs, rowLength, + multipliers[it], rhsAdjustments[it], + mColLB, mColUB, colType, primalTolerance, infinity); + if (!doKnapsack) { + rowSkipped = true; + continue; + } } - knapsackRow.processRow( - rowIdxs, rowCoefs, rowLength, rowSense, - multipliers[it], rhsAdjustments[it]); - - // Skip rows that are unbounded or have no binary variables. - if (knapsackRow.isUnbounded()) { - rowSkipped = true; - continue; + // ── Snapshot for case collection ────────────────────────────────────── + // Taken at scan time (before processRow or FBBT modify mColLB/mColUB). + // snapLB[k] / snapUB[k] are indexed by row position, matching scanRow's + // input and thus consistent with scan.bEff / scan.minAct. + std::vector< double > snapLB, snapUB; + double caseBEff = 0.0; + if (collectCases) { + snapLB.resize(rowLength); + snapUB.resize(rowLength); + for (size_t k = 0; k < rowLength; ++k) { + snapLB[k] = mColLB[rowIdxs[k]]; + snapUB[k] = mColUB[rowIdxs[k]]; + } + if (rowNeedsFBBT(idxRow)) { + caseBEff = scan.bEff; + } else { + // Binary-only path: compute bEff by discounting fixed vars. + caseBEff = multipliers[it] * rhsAdjustments[it]; + for (size_t k = 0; k < rowLength; ++k) + if (snapLB[k] == snapUB[k]) + caseBEff -= (rowCoefs[k] * multipliers[it]) * snapLB[k]; + } } - rowSkipped = false; + // Helper: build a CoinBPCase from the scan-time snapshot. + // targetRowPos is the row position (0-based) of the tightened variable. + // Fixed variables (snapLB == snapUB) are excluded from vars[] since they + // are already discounted in caseBEff. + auto buildCase = [&](size_t targetRowPos, + double oldLB, double oldUB, + double claimedBound, bool isUB, + bool isBinaryFix) -> CoinBPCase { + CoinBPCase cas; + cas.beff = caseBEff; + cas.rowIdx = static_cast< int >(idxRow); + cas.isUB = isUB; + cas.isBinaryFix = isBinaryFix; + cas.oldLB = oldLB; + cas.oldUB = oldUB; + cas.claimedBound = claimedBound; + cas.tightenedIdx = -1; + for (size_t k = 0; k < rowLength; ++k) { + if (snapLB[k] == snapUB[k]) + continue; // fixed: skip (discounted in caseBEff) + if (k == targetRowPos) + cas.tightenedIdx = static_cast< int >(cas.vars.size()); + cas.vars.push_back({ rowCoefs[k] * multipliers[it], + snapLB[k], snapUB[k], + static_cast< int >(colType[rowIdxs[k]]) }); + } + return cas; + }; + + if (doKnapsack) { + knapsackRow.processRow( + rowIdxs, rowCoefs, rowLength, rowSense, + multipliers[it], rhsAdjustments[it]); + + // Skip rows that are unbounded or have no binary variables. + if (knapsackRow.isUnbounded()) { + rowSkipped = true; + continue; + } - const double rhs = knapsackRow.rhs(); + rowSkipped = false; - // Direct row infeasibility: effective RHS is finite but strictly negative. - if (std::isfinite(rhs) && rhs < -primalTolerance) { -#ifdef COIN_BT_STATS - { - const auto statsT1 = std::chrono::high_resolution_clock::now(); - rowStats_.push_back({idxRow, - std::chrono::duration< double >(statsT1 - statsT0).count(), - newBounds_.size() - fixingsBefore, false}); - } -#endif - infeasibleRow_ = static_cast< int >(idxRow); - infeasible_ = true; - return; - } + const double rhs = knapsackRow.rhs(); - const size_t nFixed = knapsackRow.nFixedVariables(); - if (nFixed == 0) - continue; - - const int *fixedVars = knapsackRow.fixedVariables(); - for (size_t fi = 0; fi < nFixed; ++fi) { - const int rawIdx = fixedVars[fi]; - // rawIdx < numCols → original variable must be 0 - // rawIdx >= numCols → complemented, so original must be 1 - const int origCol = (rawIdx < numCols) ? rawIdx : rawIdx - numCols; - const int newVal = (rawIdx < numCols) ? 0 : 1; - - if (fixedTo[origCol] == -1) { - // First time this variable is fixed. - fixedTo[origCol] = newVal; - const double lb = static_cast< double >(newVal); - const double ub = static_cast< double >(newVal); - newBounds_.push_back( - std::make_pair(static_cast< size_t >(origCol), - std::make_pair(lb, ub))); - mColLB[origCol] = lb; - mColUB[origCol] = ub; - } else if (fixedTo[origCol] != newVal) { + // Direct row infeasibility: effective RHS is finite but strictly negative. + if (std::isfinite(rhs) && rhs < -primalTolerance) { #ifdef COIN_BT_STATS { const auto statsT1 = std::chrono::high_resolution_clock::now(); @@ -247,14 +509,200 @@ CoinBoundPropagation::CoinBoundPropagation( newBounds_.size() - fixingsBefore, false}); } #endif - // Contradictory fixing: same variable implied to be both 0 and 1. - infeasibleCol_ = origCol; - infeasibleRow_ = static_cast< int >(idxRow); // row that found the conflict + infeasibleRow_ = static_cast< int >(idxRow); infeasible_ = true; return; } - // If fixedTo[origCol] == newVal the fixing is a duplicate; skip. - } + + const size_t nFixed = knapsackRow.nFixedVariables(); + if (nFixed > 0) { + const int *fixedVars = knapsackRow.fixedVariables(); + for (size_t fi = 0; fi < nFixed; ++fi) { + const int rawIdx = fixedVars[fi]; + // rawIdx < numCols → original variable must be 0 + // rawIdx >= numCols → complemented, so original must be 1 + const int origCol = (rawIdx < numCols) ? rawIdx : rawIdx - numCols; + const int newVal = (rawIdx < numCols) ? 0 : 1; + + if (fixedTo[origCol] == -1) { + // First time this variable is fixed. + fixedTo[origCol] = newVal; + const double lb = static_cast< double >(newVal); + const double ub = static_cast< double >(newVal); + + if (collectCases && !snapLB.empty()) { + size_t rowPos = rowLength; // sentinel + for (size_t k2 = 0; k2 < rowLength; ++k2) + if (rowIdxs[k2] == origCol) { rowPos = k2; break; } + if (rowPos < rowLength) + // fixing to 0: UB lowered (isUB=true); fixing to 1: LB raised (isUB=false) + bpCases_.push_back(buildCase(rowPos, + snapLB[rowPos], snapUB[rowPos], + static_cast< double >(newVal), + newVal == 0, /*isBinaryFix=*/true)); + } + + newBounds_.push_back( + std::make_pair(static_cast< size_t >(origCol), + std::make_pair(lb, ub))); + mColLB[origCol] = lb; + mColUB[origCol] = ub; + } else if (fixedTo[origCol] != newVal) { +#ifdef COIN_BT_STATS + { + const auto statsT1 = std::chrono::high_resolution_clock::now(); + rowStats_.push_back({idxRow, + std::chrono::duration< double >(statsT1 - statsT0).count(), + newBounds_.size() - fixingsBefore, false}); + } +#endif + // Contradictory fixing: same variable implied to be both 0 and 1. + infeasibleCol_ = origCol; + infeasibleRow_ = static_cast< int >(idxRow); + infeasible_ = true; + return; + } + // If fixedTo[origCol] == newVal the fixing is a duplicate; skip. + } + } + } // doKnapsack + + // ── FBBT pass 2: tighten non-binary bounds using activity arithmetic ── + // Runs only when the combined scan found at least one non-binary variable + // that FBBT might tighten. Binary variables are intentionally skipped + // here — they are handled (more powerfully) by the knapsack above. + if (hasNonBinary && rowNeedsFBBT(idxRow) && scan.fbbtUseful) { + using CT = CoinColumnType; + double slack = scan.bEff - scan.minAct; + rowSkipped = false; + + if (scan.nUnbounded == 0) { + // Infeasibility: finite min activity exceeds effective RHS. + // If we used the cached (Jacobi) minAct and it looks infeasible, + // re-verify with a fresh scanRow before declaring infeasibility — + // the cache can drift from the true sum over many incremental rounds. + if (slack < -primalTolerance && usedCachedScan) { + scan = scanRow(rowIdxs, rowCoefs, static_cast< int >(rowLength), + multipliers[it], rhsAdjustments[it], + mColLB, mColUB, colType, primalTolerance, infinity); + slack = scan.bEff - scan.minAct; + } + if (slack < -primalTolerance) { +#ifdef COIN_BT_STATS + { + const auto statsT1 = std::chrono::high_resolution_clock::now(); + rowStats_.push_back({idxRow, + std::chrono::duration< double >(statsT1 - statsT0).count(), + newBounds_.size() - fixingsBefore, false}); + } +#endif + infeasibleRow_ = static_cast< int >(idxRow); + infeasible_ = true; + return; + } + + // Standard case: tighten every non-fixed, non-binary variable. + // Guard: only tighten when slack is strictly positive. When slack ≤ 0 + // (borderline infeasible within tolerance), newUB = lb + slack/c ≤ lb + // which would produce UB < LB — numerically invalid. The infeasibility + // check above already handles slack < -primalTolerance; here we skip the + // tightening loop for the [−primalTolerance, 0] band. + if (slack <= 0.0) + goto skip_tightening_nUnb0; + for (int k = 0; k < static_cast< int >(rowLength); ++k) { + const int col = rowIdxs[k]; + const double c = rowCoefs[k] * multipliers[it]; + if (c == 0.0) + continue; + const char ct = colType[col]; + // Knapsack handles binary; FBBT skips binary and semi. + if (ct == CT::Binary || ct == CT::SemiContinuous || ct == CT::SemiInteger) + continue; + if (mColLB[col] == mColUB[col]) + continue; // fixed + const bool isInt = (ct == CT::GeneralInteger); + if (c > 0.0) { + double newUB = mColLB[col] + slack / c; + if (isInt) + newUB = std::floor(newUB + primalTolerance); + if (newUB < mColUB[col] - primalTolerance) { + if (newUB < mColLB[col] - primalTolerance) { + infeasibleRow_ = static_cast< int >(idxRow); + infeasible_ = true; + return; + } + if (collectCases && !snapLB.empty()) + bpCases_.push_back(buildCase(static_cast< size_t >(k), + snapLB[k], snapUB[k], newUB, /*isUB=*/true, /*isBin=*/false)); + mColUB[col] = newUB; + fbbtTouched[static_cast< size_t >(col)] = true; + } + } else { // c < 0 + double newLB = mColUB[col] + slack / c; + if (isInt) + newLB = std::ceil(newLB - primalTolerance); + if (newLB > mColLB[col] + primalTolerance) { + if (newLB > mColUB[col] + primalTolerance) { + infeasibleRow_ = static_cast< int >(idxRow); + infeasible_ = true; + return; + } + if (collectCases && !snapLB.empty()) + bpCases_.push_back(buildCase(static_cast< size_t >(k), + snapLB[k], snapUB[k], newLB, /*isUB=*/false, /*isBin=*/false)); + mColLB[col] = newLB; + fbbtTouched[static_cast< size_t >(col)] = true; + } + } + } + skip_tightening_nUnb0:; + } else { + // nUnbounded == 1: tighten only the one unbounded non-binary variable. + const int k1 = scan.unboundedK; + const int col = rowIdxs[k1]; + const double c = rowCoefs[k1] * multipliers[it]; + const char ct = colType[col]; + if (ct != CT::Binary && ct != CT::SemiContinuous && ct != CT::SemiInteger + && mColLB[col] != mColUB[col]) { + const bool isInt = (ct == CT::GeneralInteger); + if (c > 0.0) { + // lb = -inf; derive new UB: x ≤ slack / c + double newUB = slack / c; + if (isInt) + newUB = std::floor(newUB + primalTolerance); + if (newUB < mColUB[col] - primalTolerance) { + if (newUB < mColLB[col] - primalTolerance) { + infeasibleRow_ = static_cast< int >(idxRow); + infeasible_ = true; + return; + } + if (collectCases && !snapLB.empty()) + bpCases_.push_back(buildCase(static_cast< size_t >(k1), + snapLB[k1], snapUB[k1], newUB, /*isUB=*/true, /*isBin=*/false)); + mColUB[col] = newUB; + fbbtTouched[static_cast< size_t >(col)] = true; + } + } else { // c < 0, ub = +inf + // derive new LB: x ≥ slack / c (flipped: c < 0) + double newLB = slack / c; + if (isInt) + newLB = std::ceil(newLB - primalTolerance); + if (newLB > mColLB[col] + primalTolerance) { + if (newLB > mColUB[col] + primalTolerance) { + infeasibleRow_ = static_cast< int >(idxRow); + infeasible_ = true; + return; + } + if (collectCases && !snapLB.empty()) + bpCases_.push_back(buildCase(static_cast< size_t >(k1), + snapLB[k1], snapUB[k1], newLB, /*isUB=*/false, /*isBin=*/false)); + mColLB[col] = newLB; + fbbtTouched[static_cast< size_t >(col)] = true; + } + } + } + } + } // FBBT pass 2 } // row iterations #ifdef COIN_BT_STATS @@ -267,4 +715,16 @@ CoinBoundPropagation::CoinBoundPropagation( } #endif } // all rows + + // Collect non-binary FBBT tightenings into newBounds_. + // These are appended after all binary fixings so nFixings() (which subtracts + // nContinuousTightened_) remains correct. + if (hasNonBinary) { + for (int j = 0; j < numCols; ++j) { + if (fbbtTouched[static_cast< size_t >(j)]) { + newBounds_.push_back({ static_cast< size_t >(j), { mColLB[j], mColUB[j] } }); + ++nContinuousTightened_; + } + } + } } diff --git a/src/CoinBoundPropagation.hpp b/src/CoinBoundPropagation.hpp index 989dee7f..4894fc60 100644 --- a/src/CoinBoundPropagation.hpp +++ b/src/CoinBoundPropagation.hpp @@ -22,6 +22,46 @@ class CoinPackedMatrix; +/** + * @brief One bound-tightening event captured for offline verification. + * + * Each case corresponds to a single bound change produced by FBBT + * (non-binary variables) or the binary knapsack. + * + * The constraint is stored in ≤ form with the multiplier already applied: + * Σ vars[k].coef * x[k] ≤ beff + * + * Variable bounds in vars[] are snapshotted at scan time (before any + * in-row modifications), which is the same point at which beff was + * computed — so an LP built from this data is self-consistent. + * + * Verification LP: + * If isUB: maximize x[tightenedIdx] s.t. row constraint + bounds + * → LP optimal should equal claimedBound (± tol; floor for integers) + * If !isUB: minimize x[tightenedIdx] s.t. row constraint + bounds + * → LP optimal should equal claimedBound (± tol; ceil for integers) + * + * isBinaryFix distinguishes knapsack binary fixings (case a) from FBBT + * tightenings (cases b/c/d in the user-facing categorisation). + */ +struct CoinBPCase { + struct Var { + double coef; ///< multiplier-adjusted coefficient + double lb; ///< lower bound at scan time + double ub; ///< upper bound at scan time + int type; ///< 0=continuous, 1=binary, 2=general-integer + }; + std::vector< Var > vars; ///< all row variables (bounds at scan time) + double beff; ///< effective RHS (fixed vars discounted) + int tightenedIdx; ///< index into vars[] of the tightened variable + double oldLB; ///< lower bound of tightened var before event + double oldUB; ///< upper bound of tightened var before event + double claimedBound; ///< new UB (isUB=true) or new LB (isUB=false) + bool isUB; ///< true → UB was tightened; false → LB was tightened + bool isBinaryFix; ///< true → binary knapsack; false → FBBT + int rowIdx; ///< constraint row index (informational) +}; + #ifdef COIN_BT_STATS /** * @brief Per-row statistics collected when COIN_BT_STATS is defined. @@ -111,7 +151,15 @@ class COINUTILSLIB_EXPORT CoinBoundPropagation { const double *rowRange, double primalTolerance = 1e-7, double infinity = 1e50, - int maxRowNz = -1); + int maxRowNz = -1, + bool collectCases = false, + bool nonBinaryFBBT = true, + const std::vector< bool > *dirtyRowsFBBT = nullptr, + const double *cachedRowMinAct = nullptr, + const double *cachedRowMaxAct = nullptr, + const int *cachedRowNUnbLB = nullptr, + const int *cachedRowNUnbUB = nullptr, + const bool *rowHasBinary = nullptr); ~CoinBoundPropagation() = default; @@ -137,8 +185,20 @@ class COINUTILSLIB_EXPORT CoinBoundPropagation { return newBounds_; } - /** @brief Number of variable fixings discovered. */ - size_t nFixings() const { return newBounds_.size(); } + /** @brief Number of variable fixings discovered (binary variables only). */ + size_t nFixings() const + { + return newBounds_.size() - static_cast< size_t >(nContinuousTightened_); + } + + /** + * @brief Number of non-binary variable bounds tightened by FBBT. + * + * Counts continuous and general-integer bounds tightened during + * the activity-based FBBT pass integrated into this class. + * Always zero when the problem has no non-binary variables. + */ + int nContinuousTightened() const { return nContinuousTightened_; } /** * @brief True if the bound-tightening pass detected infeasibility. @@ -182,6 +242,15 @@ class COINUTILSLIB_EXPORT CoinBoundPropagation { */ bool isComplete() const { return complete_; } + /** + * @brief Bound-tightening cases collected for verification. + * + * Non-empty only when the constructor was called with collectCases=true. + * Each entry is a self-contained LP verification problem: build the LP + * from the vars/beff data and check that the LP optimal equals claimedBound. + */ + const std::vector< CoinBPCase > &bpCases() const { return bpCases_; } + #ifdef COIN_BT_STATS /** * @brief Per-row statistics (only available when compiled with COIN_BT_STATS). @@ -199,6 +268,8 @@ class COINUTILSLIB_EXPORT CoinBoundPropagation { int infeasibleRow_; ///< row that triggered infeasibility (-1 if none / unknown) int infeasibleCol_; ///< column in contradictory fixing (-1 if none / unknown) bool complete_; ///< false if any row was hard-skipped via maxRowNz + int nContinuousTightened_; ///< non-binary bounds tightened by FBBT (0 when !hasNonBinary) + std::vector< CoinBPCase > bpCases_; ///< empty unless collectCases=true #ifdef COIN_BT_STATS std::vector< CoinBTRowStats > rowStats_; #endif diff --git a/src/CoinBuildInfo.cpp b/src/CoinBuildInfo.cpp new file mode 100644 index 00000000..af513b61 --- /dev/null +++ b/src/CoinBuildInfo.cpp @@ -0,0 +1,36 @@ +// Copyright (C) 2026, COIN-OR. All Rights Reserved. +// This code is licensed under the terms of the Eclipse Public License (EPL). + +#include "CoinUtilsConfig.h" +#include "CoinBuildInfo.hpp" + +namespace CoinBuildInfo { + +bool hasZlib() +{ +#ifdef COINUTILS_HAS_ZLIB + return true; +#else + return false; +#endif +} + +bool hasBzlib() +{ +#ifdef COINUTILS_HAS_BZLIB + return true; +#else + return false; +#endif +} + +bool hasLapack() +{ +#ifdef COINUTILS_HAS_LAPACK + return true; +#else + return false; +#endif +} + +} // namespace CoinBuildInfo diff --git a/src/CoinBuildInfo.hpp b/src/CoinBuildInfo.hpp new file mode 100644 index 00000000..0eb247b1 --- /dev/null +++ b/src/CoinBuildInfo.hpp @@ -0,0 +1,32 @@ +// Copyright (C) 2026, COIN-OR. All Rights Reserved. +// This code is licensed under the terms of the Eclipse Public License (EPL). + +#ifndef CoinBuildInfo_H +#define CoinBuildInfo_H + +#include "CoinUtilsConfig.h" + +/** \brief Runtime queries for optional third-party packages that CoinUtils + was compiled with. + + CoinUtils' own build configuration (zlib/bzip2/LAPACK detection) is + recorded in an internal, non-installed header, so it isn't visible to + downstream projects (Osi, Clp, Cgl, Cbc) that only see the public, + installed CoinUtilsConfig.h. These small runtime accessors let those + projects (e.g. Cbc's `-version` command) report which optional features + are actually present in the CoinUtils library they are linked against. +*/ +namespace CoinBuildInfo { + +/// True if this CoinUtils build supports reading/writing gzip (.gz) files. +COINUTILSLIB_EXPORT bool hasZlib(); + +/// True if this CoinUtils build supports reading/writing bzip2 (.bz2) files. +COINUTILSLIB_EXPORT bool hasBzlib(); + +/// True if this CoinUtils build was linked against a LAPACK implementation. +COINUTILSLIB_EXPORT bool hasLapack(); + +} // namespace CoinBuildInfo + +#endif diff --git a/src/CoinCliqueExtender.cpp b/src/CoinCliqueExtender.cpp index 865f9d9d..6630b6c7 100644 --- a/src/CoinCliqueExtender.cpp +++ b/src/CoinCliqueExtender.cpp @@ -57,6 +57,8 @@ CoinCliqueExtender::CoinCliqueExtender(const CoinConflictGraph *cgraph, size_t e iv_ = std::vector(cgSize); iv2_ = std::vector(cgSize); + raw_ = std::vector(cgSize); + candidatesRanked_ = false; extMethod_ = extMethod; rc_ = rc; @@ -144,7 +146,7 @@ bool CoinCliqueExtender::randomExtension(const size_t *clqIdxs, const size_t clq assert(clqSize > 0); #endif - fillCandidates(clqIdxs, clqSize); + fillCandidates(clqIdxs, clqSize, NULL); if (nCandidates_ == 0) { return false; @@ -186,15 +188,19 @@ bool CoinCliqueExtender::greedySelection(const size_t *clqIdxs, const size_t clq assert(clqSize > 0); #endif - fillCandidates(clqIdxs, clqSize); + fillCandidates(clqIdxs, clqSize, costs); if (nCandidates_ == 0) { return false; } - const size_t n = nCandidates_; - nCandidates_ = std::min(nCandidates_, maxCandidates_); - std::partial_sort(candidates_.begin(), candidates_.begin() + nCandidates_, candidates_.begin() + n, CompareCost(costs)); + if (!candidatesRanked_) { + // Fewer survivors than the cap, so every one was verified and they are + // still in node order; rank them here as before. + const size_t n = nCandidates_; + nCandidates_ = std::min(nCandidates_, maxCandidates_); + std::partial_sort(candidates_.begin(), candidates_.begin() + nCandidates_, candidates_.begin() + n, CompareCost(costs)); + } for (size_t i = 0; i < nCandidates_; i++) { /* need to have conflict with all nodes in clique */ @@ -269,7 +275,8 @@ bool CoinCliqueExtender::extendClique(const size_t *clqIdxs, const size_t clqSiz return result; } -void CoinCliqueExtender::fillCandidates(const size_t *clqIdxs, const size_t clqSize) { +void CoinCliqueExtender::fillCandidates(const size_t *clqIdxs, const size_t clqSize, + const double *costs) { const size_t cgSize = cgraph_->size(); size_t nodeSD = 0, minDegree = cgSize; @@ -292,6 +299,12 @@ void CoinCliqueExtender::fillCandidates(const size_t *clqIdxs, const size_t clqS const std::pair rescg = cgraph_->conflictingNodes(nodeSD, candidates_.data(), iv2_.data()); + // Cheap filters first: iv_ and the reduced-cost cap cost one array read each, + // while the "conflicts with every clique element" test below costs up to + // clqSize calls to conflicting(), two binary searches apiece. On the slow + // fixtures clqSize is 400-500 and rescg.first is tens of thousands, so that + // test is where essentially all of separation time goes. + size_t nRaw = 0; for (size_t i = 0; i < rescg.first; i++) { const size_t node = rescg.second[i]; @@ -303,6 +316,27 @@ void CoinCliqueExtender::fillCandidates(const size_t *clqIdxs, const size_t clqS continue; } + raw_[nRaw++] = node; + } + + // The caller keeps only the maxCandidates_ cheapest survivors, so verifying + // the rest is pure waste. Verifying in cost order and stopping at the cap + // selects that same set: CompareCost is a strict weak ordering, so "the + // cheapest k of all verified" and "verify in cost order, take the first k" + // agree. + // No clique-size gate: a threshold sweep (0/2/4/8/16 on the eight fixtures + // that dominate separation time) found ungated to be best everywhere, and the + // short-clique fixtures that appeared to regress -- bnatt400, uct-subprob -- + // are unchanged when timed serially. Those apparent slowdowns were load noise + // from a 12-way parallel harness, on fixtures whose absolute times are ~10ms. + candidatesRanked_ = (costs != NULL) && (nRaw > maxCandidates_); + if (candidatesRanked_) { + std::sort(raw_.begin(), raw_.begin() + nRaw, CompareCost(costs)); + } + const size_t cap = candidatesRanked_ ? maxCandidates_ : nRaw; + + for (size_t i = 0; i < nRaw && nCandidates_ < cap; i++) { + const size_t node = raw_[i]; bool insert = true; for (size_t j = 0; j < clqSize; j++) { if (!cgraph_->conflicting(node, clqIdxs[j])) { diff --git a/src/CoinCliqueExtender.hpp b/src/CoinCliqueExtender.hpp index ba218777..ad979d90 100644 --- a/src/CoinCliqueExtender.hpp +++ b/src/CoinCliqueExtender.hpp @@ -85,10 +85,16 @@ class COINUTILSLIB_EXPORT CoinCliqueExtender { /** * Construct a list of candidates to enter the clique. * + * Candidates are verified in @p costs order and verification stops once + * maxCandidates_ of them have been accepted, since the caller keeps only that + * many. Sets candidatesRanked_ to say whether that happened. + * * @param clqIdxs indexes of the clique to be extended * @param clqSize size of the clique to be extended + * @param costs ranking used by the caller, or NULL for an unranked (random) + * selection, in which case every survivor is verified **/ - void fillCandidates(const size_t *clqIdxs, const size_t clqSize); + void fillCandidates(const size_t *clqIdxs, const size_t clqSize, const double *costs); /** * Randomly select the vertices to be inserted in the clique. @@ -155,6 +161,18 @@ class COINUTILSLIB_EXPORT CoinCliqueExtender { **/ std::vector iv_, iv2_; + /** + * Nodes surviving the cheap filters in fillCandidates, before the expensive + * conflicts-with-every-clique-element verification. + **/ + std::vector raw_; + + /** + * True when fillCandidates returned the candidates already in cost order and + * truncated to maxCandidates_, so the caller must not rank them again. + **/ + bool candidatesRanked_; + /** * Array containing the reduced cost associated * with each vertex. diff --git a/src/CoinCliqueList.cpp b/src/CoinCliqueList.cpp index 33b2d539..38a64095 100644 --- a/src/CoinCliqueList.cpp +++ b/src/CoinCliqueList.cpp @@ -42,6 +42,20 @@ void CoinCliqueList::addClique(size_t size, const size_t els[]) { clqEls_.push_back(std::vector(els, els + size)); + /* Sorted is an invariant, not a convenience: CoinConflictGraph::conflictInCliques + * and CoinStaticConflictGraph::nodeInClique both std::binary_search this list, + * so an unsorted clique makes conflicting() answer *false* for a pair that + * really does share a clique. The callers do not hand us sorted elements -- + * CoinDynamicConflictGraph builds them from a row whose columns are ordered by + * coefficient value, not by index -- and on 6 of the 358 replay instances that + * left 17.8% of all cliques unsorted, costing neos-4532248-waihi 163016 real + * conflicts and one odd-wheel cut. CoinCliqueSet::insertIfNotDuplicate already + * sorts before calling us, which is where the invariant was already assumed. + * + * Sorting here rather than at each query keeps it O(k log k) once instead of + * O(k) per lookup, and cliques are searched far more often than they are built. */ + std::sort(clqEls_.back().begin(), clqEls_.back().end()); + nCliqueElements_ += size; } diff --git a/src/CoinConflictGraph.cpp b/src/CoinConflictGraph.cpp index 22336893..be8c7c77 100644 --- a/src/CoinConflictGraph.cpp +++ b/src/CoinConflictGraph.cpp @@ -268,7 +268,10 @@ void CoinConflictGraph::iniCoinConflictGraph(const CoinConflictGraph *other) { minDegree_ = other->minDegree_; maxDegree_ = other->maxDegree_; updateMDegree = other->updateMDegree; - infeasibleImplications_ = infeasibleImplications_; + // Was a self-assignment, which silently dropped the implications on every + // copy/clone. CbcSolver reports these (see its infeasibleImplications() + // use), so a cloned graph has to carry them. + infeasibleImplications_ = other->infeasibleImplications_; } void CoinConflictGraph::setMinCliqueRow(size_t minClqRow) { diff --git a/src/CoinCutPool.cpp b/src/CoinCutPool.cpp index 21f13985..c2b107b6 100644 --- a/src/CoinCutPool.cpp +++ b/src/CoinCutPool.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #define CUTPOOL_EPS 1e-8 @@ -44,10 +45,11 @@ struct CompareIdxs { const int *idxs_; }; -CoinCut::CoinCut(const int *idxs, const double *coefs, size_t nz, double rhs) +CoinCut::CoinCut(const int *idxs, const double *coefs, size_t nz, double rhs, size_t tag) : idxs_(std::vector(nz)) , coefs_(std::vector(nz)) , rhs_(rhs) + , tag_(tag) { for (size_t i = 0; i < nz; i++) { idxs_[i] = i; @@ -149,9 +151,13 @@ bool CoinCut::dominates(const CoinCut *other) const { return true; } -CoinCutPool::CoinCutPool(const double *x, int numCols) { +CoinCutPool::CoinCutPool(const double *x, int numCols, const char *tag) { x_ = x; nullCuts_ = 0; + numCandidates_ = 0; + tag_ = tag; + filterEnabled_ = true; + maxParallelism_ = 1.0; bestCutByCol_ = std::vector(numCols, -1); @@ -162,32 +168,95 @@ CoinCutPool::CoinCutPool(const double *x, int numCols) { cutFitness_ = std::vector(cutsCap_); } +// True if CBC_CLIQUE_POOL_DEBUG is set. MSVC's /sdl (on in the v17 projects) +// turns the deprecation warning for getenv() into an error, so use +// _dupenv_s() there. +static bool cutPoolDebugEnabled() { +#ifdef _MSC_VER + char *value = NULL; + size_t length = 0; + const bool set = _dupenv_s(&value, &length, "CBC_CLIQUE_POOL_DEBUG") == 0 && value != NULL; + free(value); + return set; +#else + return getenv("CBC_CLIQUE_POOL_DEBUG") != NULL; +#endif +} + CoinCutPool::~CoinCutPool() { + if (tag_ && cutPoolDebugEnabled()) { + const size_t kept = nCuts_ - nullCuts_; + fprintf(stderr, "[cutpool] %s: candidates=%zu kept=%zu (filtered=%zu, %.1f%%) filterEnabled=%d\n", + tag_, numCandidates_, kept, numCandidates_ - kept, + numCandidates_ ? 100.0 * (numCandidates_ - kept) / numCandidates_ : 0.0, + (int)filterEnabled_); + } for (size_t i = 0; i < nCuts_; i++) { delete cuts_[i]; } } -bool CoinCutPool::add(const int *idxs, const double *coefs, int nz, double rhs) { - CoinCut *cut = new CoinCut(idxs, coefs, nz, rhs); +size_t CoinCutPool::hashCut(const CoinCut *cut) const { + // FNV-1a style combine over the cut's (already sorted, per CoinCut's + // constructor) indices/coefficients/rhs. + size_t h = 1469598103934665603ULL; + auto mix = [&h](size_t v) { + h ^= v; + h *= 1099511628211ULL; + }; + const int size = cut->size(); + mix(static_cast(size)); + const int *idxs = cut->idxs(); + const double *coefs = cut->coefs(); + for (int i = 0; i < size; i++) { + mix(static_cast(idxs[i])); + size_t bits; + memcpy(&bits, &coefs[i], sizeof(bits)); + mix(bits); + } + double rhs = cut->rhs(); + size_t rbits; + memcpy(&rbits, &rhs, sizeof(rbits)); + mix(rbits); + return h; +} - checkMemory(); +bool CoinCutPool::add(const int *idxs, const double *coefs, int nz, double rhs, size_t tag) { + numCandidates_++; + CoinCut *cut = new CoinCut(idxs, coefs, nz, rhs, tag); - if (updateCutFrequency(cut) == 0) { + // Always reject exact-duplicate candidates (same vars/coefs/rhs), + // independent of filterEnabled_ -- generators that search from + // different starting points (e.g. CglImpliedClique's per-hub search, + // CglBKClique's clique extension) can rediscover the same clique + // more than once. + if (!seenHashes_.insert(hashCut(cut)).second) { delete cut; return false; } + checkMemory(); + + const double fitness = calculateFitness(cut); + cutFitness_[nCuts_] = fitness; + + if (filterEnabled_) { + if (updateCutFrequency(cut, fitness) == 0) { + delete cut; + return false; + } + } else { + cutFrequency_[nCuts_] = 1; + } + cuts_[nCuts_++] = cut; return true; } -size_t CoinCutPool::updateCutFrequency(const CoinCut *cut) { +size_t CoinCutPool::updateCutFrequency(const CoinCut *cut, double fitness) { const int nz = cut->size(); const int *idxs = cut->idxs(); - const double fitness = calculateFitness(cut); - cutFitness_[nCuts_] = fitness; cutFrequency_[nCuts_] = 0; for (int i = 0; i < nz; i++) { @@ -254,6 +323,100 @@ double CoinCutPool::calculateFitness(const CoinCut *cut) const { return ((violation / ((double) activeCols)) * 100000.0) + ((1.0 / (diffOfCoefs + 1.0)) * 1000.0); } +double CoinCutPool::parallelism(const CoinCut *a, const CoinCut *b) const { + const int sizeA = a->size(), sizeB = b->size(); + const int *idxsA = a->idxs(), *idxsB = b->idxs(); + const double *coefsA = a->coefs(), *coefsB = b->coefs(); + + double normA = 0.0, normB = 0.0, dot = 0.0; + for (int i = 0; i < sizeA; i++) { + normA += coefsA[i] * coefsA[i]; + } + for (int i = 0; i < sizeB; i++) { + normB += coefsB[i] * coefsB[i]; + } + + // idxs() is sorted ascending (CoinCut's constructor invariant), so a + // single merge pass finds the dot product over the shared support. + int i = 0, j = 0; + while (i < sizeA && j < sizeB) { + if (idxsA[i] == idxsB[j]) { + dot += coefsA[i] * coefsB[j]; + i++; + j++; + } else if (idxsA[i] < idxsB[j]) { + i++; + } else { + j++; + } + } + + const double denom = std::sqrt(normA) * std::sqrt(normB); + if (denom <= CUTPOOL_EPS) { + return 0.0; + } + + return dot / denom; +} + +void CoinCutPool::filterByParallelism() { + if (maxParallelism_ >= 1.0 - CUTPOOL_EPS) { + return; // disabled + } + + if (nullCuts_) { + removeNullCuts(); + } + + if (nCuts_ <= 1) { + return; + } + + // Greedily accept cuts by descending fitness (the same score already + // used by updateCutFrequency()'s per-column contest), skipping any + // candidate too parallel (cosine similarity > maxParallelism_) to an + // already-accepted cut. Mirrors HiGHS's HighsCutPool (maxpar=0.1) and + // SCIP's cutsel_hybrid/cutsel_dynamic (minortho=0.9, i.e. + // maxparallelism=0.1) orthogonality-based cut selection. + std::vector order(nCuts_); + for (size_t i = 0; i < nCuts_; i++) { + order[i] = i; + } + std::sort(order.begin(), order.end(), [this](size_t a, size_t b) { + return cutFitness_[a] > cutFitness_[b]; + }); + + std::vector keptIdx; + keptIdx.reserve(nCuts_); + std::vector keep(nCuts_, false); + + for (size_t oi : order) { + bool tooParallel = false; + for (size_t ki : keptIdx) { + if (parallelism(cuts_[oi], cuts_[ki]) > maxParallelism_) { + tooParallel = true; + break; + } + } + if (!tooParallel) { + keep[oi] = true; + keptIdx.push_back(oi); + } + } + + for (size_t i = 0; i < nCuts_; i++) { + if (!keep[i]) { + delete cuts_[i]; + cuts_[i] = NULL; + nullCuts_++; + } + } + + if (nullCuts_) { + removeNullCuts(); + } +} + void CoinCutPool::checkMemory() { if (nCuts_ + 1 <= cutsCap_) { return; @@ -365,6 +528,14 @@ double CoinCutPool::cutRHS(size_t i) const { return cuts_[i]->rhs(); } +size_t CoinCutPool::cutTag(size_t i) const { +#ifdef DEBUGCG + assert(i < nCuts_); + assert(cuts_[i]); +#endif + return cuts_[i]->tag(); +} + void CoinCutPool::removeNullCuts() { if (nCuts_ == 0) { return; diff --git a/src/CoinCutPool.hpp b/src/CoinCutPool.hpp index afd82d37..1d08f06a 100644 --- a/src/CoinCutPool.hpp +++ b/src/CoinCutPool.hpp @@ -31,6 +31,7 @@ #include "CoinUtilsConfig.h" #include #include +#include /** * Class for representing a cut. @@ -44,8 +45,12 @@ class COINUTILSLIB_EXPORT CoinCut { * @param coefs coefficients of the variables the cut * @param nz size of the cut * @param rhs right-hand side of the cut + * @param tag opaque caller-supplied identifier (e.g. the cut's original + * index in some external OsiCuts collection), carried through + * unchanged so callers can recover which of their original cuts + * survived the pool's filtering. Defaults to SIZE_MAX (unused). **/ - CoinCut(const int *idxs, const double *coefs, size_t nz, double rhs); + CoinCut(const int *idxs, const double *coefs, size_t nz, double rhs, size_t tag = static_cast(-1)); /** * Destructor @@ -72,6 +77,14 @@ class COINUTILSLIB_EXPORT CoinCut { **/ double rhs() const { return rhs_; } + /** + * Return the opaque caller-supplied tag, if any (see constructor). + * Survives pool compaction (removeNullCuts()/filterByParallelism()) + * unchanged, since it travels with the CoinCut object rather than a + * position in a parallel array. + **/ + size_t tag() const { return tag_; } + /** * Check if the cut dominates another one. * @@ -94,6 +107,39 @@ class COINUTILSLIB_EXPORT CoinCut { * right-hand side of the cut **/ double rhs_; + + /** + * opaque caller-supplied tag (see tag() accessor) + **/ + size_t tag_; +}; + +/** + * When a conflict-graph cut generator (CglBKClique, CglOddWheel, + * CglImpliedClique) turns on its CoinCutPool's filtering. The generator + * applies these itself, before its add() loop: + * - minCols: on a model with fewer columns, filtering and the + * parallelism filter are both skipped (unless alwaysFilter); + * - minCandidates: filtering is skipped when there are fewer candidate + * cuts than this (CglBKClique and CglOddWheel only, since they know + * their candidate count up front); + * - maxParallelism: passed to setMaxParallelism(); 1.0 disables it; + * - alwaysFilter: filter regardless of minCols and minCandidates. + * Cbc sets these from its cliqueFilter* parameters. + **/ +struct CoinCutPoolGate { + size_t minCols; + size_t minCandidates; + double maxParallelism; + bool alwaysFilter; + + CoinCutPoolGate() + : minCols(500) + , minCandidates(20) + , maxParallelism(1.0) + , alwaysFilter(false) + { + } }; /** @@ -108,8 +154,55 @@ class COINUTILSLIB_EXPORT CoinCutPool { * * @param x current solution of the LP relaxation of the MILP. * @param numCols number of variables of the MILP. + * @param tag optional identifier (e.g. generator name) printed with + * per-pool candidate/kept counts when the CBC_CLIQUE_POOL_DEBUG + * env var is set; nullptr (default) means no debug output. **/ - CoinCutPool(const double *x, int numCols); + CoinCutPool(const double *x, int numCols, const char *tag = nullptr); + + /** + * Enable/disable the expensive per-column best-score filtering (the + * fitness-based contest in updateCutFrequency()/calculateFitness()). + * Exact-duplicate cuts are always rejected regardless of this setting + * (via a cheap hash check in add()) -- this only controls whether + * *non-duplicate* candidates additionally compete for a "best cut per + * column" slot. Callers that already know their candidate count + * up front (e.g. a clique list already fully built) should measure it + * against a caller-chosen threshold and pass the result here *before* + * the add() loop starts; leaving filtering enabled on a large batch of + * candidates is what actually prunes redundant/dominated cuts, but + * doing so on a handful of candidates only adds cost for no benefit + * (see CBC_CLIQUE_POOL_DEBUG measurements: <=20 candidates are almost + * never filtered in practice; CoinCutPoolGate holds the thresholds the + * conflict-graph generators use). Defaults to enabled (the original, + * always-filter behaviour) for any caller that doesn't call this. + **/ + void setFilteringEnabled(bool enabled) { filterEnabled_ = enabled; } + + /** + * Set the maximum cosine-similarity ("parallelism") allowed between a + * kept cut and any other cut already kept, applied by + * filterByParallelism(). Two cuts pointing in nearly the same + * direction add little beyond what the first already provides (to the + * reoptimized LP) while still costing a row -- this is the same + * orthogonality/parallelism-based selection idea used by HiGHS + * (HighsCutPool: maxpar=0.1) and SCIP (cutsel_hybrid/cutsel_dynamic: + * minortho=0.9, i.e. maxparallelism=0.1), neither of which CBC + * previously implemented for its clique-cut pool. Default 1.0 means + * "disabled" (no two non-identical cuts have cosine similarity of + * exactly 1.0, since exact duplicates are already rejected in add()). + **/ + void setMaxParallelism(double maxParallelism) { maxParallelism_ = maxParallelism; } + + /** + * Greedily select cuts by descending fitness, skipping any candidate + * whose cosine similarity to an already-selected cut exceeds + * maxParallelism_ (see setMaxParallelism()). No-op if maxParallelism_ + * is 1.0 (the default/disabled value). Should be called after the + * add() loop (and, ideally, after removeNullCuts()) and before + * reading cuts out via cutIdxs()/cutCoefs()/etc. + **/ + void filterByParallelism(); /** * Destructor @@ -145,6 +238,12 @@ class COINUTILSLIB_EXPORT CoinCutPool { **/ double cutRHS(size_t i) const; + /** + * Return the opaque caller-supplied tag of the i-th cut in the pool + * (see CoinCut::tag()). SIZE_MAX if none was supplied to add(). + **/ + size_t cutTag(size_t i) const; + /** * Try to add a cut in the pool. Return true * if the cut was added. @@ -155,8 +254,11 @@ class COINUTILSLIB_EXPORT CoinCutPool { * the cut to be added. * @param nz size of the cut to be added * @param rhs right-hand side of the cut to be added + * @param tag optional opaque identifier for this cut, retrievable via + * cutTag() after filtering (e.g. its original index in some caller-side + * OsiCuts collection). Defaults to SIZE_MAX (unused). **/ - bool add(const int *idxs, const double *coefs, int nz, double rhs); + bool add(const int *idxs, const double *coefs, int nz, double rhs, size_t tag = static_cast(-1)); /** * Remove dominated cuts. @@ -177,19 +279,34 @@ class COINUTILSLIB_EXPORT CoinCutPool { * to reflect this calculation. Return the number * of variables for which the cut has the best score. **/ - size_t updateCutFrequency(const CoinCut *cut); + size_t updateCutFrequency(const CoinCut *cut, double fitness); /** * Compute the score of the cut. **/ double calculateFitness(const CoinCut *cut) const; + /** + * Compute the cosine similarity ("parallelism", in [-1, 1]) between + * the coefficient vectors of two cuts, treating missing entries as 0. + * Both cuts' idxs() are sorted ascending (CoinCut's constructor + * invariant), so this runs in a single O(sizeA + sizeB) merge pass. + **/ + double parallelism(const CoinCut *a, const CoinCut *b) const; + /** * Check if it is necessary expand the memory * allocated for storing the cuts. **/ void checkMemory(); + /** + * Compute a hash over a cut's (sorted) variable indices, coefficients, + * and rhs, used to reject exact-duplicate candidates cheaply (O(1) + * amortized) independent of whether score-based filtering is enabled. + **/ + size_t hashCut(const CoinCut *cut) const; + /** * Check the dominance relation between two cuts * in the pool. Return 0 if cut idxA dominates @@ -239,6 +356,36 @@ class COINUTILSLIB_EXPORT CoinCutPool { * Current solution of the LP relaxation of the MILP. **/ const double *x_; + + /** + * Total number of candidate cuts submitted via add(), including ones + * later filtered out by the best-score-per-column rule -- tracked only + * to report a candidates-vs-kept ratio under CBC_CLIQUE_POOL_DEBUG. + **/ + size_t numCandidates_; + + /** + * Optional tag identifying the calling generator, for debug output. + **/ + const char *tag_; + + /** + * Whether the per-column best-score filtering is active for this pool + * instance (see setFilteringEnabled()). Defaults to true. + **/ + bool filterEnabled_; + + /** + * Hashes of cuts already accepted into the pool, used to reject exact + * duplicates regardless of filterEnabled_. + **/ + std::unordered_set seenHashes_; + + /** + * Maximum cosine similarity allowed between two kept cuts (see + * setMaxParallelism()). Defaults to 1.0 (disabled). + **/ + double maxParallelism_; }; diff --git a/src/CoinDynamicConflictGraph.cpp b/src/CoinDynamicConflictGraph.cpp index d92c866b..02deb098 100644 --- a/src/CoinDynamicConflictGraph.cpp +++ b/src/CoinDynamicConflictGraph.cpp @@ -33,6 +33,7 @@ #include "CoinCliqueList.hpp" #include "CoinColumnType.hpp" #include "CoinKnapsackRow.hpp" +#include "CoinTime.hpp" #ifdef CGRAPH_STATS #include @@ -114,11 +115,14 @@ CoinDynamicConflictGraph::CoinDynamicConflictGraph( const double primalTolerance, const double infinity, const std::vector< std::string > &colNames, - const std::vector< std::string > &rowNames) + const std::vector< std::string > &rowNames, + const double timeLimit) : conflicts(new CoinAdjacencyVector(numCols * 2, CG_INI_SPACE_ADJACENCY_VECTOR)) , largeClqs(new CoinCliqueList(CG_LARGE_CLIQUE_INIT, CG_LARGE_CLIQUE_GROW)) , degree_(std::vector< size_t >(numCols * 2)) , modifiedDegree_(std::vector< size_t >(numCols * 2)) + , timeLimit_(timeLimit) + , timeLimitReached_(false) { iniCoinConflictGraph(numCols * 2); @@ -175,6 +179,10 @@ CoinDynamicConflictGraph::CoinDynamicConflictGraph( memset(rowTypeStats_, 0, sizeof(rowTypeStats_)); #endif for (size_t idxRow = 0; idxRow < nRows; idxRow++) { + if (timeLimit_ > 0.0 && (idxRow & 63) == 0 && CoinWallclockTime() > timeLimit_) { + timeLimitReached_ = true; + break; + } const char rowSense = sense[idxRow]; const CoinBigIndex rowStart = start[idxRow]; const size_t rowLength = static_cast< size_t >(length[idxRow]); @@ -327,10 +335,16 @@ CoinDynamicConflictGraph::CoinDynamicConflictGraph( addVariableComplementConflicts(numCols, colType); // detecting cliques in less-structured constraints - for (size_t idxTR = 0; (idxTR < tRowElements.size()); ++idxTR) { - double rhs = tRowRHS[idxTR]; - if (isGoodNumber(rhs)) - cliqueDetection(tRowElements[idxTR], tRowElements[idxTR].size(), rhs + primalTolerance); + if (!timeLimitReached_) { + for (size_t idxTR = 0; (idxTR < tRowElements.size()); ++idxTR) { + if (timeLimit_ > 0.0 && (idxTR & 63) == 0 && CoinWallclockTime() > timeLimit_) { + timeLimitReached_ = true; + break; + } + double rhs = tRowRHS[idxTR]; + if (isGoodNumber(rhs)) + cliqueDetection(tRowElements[idxTR], tRowElements[idxTR].size(), rhs + primalTolerance); + } } // at this point large cliques will already be include @@ -343,6 +357,10 @@ CoinDynamicConflictGraph::CoinDynamicConflictGraph( std::vector< char > iv(size_); for (size_t k = 0; (k < smallCliques->nDifferentNodes()); ++k) { + if (timeLimit_ > 0.0 && (k & 63) == 0 && CoinWallclockTime() > timeLimit_) { + timeLimitReached_ = true; + break; + } size_t idxNode = smallCliques->differentNodes()[k]; const size_t nNodeCliques = smallCliques->nNodeOccurrences(idxNode); const size_t *nodeCliques = smallCliques->nodeOccurrences(idxNode); diff --git a/src/CoinDynamicConflictGraph.hpp b/src/CoinDynamicConflictGraph.hpp index d045fc76..dc834c6b 100644 --- a/src/CoinDynamicConflictGraph.hpp +++ b/src/CoinDynamicConflictGraph.hpp @@ -81,6 +81,18 @@ class COINUTILSLIB_EXPORT CoinDynamicConflictGraph : public CoinConflictGraph * when discounting continuous variables * @param colNames column names, used exclusively for diagnostic messages * while reporting newly inferred bounds + * @param timeLimit absolute wall-clock deadline (CoinWallclockTime() basis) + * for the graph-building scan, or -1.0 (default) to + * disable it. On instances with many large/dense rows the + * per-row clique detection and the final small-clique + * expansion pass can take a long time with no other + * natural place to stop; this deadline is checked + * periodically (every 64 rows/nodes) so a caller with a + * time budget (e.g. CBC's overall -seconds limit) can + * abort cleanly instead of running unbounded. On timeout, + * the graph is finalized early with whatever conflicts/ + * cliques were already found (a partial but still sound + * conflict graph) -- see `timeLimitReached()`. **/ CoinDynamicConflictGraph( const int numCols, @@ -94,7 +106,8 @@ class COINUTILSLIB_EXPORT CoinDynamicConflictGraph : public CoinConflictGraph const double primalTolerance, const double infinity, const std::vector &colNames, - const std::vector< std::string > &rowNames + const std::vector< std::string > &rowNames, + const double timeLimit = -1.0 ); /** @@ -102,6 +115,14 @@ class COINUTILSLIB_EXPORT CoinDynamicConflictGraph : public CoinConflictGraph **/ virtual ~CoinDynamicConflictGraph(); + /** + * Returns true if the graph-building scan was aborted early because + * `timeLimit` (passed to the constructor) was reached. When this happens + * the graph is still sound (every conflict/clique that was found is + * correct), just possibly incomplete. + **/ + bool timeLimitReached() const { return timeLimitReached_; } + /** * Add conflicts to a node to be stored directly (not as cliques). * @@ -404,6 +425,17 @@ class COINUTILSLIB_EXPORT CoinDynamicConflictGraph : public CoinConflictGraph * implication (name and index). */ std::map>> columnRowImplications_; + + /** + * Absolute wall-clock deadline (CoinWallclockTime() basis) for the + * graph-building scan, or -1.0 if disabled. See `timeLimitReached()`. + **/ + double timeLimit_ = -1.0; + + /** + * Set to true once `timeLimit_` is reached during construction. + **/ + bool timeLimitReached_ = false; }; #endif // DYNAMICCONFLICTGRAPH_H diff --git a/src/CoinFactorization.hpp b/src/CoinFactorization.hpp index 2a11d1a9..66fc484a 100644 --- a/src/CoinFactorization.hpp +++ b/src/CoinFactorization.hpp @@ -143,6 +143,21 @@ class COINUTILSLIB_EXPORT CoinFactorization { { status_ = value; } + /** Returns absolute wall-clock deadline (CoinWallclockTime() basis) for + the current factorization, or -1.0 if no deadline is set. Checked + periodically inside the Markowitz pivot loop (factorSparseSmall()/ + factorSparseLarge()) so a very slow factorization can be aborted + cleanly (status -100) instead of stalling past the caller's time + limit with no way to detect it. */ + inline double timeLimit() const + { + return timeLimit_; + } + /// Sets wall-clock deadline (-1.0 to disable) + inline void setTimeLimit(double value) + { + timeLimit_ = value; + } /// Returns number of pivots since factorization inline int pivots() const { @@ -1470,6 +1485,8 @@ class COINUTILSLIB_EXPORT CoinFactorization { CoinIntArrayWithLength pivotColumnBack_; /// Status of factorization int status_; + /// Absolute wall-clock deadline for factorization loop, -1.0 if disabled + double timeLimit_; /** 0 - no increasing rows - no permutations, 1 - no increasing rows but permutations diff --git a/src/CoinFactorization1.cpp b/src/CoinFactorization1.cpp index 52d65109..7fd5a653 100644 --- a/src/CoinFactorization1.cpp +++ b/src/CoinFactorization1.cpp @@ -210,6 +210,7 @@ void CoinFactorization::gutsOfInitialize(int type) areaFactor_ = 0.0; pivotTolerance_ = 1.0e-1; zeroTolerance_ = 1.0e-13; + timeLimit_ = -1.0; #ifndef COIN_FAST_CODE slackValue_ = -1.0; #endif diff --git a/src/CoinFactorization2.cpp b/src/CoinFactorization2.cpp index da6985bd..d40caa2f 100644 --- a/src/CoinFactorization2.cpp +++ b/src/CoinFactorization2.cpp @@ -13,6 +13,7 @@ #include "CoinIndexedVector.hpp" #include "CoinHelperFunctions.hpp" #include "CoinFinite.hpp" +#include "CoinTime.hpp" #if COIN_FACTORIZATION_DENSE_CODE == 1 // using simple lapack interface @@ -409,6 +410,14 @@ int CoinFactorization::factorSparseSmall() // This should not need to be trapped here - but be safe if (numberGoodU_ == numberRows_) count = biggerDimension_ + 1; + // Periodic wall-clock deadline check (every 64 pivots to keep overhead + // negligible). A slow/degenerate factorization can otherwise run for + // many minutes with no way for the caller's time limit to be noticed. + if (timeLimit_ > 0.0 && (numberGoodU_ & 63) == 0 && CoinWallclockTime() > timeLimit_) { + status = -100; + count = biggerDimension_ + 1; + break; + } #if COIN_DEBUG == 2 checkConsistency(); #endif @@ -512,6 +521,14 @@ int CoinFactorization::factorDense() if (sizeof(int) == 4 && numberDense_ >= 2 << 15) { abort(); } + // Wall-clock deadline check before doing any dense-phase work. The dense + // fallback (LAPACK dgetrf below) is a single atomic call with no way to + // interrupt it mid-flight once started, so unlike the sparse Markowitz + // loop (which can check periodically between pivots) the only place we + // can bail out here is before committing to it at all. + if (timeLimit_ > 0.0 && CoinWallclockTime() > timeLimit_) { + return -100; + } int full; if (denseThreshold_ > 0 || true) full = numberDense_ * numberDense_; @@ -601,6 +618,14 @@ int CoinFactorization::factorDense() if (denseThreshold_ /*>0*/) { assert(numberGoodU_ == numberRows_); numberGoodL_ = numberRows_; + // Re-check the deadline here: building the dense submatrix above is + // O(numberDense_^2) and can itself take a while for a large dense + // block, so the entry guard at the top of this function may already + // be stale by the time we are about to commit to the O(numberDense_^3) + // dgetrf call below, which cannot be interrupted once started. + if (timeLimit_ > 0.0 && CoinWallclockTime() > timeLimit_) { + return -100; + } //now factorize //dgef(denseAreaAddress_,&numberDense_,&numberDense_,densePermute_); #if COIN_FACTORIZATION_DENSE_CODE == 1 @@ -1396,6 +1421,13 @@ int CoinFactorization::factorSparseLarge() // This should not need to be trapped here - but be safe if (numberGoodU_ == numberRows_) count = biggerDimension_ + 1; + // Periodic wall-clock deadline check (every 64 pivots to keep + // overhead negligible) - see identical check in factorSparseSmall(). + if (timeLimit_ > 0.0 && (numberGoodU_ & 63) == 0 && CoinWallclockTime() > timeLimit_) { + status = -100; + count = biggerDimension_ + 1; + break; + } } #if COIN_DEBUG == 2 checkConsistency(); diff --git a/src/CoinMemUsage.hpp b/src/CoinMemUsage.hpp new file mode 100644 index 00000000..090e7d19 --- /dev/null +++ b/src/CoinMemUsage.hpp @@ -0,0 +1,122 @@ +// Copyright (C) 2026, International Business Machines +// Corporation and others. All Rights Reserved. +// This code is licensed under the terms of the Eclipse Public License (EPL). + +#ifndef _CoinMemUsage_hpp +#define _CoinMemUsage_hpp + +#include "CoinUtilsConfig.h" + +//############################################################################# +// +// Portable helpers to query the current process's resident memory usage and +// the total physical memory installed on the machine, in bytes. These are +// used (among other things) to implement a "memory limit" stopping criterion +// during branch and bound, in the same spirit as CoinCpuTime()/ +// CoinWallclockTime() in CoinTime.hpp are used for time-based limits. +// +// Both functions return 0 if the requested quantity could not be +// determined; callers should treat 0 as "unknown" rather than "no memory +// used"/"no memory installed". +// +//############################################################################# + +#include + +#if defined(_WIN32) + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include + +/// Returns the current resident set size (physical memory currently used by +/// this process, a.k.a. the "working set" on Windows), in bytes. +inline size_t CoinGetCurrentMemUsage() +{ + PROCESS_MEMORY_COUNTERS pmc; + // K32GetProcessMemoryInfo has been part of kernel32.dll since Windows + // Vista, so no extra link-time dependency on psapi.lib is needed. + if (K32GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(pmc))) + return static_cast< size_t >(pmc.WorkingSetSize); + return 0; +} + +/// Returns the total physical memory installed on this machine, in bytes. +inline size_t CoinGetTotalPhysicalMemory() +{ + MEMORYSTATUSEX status; + status.dwLength = sizeof(status); + if (GlobalMemoryStatusEx(&status)) + return static_cast< size_t >(status.ullTotalPhys); + return 0; +} + +#elif defined(__APPLE__) + +#include +#include +#include + +inline size_t CoinGetCurrentMemUsage() +{ + mach_task_basic_info_data_t info; + mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT; + if (task_info(mach_task_self(), MACH_TASK_BASIC_INFO, + (task_info_t)&info, &count) == KERN_SUCCESS) { + return static_cast< size_t >(info.resident_size); + } + return 0; +} + +inline size_t CoinGetTotalPhysicalMemory() +{ + int64_t mem = 0; + size_t len = sizeof(mem); + int mib[2] = { CTL_HW, HW_MEMSIZE }; + if (sysctl(mib, 2, &mem, &len, NULL, 0) == 0 && mem > 0) + return static_cast< size_t >(mem); + return 0; +} + +#else +// Linux and other systems providing /proc/self/statm and sysconf(). + +#include +#include + +inline size_t CoinGetCurrentMemUsage() +{ + FILE *fp = fopen("/proc/self/statm", "r"); + if (!fp) + return 0; + long residentPages = 0; + // Fields are: size resident shared text lib data dt (all in pages). + int matched = fscanf(fp, "%*ld%ld", &residentPages); + fclose(fp); + if (matched != 1 || residentPages < 0) + return 0; + long pageSize = sysconf(_SC_PAGESIZE); + if (pageSize <= 0) + return 0; + return static_cast< size_t >(residentPages) * static_cast< size_t >(pageSize); +} + +inline size_t CoinGetTotalPhysicalMemory() +{ +#if defined(_SC_PHYS_PAGES) && defined(_SC_PAGESIZE) + long pages = sysconf(_SC_PHYS_PAGES); + long pageSize = sysconf(_SC_PAGESIZE); + if (pages > 0 && pageSize > 0) + return static_cast< size_t >(pages) * static_cast< size_t >(pageSize); +#endif + return 0; +} + +#endif + +#endif + +/* vi: softtabstop=2 shiftwidth=2 expandtab tabstop=2 +*/ diff --git a/src/CoinNodeHeap.cpp b/src/CoinNodeHeap.cpp index bae95389..e52d2b54 100644 --- a/src/CoinNodeHeap.cpp +++ b/src/CoinNodeHeap.cpp @@ -38,16 +38,68 @@ CoinNodeHeap::CoinNodeHeap(size_t numNodes) { numNodes_ = numNodes; pq_ = std::vector >(numNodes); pos_ = std::vector(numNodes); + // The first reset() has nothing to replay, and that is not a density + // signal, so it must not start the backoff either. + recording_ = false; + skipped_ = true; + skipsLeft_ = 0; reset(); } CoinNodeHeap::~CoinNodeHeap() {} +/** + * Restore the initial state: pq_[i] == (i, INFTY) and pos_[i] == i. + * + * Rewriting all numNodes_ entries dominated the one caller that matters. + * CoinOddWheelSeparator runs one shortest path per active node over a graph + * with 2 * activeColumns nodes, so a full reset per call is quadratic in the + * active count *regardless of how sparse the graph is*: on the bab6 fixture + * that is 18407 calls over 36814 nodes to find no odd hole at all. + * + * Replaying only the positions that were written reproduces the initial state + * exactly. Untouched positions still hold their initial (i, INFTY) since + * nothing wrote them. For pos_, note that a node only ever leaves a position + * by a write to that position -- update() and removeFirst() move nodes with + * std::swap or by overwriting pq_[0] and pq_[numNodes_-1], all of which are + * recorded -- so pos_[x] != x implies position x was touched, and setting + * pos_[p] = p over the touched set restores every entry that moved. + * + * Because the array contents end up identical rather than merely equivalent, + * every later comparison, and so every tie-break in the heap order, is + * unchanged. That matters here: the arc weights are 1001 - 1000*x and repeat + * often, and a different tie-break would silently return a different odd cycle. + */ void CoinNodeHeap::reset() { - for (size_t i = 0; i < numNodes_; i++) { - pq_[i].first = i; - pq_[i].second = NODEHEAP_INFTY; - pos_[i] = i; + if (recording_) { + for (size_t i = 0; i < touched_.size(); i++) { + const size_t p = touched_[i]; + pq_[p].first = p; + pq_[p].second = NODEHEAP_INFTY; + pos_[p] = p; + } + } else { + for (size_t i = 0; i < numNodes_; i++) { + pq_[i].first = i; + pq_[i].second = NODEHEAP_INFTY; + pos_[i] = i; + } + // Recording was on and the use outgrew the list: enough of the heap + // moves that the replay will not be used, so stop paying for it. + if (!skipped_) { + skipsLeft_ = recordBackoff(); + } + } + + touched_.clear(); + + if (skipsLeft_ > 0) { + skipsLeft_--; + recording_ = false; + skipped_ = true; + } else { + recording_ = true; + skipped_ = false; } } @@ -57,10 +109,13 @@ void CoinNodeHeap::update(size_t node, double cost) { assert(cost + NODEHEAP_EPS <= pq_[pos].second); pq_[pos].second = cost; + touch(pos); while ((root = rootPos(child)) != std::numeric_limits::max()) { if (pq_[root].second >= pq_[child].second + NODEHEAP_EPS) { std::swap(pq_[child], pq_[root]); + touch(child); + touch(root); pos_[pq_[root].first] = root; pos_[pq_[child].first] = child; child = root; @@ -78,6 +133,8 @@ double CoinNodeHeap::removeFirst(size_t *node) { pq_[0] = pq_[posLastNode]; pq_[posLastNode].first = (*node); pq_[posLastNode].second = NODEHEAP_INFTY; + touch(0); + touch(posLastNode); pos_[pq_[0].first] = 0; pos_[(*node)] = posLastNode; @@ -91,6 +148,8 @@ double CoinNodeHeap::removeFirst(size_t *node) { if (pq_[root].second >= pq_[child].second + NODEHEAP_EPS) { std::swap(pq_[root], pq_[child]); + touch(root); + touch(child); pos_[pq_[root].first] = root; pos_[pq_[child].first] = child; root = child; diff --git a/src/CoinNodeHeap.hpp b/src/CoinNodeHeap.hpp index 11fdc93c..fa8238dd 100644 --- a/src/CoinNodeHeap.hpp +++ b/src/CoinNodeHeap.hpp @@ -80,6 +80,65 @@ class COINUTILSLIB_EXPORT CoinNodeHeap { * Number of nodes of the heap. **/ size_t numNodes_; + + /** + * Positions of pq_ written since the last reset(), so that reset() can + * restore just those instead of rewriting all numNodes_ entries. See the + * comment on reset() in the .cpp for why restoring exactly the written + * positions reproduces the initial state. + **/ + std::vector touched_; + + /** + * Whether the heap use now in progress is recording into touched_. Cleared by + * touch() when the list grows past fullResetThreshold(), and by reset() for a + * deliberately skipped use (see skipsLeft_). + **/ + bool recording_; + + /** + * Whether recording was *deliberately* off for the use that just finished, + * which is what separates "this graph is dense" from "we chose not to look". + * Only the former should extend the backoff. + **/ + bool skipped_; + + /** + * Uses remaining with recording deliberately off. + **/ + size_t skipsLeft_; + + /** + * Size of touched_ at which replaying it stops being cheaper than rewriting + * all numNodes_ entries, so both touch() and reset() give up on it. + * + * reset() writes three words per position sequentially; a replay writes the + * same three by random access, at several times the cost each, so the + * crossover is well below numNodes_. + **/ + inline size_t fullResetThreshold() const { return numNodes_ / 4; } + + /** + * How many resets to stop recording for once one has proved the heap use too + * broad for the replay to be used. See CoinShortestPath::recordBackoff() for + * the reasoning; correctness never depends on the value, since rewriting all + * numNodes_ entries is always a valid reset. + **/ + inline size_t recordBackoff() const { return 32; } + + /** + * Record that position pos of pq_ was written. + **/ + inline void touch(size_t pos) { + if (!recording_) { + return; + } + if (touched_.size() >= fullResetThreshold()) { + recording_ = false; + return; + } + touched_.push_back(pos); + } }; diff --git a/src/CoinOddWheelSeparator.cpp b/src/CoinOddWheelSeparator.cpp index 041257ee..e4c041f7 100644 --- a/src/CoinOddWheelSeparator.cpp +++ b/src/CoinOddWheelSeparator.cpp @@ -34,6 +34,29 @@ #define ODDWHEEL_SEP_DEF_MAX_RC 100.0 #define ODDWHEEL_SEP_DEF_MIN_VIOL 0.02 #define ODDWHEEL_SEP_DEF_MAX_WHEEL_CENTERS ((size_t)256) +#define ODDWHEEL_SEP_NOT_ACTIVE (std::numeric_limits::max()) +/* Lightest out-neighbours examined when looking for a dominating triangle. + * Being incomplete costs a skip that could have been taken, never a cut, so + * the cap trades gate strength for gate cost and nothing else. */ +#define ODDWHEEL_SEP_GATE_TRI_CAND ((size_t)16) + +/** + * Append one arc, growing the arrays if needed. + * + * Factored out only so the two ways of finding the conflicts cannot drift in + * how they emit them. + **/ +static inline void pushArc(std::vector &arcTo, std::vector &arcDist, + size_t &arcCap, size_t &idxArc, size_t to, double dist) { + if (idxArc + 1 > arcCap) { + arcCap *= 2; + arcTo.resize(arcCap); + arcDist.resize(arcCap); + } + arcTo[idxArc] = to; + arcDist[idxArc] = dist; + idxArc++; +} struct CompareCost { explicit CompareCost(const double *costs) { this->costs_ = costs; } @@ -58,9 +81,16 @@ CoinOddWheelSeparator::CoinOddWheelSeparator(const CoinConflictGraph *cgraph, co icaCount_ = 0; icaIdx_ = std::vector(cgSize); icaActivity_ = std::vector(cgSize); + stats_ = Stats(); // value-initialized: every counter and timer starts at zero + const double startActive = CoinGetTimeOfDay(); fillActiveColumns(); + stats_.tActiveColumns = CoinGetTimeOfDay() - startActive; + stats_.activeColumns = icaCount_; extMethod_ = extMethod; maxSeconds_ = 0.0; + verifyPrepare_ = false; + useGate_ = true; + recordOutcomes_ = false; spf_ = NULL; if (icaCount_ > 4) { @@ -104,24 +134,48 @@ void CoinOddWheelSeparator::searchOddWheels() { const double startTime = (maxSeconds_ > 0.0) ? CoinGetTimeOfDay() : 0.0; - if (!prepareGraph(startTime)) + if (!prepareGraph(startTime)) { + stats_.timeLimitReached = true; return; + } + + if (useGate_) { + buildFutilityGate(); + } + + if (recordOutcomes_) { + nodeOutcome_.assign(icaCount_, OUTCOME_NOT_CALLED); + } + const double startSearch = CoinGetTimeOfDay(); // Check time every 128 nodes so we don't check too frequently on small // instances or too rarely on large ones. for (size_t i = 0; i < icaCount_; i++) { if (maxSeconds_ > 0.0 && (i & 127) == 0 && i > 0) { - if (CoinGetTimeOfDay() - startTime >= maxSeconds_) + if (CoinGetTimeOfDay() - startTime >= maxSeconds_) { + stats_.timeLimitReached = true; break; + } + } + if (useGate_ && gateSkip_[i]) { + continue; } findOddHolesWithNode(i); } + stats_.tSearch = CoinGetTimeOfDay() - startSearch; + stats_.oddHolesFound = ohIdxs_.size(); if (extMethod_ > 0) { + const double startWC = CoinGetTimeOfDay(); //try to insert a wheel center for (size_t i = 0; i < ohIdxs_.size(); i++) { searchWheelCenter(i); + if (!wcIdxs_[i].empty()) { + stats_.wheelCenters++; + stats_.wheelCenterElements += wcIdxs_[i].size(); + } } + stats_.tWheelCenter = CoinGetTimeOfDay() - startWC; } } @@ -157,36 +211,247 @@ void CoinOddWheelSeparator::fillActiveColumns() { #endif } -bool CoinOddWheelSeparator::prepareGraph(double startTime) { - size_t idxArc = 0; - const size_t nodes = icaCount_ * 2; +/** + * Number of node visits the neighbour walk in buildForwardArcs() would make. + * + * That is exactly what conflictingNodes() reads for each active node: its direct + * conflicts, then every element of every clique it belongs to. The pairwise loop + * makes icaCount_ * icaCount_ conflicting() calls instead, so the two are + * directly comparable and the cheaper one can be picked per graph. + * + * Computing it is itself cheap -- O(icaCount_ + total node-clique memberships), + * with no element ever touched -- so it is worth paying to avoid the case where + * the active subgraph is dense and the walk would lose. That case is real: the + * analogous sparse rewrite in Gomory was bit-exact and 100x *slower* on wide + * rows until it was cost-gated. + **/ +size_t CoinOddWheelSeparator::neighborWalkCost() const { + size_t cost = 0; + + for (size_t i = 0; i < icaCount_; i++) { + const size_t idx = icaIdx_[i]; + cost += cgraph_->nDirectConflicts(idx); + + const size_t nCliques = cgraph_->nNodeCliques(idx); + if (nCliques) { + const size_t *cliques = cgraph_->nodeCliques(idx); + for (size_t c = 0; c < nCliques; c++) { + cost += cgraph_->cliqueSize(cliques[c]); + } + } + } + + return cost; +} + +/** + * Build the (x', y'') arcs, either by testing all icaCount_^2 pairs or by + * walking each active node's neighbourhood. + * + * The two produce the same arcs in the same order. The pairwise loop emits i2 + * ascending, and the walk gets there for free: conflictingNodes() returns the + * neighbour set sorted, fillActiveColumns() builds icaIdx_ in strictly + * increasing node order, so posInActive is monotone and sorted neighbours map to + * ascending positions. No sort is needed here, and since the arc *order* decides + * which of several equal-length cycles the shortest path returns, that is what + * keeps the cut set identical rather than merely equivalent. + * + * Two asymmetries between the paths are handled explicitly: + * - conflicting(n, n) returns false, so the pairwise loop never emits a self + * arc. The clique-merge branch of conflictingNodes() excludes the node too, + * but its fast path returns directConflicts() unfiltered, so the i2 == i1 + * skip below is load-bearing, not defensive. + * - conflicting() searches only the shorter of the two adjacency lists, so it + * is only well defined on a symmetric graph -- which addNeighbor() does not + * itself guarantee, it is the callers' responsibility. The walk reads one + * direction only, so it would diverge on an asymmetric graph. That is what + * setVerifyPrepare() exists to rule out empirically. + * - a repeated neighbour. conflictingNodes() copies directConflicts() into its + * scratch buffer without consulting iv, and its fast path returns that array + * untouched, so a pair recorded twice in the graph is reported twice; the + * pairwise loop tests each i2 once and cannot repeat one. Since the list is + * sorted the repeats are adjacent, so skipping them costs one comparison. + * + * All of that rests on the neighbour list being sorted, which is also what + * conflicting()'s own binary_search assumes -- but nothing enforces it, so it is + * checked here rather than trusted: a mapped position that goes backwards sets + * *sawUnsorted, and prepareGraph() then discards the walk and rebuilds pairwise. + * + * Writes through the parameters rather than the members so the verification in + * prepareGraph() can run the unselected path into scratch vectors, leaving the + * selected path's output and timing untouched. + * + * @return false if the wall-clock limit was hit, in which case the arrays are + * incomplete and the caller must abandon them. + **/ +bool CoinOddWheelSeparator::buildForwardArcs(bool useWalk, double startTime, + std::vector &arcStart, + std::vector &arcTo, + std::vector &arcDist, + size_t &arcCap, size_t &idxArc, + bool *sawUnsorted) { + std::vector posInActive; + if (useWalk) { + posInActive.assign(cgraph_->size(), ODDWHEEL_SEP_NOT_ACTIVE); + for (size_t i = 0; i < icaCount_; i++) { + posInActive[icaIdx_[i]] = i; + } + } - //Conflicts: (x', y'') for (size_t i1 = 0; i1 < icaCount_; i1++) { - // Check time every 64 outer iterations (each does icaCount_ inner steps). + // Check time every 64 outer iterations (each does icaCount_ inner steps + // pairwise, or one neighbourhood walk). if (maxSeconds_ > 0.0 && (i1 & 63) == 0 && i1 > 0) { - if (CoinGetTimeOfDay() - startTime >= maxSeconds_) + if (CoinGetTimeOfDay() - startTime >= maxSeconds_) { return false; + } } - spArcStart_[i1] = idxArc; + arcStart[i1] = idxArc; const size_t idx1 = icaIdx_[i1]; - for (size_t i2 = 0; i2 < icaCount_; i2++) { - const size_t idx2 = icaIdx_[i2]; + if (useWalk) { + const std::pair conf = + cgraph_->conflictingNodes(idx1, tmp_.data(), iv2_.data()); + + size_t prevPos = ODDWHEEL_SEP_NOT_ACTIVE; + for (size_t k = 0; k < conf.first; k++) { + const size_t i2 = posInActive[conf.second[k]]; - if (cgraph_->conflicting(idx1, idx2)) { - if(idxArc + 1 > spArcCap_) { - spArcCap_ *= 2; - spArcTo_.resize(spArcCap_); - spArcDist_.resize(spArcCap_); + if (i2 == ODDWHEEL_SEP_NOT_ACTIVE || i2 == i1) { + continue; } - spArcTo_[idxArc] = icaCount_ + i2; - spArcDist_[idxArc] = icaActivity_[i2]; - idxArc++; - } // conflict found - } // i2 + + if (prevPos != ODDWHEEL_SEP_NOT_ACTIVE) { + if (i2 == prevPos) { + continue; // the same neighbour reported twice + } + if (i2 < prevPos && sawUnsorted) { + *sawUnsorted = true; + } + } + prevPos = i2; + + pushArc(arcTo, arcDist, arcCap, idxArc, icaCount_ + i2, icaActivity_[i2]); + } // neighbors of idx1 + } else { + for (size_t i2 = 0; i2 < icaCount_; i2++) { + const size_t idx2 = icaIdx_[i2]; + + if (cgraph_->conflicting(idx1, idx2)) { + pushArc(arcTo, arcDist, arcCap, idxArc, icaCount_ + i2, icaActivity_[i2]); + } // conflict found + } // i2 + } } // i1 + return true; +} + +bool CoinOddWheelSeparator::prepareGraph(double startTime) { + size_t idxArc = 0; + const size_t nodes = icaCount_ * 2; + const double startArcs = CoinGetTimeOfDay(); + + const size_t walkCost = neighborWalkCost(); + // Compare against icaCount_ * icaCount_ without forming the product, which + // would overflow a 32-bit size_t at icaCount_ > 65535 (the largest graph in + // the fixture set has 380804 nodes). Integer division gets the boundary + // exactly right: walkCost == icaCount_^2 keeps the pairwise loop. + bool useWalk = (walkCost / icaCount_) < icaCount_; + stats_.prepareWalkCost = walkCost; + + //Conflicts: (x', y'') + bool sawUnsorted = false; + if (!buildForwardArcs(useWalk, startTime, spArcStart_, spArcTo_, spArcDist_, spArcCap_, + idxArc, &sawUnsorted)) { + stats_.prepareMethod = useWalk ? 2 : 1; + stats_.tPrepareArcs = CoinGetTimeOfDay() - startArcs; + return false; + } + + if (sawUnsorted) { + // A neighbour list came back out of order, so the walk cannot reproduce + // the ascending arc order the pairwise loop emits -- and the arc order + // decides which of several equal-length cycles is returned. Throw the + // walk away and pay for the pairwise loop, which is the only thing here + // that does not depend on the graph being sorted. + idxArc = 0; + useWalk = false; + stats_.prepareUnsorted = 1; + if (!buildForwardArcs(false, startTime, spArcStart_, spArcTo_, spArcDist_, spArcCap_, + idxArc, NULL)) { + stats_.prepareMethod = 1; + stats_.tPrepareArcs = CoinGetTimeOfDay() - startArcs; + return false; + } + } + + stats_.prepareMethod = useWalk ? 2 : 1; + stats_.tPrepareArcs = CoinGetTimeOfDay() - startArcs; + + if (verifyPrepare_) { + // Run whichever path was not selected into scratch vectors and compare + // element by element. Nothing here writes a member, so the selected + // path's arcs are unchanged and only its timing already stands recorded. + size_t vArcCap = icaCount_ * 2; + std::vector vArcStart((icaCount_ * 2) + 1, 0); + std::vector vArcTo(vArcCap); + std::vector vArcDist(vArcCap); + size_t vIdxArc = 0; + + // A time-limited run can leave the second path truncated, which is not a + // disagreement about the graph, so only a completed pair is compared. + if (buildForwardArcs(!useWalk, startTime, vArcStart, vArcTo, vArcDist, vArcCap, vIdxArc, + NULL)) { + /* Compare the two as *sets* per node, not position by position. A + * single extra arc early on shifts every later position, which once + * reported 164353 mismatches for a real disagreement of 395 arcs and + * said nothing about its nature. Both sides are ascending and + * duplicate-free per node, so one merge pass gives the symmetric + * difference, split by which method has the surplus -- and that split + * is the diagnosis: arcs only the walk has mean it saw a conflict + * conflicting() denies, arcs only the pairwise loop has mean the walk + * missed one, and the two have opposite verdicts about which method to + * trust. */ + size_t onlySel = 0, onlyVer = 0; + + for (size_t i1 = 0; i1 < icaCount_; i1++) { + size_t a = spArcStart_[i1]; + const size_t aEnd = (i1 + 1 < icaCount_) ? spArcStart_[i1 + 1] : idxArc; + size_t b = vArcStart[i1]; + const size_t bEnd = (i1 + 1 < icaCount_) ? vArcStart[i1 + 1] : vIdxArc; + + while (a < aEnd && b < bEnd) { + if (spArcTo_[a] == vArcTo[b]) { + // Exact comparison on purpose: both sides copy the same + // icaActivity_ entry, so any difference at all is real. + if (spArcDist_[a] != vArcDist[b]) { + onlySel++; + onlyVer++; + } + a++; + b++; + } else if (spArcTo_[a] < vArcTo[b]) { + onlySel++; + a++; + } else { + onlyVer++; + b++; + } + } + onlySel += aEnd - a; + onlyVer += bEnd - b; + } + + stats_.prepareVerifyArcs = vIdxArc; + stats_.prepareMismatches = onlySel + onlyVer; + stats_.prepareWalkOnly = useWalk ? onlySel : onlyVer; + stats_.preparePairOnly = useWalk ? onlyVer : onlySel; + } + } + + const double startRev = CoinGetTimeOfDay(); + //Conflicts: (x'', y') for (size_t i1 = 0; i1 < icaCount_; i1++) { spArcStart_[icaCount_ + i1] = idxArc; @@ -196,7 +461,12 @@ bool CoinOddWheelSeparator::prepareGraph(double startTime) { assert(spArcTo_[i2] >= icaCount_); #endif const size_t arcTo = spArcTo_[i2] - icaCount_; - const size_t arcDist = spArcDist_[i2]; + /* double, not size_t: the weights are icaActivity_ = 1001 - 1000*x + * and are not integral. Truncating here gave the reverse copy + * (y'', x') of a conflict a length up to 1 shorter than its forward + * arc (x', y''), so the two directions of the same edge disagreed and + * a path was cheaper the more reverse arcs it used. */ + const double arcDist = spArcDist_[i2]; if(idxArc + 1 > spArcCap_) { spArcCap_ *= 2; @@ -211,13 +481,527 @@ bool CoinOddWheelSeparator::prepareGraph(double startTime) { } spArcStart_[icaCount_ * 2] = idxArc; + stats_.tPrepareReverse = CoinGetTimeOfDay() - startRev; + stats_.arcs = idxArc; + + const double startSpf = CoinGetTimeOfDay(); spf_ = new CoinShortestPath(nodes, idxArc, spArcStart_.data(), spArcTo_.data(), spArcDist_.data()); + stats_.tPrepareShortestPath = CoinGetTimeOfDay() - startSpf; return true; } +/** + * Mark the active nodes whose shortest-path call cannot produce a cut. + * + * findOddHolesWithNode(v) asks CoinShortestPath for the minimum-weight path from + * v' to v''. prepareGraph() builds the bipartite double cover -- forward arcs + * (i1 -> icaCount_+i2) and their mirrors (icaCount_+i1 -> i2) -- so such a path + * is exactly an odd-length closed walk through v in the conflict graph, and each + * arc carries its *destination* node's activity, so the path's weight is the sum + * of the activities of the nodes it enters: 3 terms for a triangle, at least 5 + * for anything longer. Activities are 1001 - 1000x in [1, 1001], strictly + * positive, so length can only add weight. + * + * That gives four certificates. Each is a proof about what the call *must* + * return, not a guess about what it probably returns, so a skipped node cannot + * cost a cut -- which is the whole point, since the caller has no way to notice a + * cut that was never generated. + * + * Certificates 1 and 3 are stated first because they are how this was built and + * because they are what certificate 4 is measured against; certificate 4 decides + * the same question exactly and subsumes both, so it is the one that actually + * makes the skip. Only certificate 2 is independent of it. + * + * 1. **Bipartite component.** Two-colour the node's component of the + * symmetrised active subgraph. If that succeeds every arc joins unlike + * colours, so every closed walk has even length and no v' -> v'' path exists + * at all. find() then leaves previous_[v''] unset, path() returns just the + * destination, and the call is charged to oddHolesShort. (Which is why + * "short" must not be read as "found a triangle": oddSize == 0 lands there + * too.) + * + * 2. **Triangle dominance.** Any odd closed walk of length L >= 5 through v + * enters a neighbour of v at position 1, a neighbour of v at position L-1 -- + * two entries in the multiset even when they are the same node -- and at + * least L-3 >= 2 further active nodes. Bounding those two interior entries by + * the *global* minimum activity is far too generous. Instead all four are + * charged to shortest k-arc walks out of and into v, split every possible way: + * + * acti(v) + max over 0<=k<=4 of ( h_k[v] + g_(4-k)[v] ) + * + * which is strictly larger termwise and computable in four passes over the + * arcs. See hOut/gIn below for the derivation and for why the old form + * collapsed on any instance holding a variable at 0 or 1. + * + * If some triangle through v beats that bound, the minimum-weight odd walk + * through v *is* a triangle, find() must return one, and oddSize < 5 discards + * it. Note that walks out of v and walks into v are tracked separately rather + * than collapsed into one relation: nothing in CoinConflictGraph guarantees a + * symmetric adjacency (addNeighbor() is one-directional and symmetry is the + * callers' responsibility), and assuming it here would make the bound too + * large, hence the gate too strong, hence able to lose a cut. + * + * 3. **Outside the 2-core.** A node peeled away by repeatedly removing + * symmetrised degree < 2 lies on no simple cycle, so it lies in no odd + * *hole*, so no call from it can store one. + * + * Certificate 3 is about cuts and NOT about oddHolesShort, and the difference + * is easy to get wrong. The doubled graph lets a walk traverse the same edge + * twice, because v' and v'' are distinct nodes: v of degree 1 whose only + * neighbour a sits on a triangle a-b-c admits the perfectly legal five-arc + * path v' -> a'' -> b' -> c'' -> a' -> v''. So these calls come back long and + * are discarded by the repeated-node filter, not the short-cycle one. A check + * that predicted oddHolesShort for them reported a soundness failure on 31 of + * 336 fixtures; the gate was right and the check was wrong. + * + * 4. **A non-bipartite block.** An odd cycle is 2-connected, so it lies inside a + * single biconnected component; and in a 2-connected non-bipartite graph every + * vertex lies on an odd cycle. So v lies on an odd cycle *if and only if* some + * block containing v is non-bipartite, and a node on no odd cycle is in no odd + * hole. This is exact rather than a bound, and it strictly subsumes 1 and 3: a + * bipartite component has only bipartite blocks, and a node outside the 2-core + * lies only in bridge blocks, which are single edges. It also decides cases + * neither reaches -- a 4-cycle sharing one vertex with a triangle puts the + * 4-cycle's other nodes in a non-bipartite component and inside the 2-core, + * yet on no odd cycle. Certificates 1 and 3 are kept only to attribute the + * skip, so gateBlockOnly prices exactly what this adds. + * + * Cost is O(arcs) for the transpose, the merge, the peel, the colouring, the four + * walk-bound passes and the block decomposition, plus a bounded triangle probe per + * surviving node -- against a Dijkstra over the whole doubled graph per node + * saved. Measured over 337 replay fixtures it skips 88.9% of the calls (945677 -> + * 104683), worth 11.41x on total separation time (1374.06s -> 120.43s) for 1.08s + * of gate, i.e. 0.9% of what is left. Every one of the 63 non-timing output fields + * is string-identical on all 337, and the skipped calls account exactly: + * dSpFindCalls == d(oddHolesShort + oddHolesRepeatedNode) == gateSkipped. + **/ +void CoinOddWheelSeparator::buildFutilityGate() { + const double startGate = CoinGetTimeOfDay(); + const size_t n = icaCount_; + + /* Stage trace, off unless ODDWHEEL_GATE_TRACE is set in the environment. + * tGate alone cannot say which certificate is expensive, and on a large sparse + * graph the answer was not the one the asymptotics suggested. */ + const bool trace = (getenv("ODDWHEEL_GATE_TRACE") != NULL); + double tMark = startGate; +#define ODDWHEEL_GATE_MARK(what) \ + if (trace) { \ + const double now = CoinGetTimeOfDay(); \ + fprintf(stderr, " gate %-12s %8.3fs\n", what, now - tMark); \ + tMark = now; \ + } + + gateSkip_.assign(n, 0); + + /* The primed half's arcs, i.e. the forward ones. prepareGraph() writes + * spArcStart_[icaCount_] when it starts mirroring, so it is the count. */ + const size_t fwdArcs = spArcStart_[n]; + + /* Transpose them, in active-position space. Filling in increasing source + * order leaves each in-list sorted, which the merge below relies on. */ + std::vector inStart(n + 1, 0); + for (size_t k = 0; k < fwdArcs; k++) { + inStart[spArcTo_[k] - n + 1]++; + } + for (size_t i = 0; i < n; i++) { + inStart[i + 1] += inStart[i]; + } + std::vector inTo(fwdArcs); + { + std::vector fill(inStart.begin(), inStart.end() - 1); + for (size_t i = 0; i < n; i++) { + for (size_t k = spArcStart_[i]; k < spArcStart_[i + 1]; k++) { + inTo[fill[spArcTo_[k] - n]++] = i; + } + } + } + ODDWHEEL_GATE_MARK("transpose") + + /* Merge the sorted out- and in-lists into one *deduplicated* undirected + * adjacency. + * + * Materialising this rather than walking the two directed lists in turn is + * not tidiness: the peel below must decrement each endpoint once per + * undirected edge, and on a symmetric graph -- which every fixture here is -- + * a neighbour appears in *both* directed lists. Decrementing twice peels + * nodes that are in the 2-core, which would make certificate 3 skip a node + * that really is on an odd hole, i.e. lose a cut. The same merge also + * absorbs a pair recorded twice within one list. */ + std::vector symStart(n + 1, 0), symTo; + symTo.reserve(2 * fwdArcs); + for (size_t i = 0; i < n; i++) { + symStart[i] = symTo.size(); + size_t a = spArcStart_[i], b = inStart[i]; + const size_t aEnd = spArcStart_[i + 1], bEnd = inStart[i + 1]; + size_t prev = ODDWHEEL_SEP_NOT_ACTIVE; + while (a < aEnd || b < bEnd) { + size_t v; + if (b >= bEnd || (a < aEnd && spArcTo_[a] - n <= inTo[b])) { + v = spArcTo_[a] - n; + a++; + } else { + v = inTo[b]; + b++; + } + if (v != prev) { + symTo.push_back(v); + prev = v; + } + } + } + symStart[n] = symTo.size(); + ODDWHEEL_GATE_MARK("merge") + + /* Certificate 2's bound on a path of >= 5 arcs, in the tightest form one + * O(arcs) sweep can give. + * + * Both parities of a node have the same successor set in the search graph -- + * forward arcs send i' to j'' and their mirrors send i'' to j' for the same j + * -- so one relation Fwd() describes every hop, and a closed walk + * v -> n1 -> ... -> n(L-1) -> v of L >= 5 arcs weighs + * acti(v) + sum acti(n_j), where n1..n(L-1) is a walk out of v and + * n(L-1)..n1 is a walk into v. + * + * Let h_k[i] be the least weight of a k-arc walk leaving i (counting the nodes + * entered) and g_k[i] the same for walks arriving at i. Each is one pass over + * the arcs given the previous: + * + * h_0 = 0, h_k[i] = min over a in Fwd(i) [ acti(a) + h_(k-1)[a] ] + * g_0 = 0, g_k[i] = min over d in In(i) [ acti(d) + g_(k-1)[d] ] + * + * For any 0 <= k <= 4 the first k interior nodes form a k-arc walk out of v and + * the last 4-k form a (4-k)-arc walk into v, so + * + * weight >= acti(v) + h_k[v] + g_(4-k)[v] + * + * is valid -- for L = 5 those are all four interior nodes, and for L >= 7 the + * two stretches are disjoint and the terms left over are positive. Five valid + * bounds, so their maximum is valid too, and it is far stronger than charging + * two of the interior nodes the *global* minimum activity as this did before. + * + * That mattered: activities are 1001 - 1000x, so a single variable sitting at + * x = 1 -- or the complement of one at x = 0, which the doubled graph creates + * whenever any variable is at 0 -- drags the global minimum to 1 and empties + * two of the five terms. Measured over 79 replay fixtures the best triangle + * through a node overshot the old bound by a median of 188 weight units, and + * 36944 nodes carrying a triangle failed the proof for that reason alone -- + * every one of which then came back from the search with a triangle. + * + * INFTY propagates: a node with no k-arc walk contributes no bound from that + * split rather than a wrapped-around sum. If every split is infinite there is + * no >= 5 path through v at all and the node is certainly futile, but that + * cannot happen on a symmetric adjacency, so rather than add a skip class for + * it the node is simply left to the search -- fewer skips is the safe way to + * be wrong. */ + const double INFW = std::numeric_limits::max(); + const size_t HOPS = 4; + std::vector hOut((HOPS + 1) * n, INFW), gIn((HOPS + 1) * n, INFW); + for (size_t i = 0; i < n; i++) { + hOut[i] = 0.0; + gIn[i] = 0.0; + } + for (size_t k = 1; k <= HOPS; k++) { + double *hk = &hOut[k * n], *hp = &hOut[(k - 1) * n]; + double *gk = &gIn[k * n], *gp = &gIn[(k - 1) * n]; + for (size_t i = 0; i < n; i++) { + for (size_t e = spArcStart_[i]; e < spArcStart_[i + 1]; e++) { + const size_t a = spArcTo_[e] - n; + if (hp[a] == INFW) { + continue; + } + const double w = icaActivity_[a] + hp[a]; + if (w < hk[i]) { + hk[i] = w; + } + } + for (size_t e = inStart[i]; e < inStart[i + 1]; e++) { + const size_t d = inTo[e]; + if (gp[d] == INFW) { + continue; + } + const double w = icaActivity_[d] + gp[d]; + if (w < gk[i]) { + gk[i] = w; + } + } + } + } + + /* The best of the five splits, per node. INFW marks "no bound available". */ + std::vector lbPath(n, INFW); + for (size_t i = 0; i < n; i++) { + double best = -1.0; + for (size_t k = 0; k <= HOPS; k++) { + const double a = hOut[k * n + i], b = gIn[(HOPS - k) * n + i]; + if (a == INFW || b == INFW) { + continue; + } + if (a + b > best) { + best = a + b; + } + } + if (best >= 0.0) { + lbPath[i] = icaActivity_[i] + best; + } + } + ODDWHEEL_GATE_MARK("karc") + + /* Certificate 3: peel down to the 2-core. Peeling the *undirected* graph is + * the conservative direction -- it has at least as many edges as the directed + * one, so its 2-core contains every node that lies on a directed cycle. */ + std::vector symDeg(n); + for (size_t i = 0; i < n; i++) { + symDeg[i] = symStart[i + 1] - symStart[i]; + } + std::vector peeled(n, 0); + std::vector stack; + for (size_t i = 0; i < n; i++) { + if (symDeg[i] < 2) { + peeled[i] = 1; + stack.push_back(i); + } + } + while (!stack.empty()) { + const size_t v = stack.back(); + stack.pop_back(); + for (size_t k = symStart[v]; k < symStart[v + 1]; k++) { + const size_t u = symTo[k]; + if (peeled[u] || u == v) { + continue; + } + if (symDeg[u]) { + symDeg[u]--; + } + if (symDeg[u] < 2) { + peeled[u] = 1; + stack.push_back(u); + } + } + } + ODDWHEEL_GATE_MARK("peel") + + /* Certificate 1: two-colour each component. Undirected again, and again the + * conservative direction: more edges means the colouring fails more often, + * so fewer nodes are certified. */ + std::vector colour(n, -1); + std::vector bipartite(n, 0); + std::vector comp, queue; + for (size_t s = 0; s < n; s++) { + if (colour[s] >= 0) { + continue; + } + comp.clear(); + queue.clear(); + colour[s] = 0; + queue.push_back(s); + bool twoColourable = true; + for (size_t head = 0; head < queue.size(); head++) { + const size_t v = queue[head]; + comp.push_back(v); + for (size_t k = symStart[v]; k < symStart[v + 1]; k++) { + const size_t u = symTo[k]; + if (colour[u] < 0) { + colour[u] = colour[v] ^ 1; + queue.push_back(u); + } else if (colour[u] == colour[v]) { + twoColourable = false; + } + } + } + if (twoColourable) { + for (size_t k = 0; k < comp.size(); k++) { + bipartite[comp[k]] = 1; + } + } + } + ODDWHEEL_GATE_MARK("colour") + + /* Certificate 4, which strictly subsumes both of the two above and is exact + * rather than a bound. + * + * An odd cycle is 2-connected, so it lies entirely inside one block + * (biconnected component). Conversely, in a 2-connected non-bipartite graph + * every vertex lies on an odd cycle. Hence + * + * v lies on an odd cycle <=> some block containing v is non-bipartite + * + * and a node on no odd cycle is in no odd hole, so no call from it can yield + * a cut. Both older certificates are special cases: a bipartite component has + * only bipartite blocks, and a node outside the 2-core lies only in bridge + * blocks, which are single edges and therefore bipartite. It also decides + * cases neither can reach -- a node inside a bipartite block hanging off a cut + * vertex whose other block is not, say a 4-cycle sharing one vertex with a + * triangle: the component is non-bipartite (certificate 1 fails) and the node + * is in the 2-core (certificate 3 fails), yet it is on no odd cycle. + * + * The bipartiteness of a block is decided from DFS depth parity, not by + * re-colouring it. The DFS tree restricted to a block is a spanning tree of + * that block, cycle parity is linear over GF(2), so the block is bipartite iff + * every fundamental cycle is even -- and the fundamental cycle closed by an + * edge (v,u) has length depth[v] - depth[u] + 1, which is odd exactly when the + * two depths share parity. Tree edges always join opposite parities, so the + * same test can be applied to every edge of the block without distinguishing + * them. That makes the whole certificate one O(n + arcs) pass with no nested + * containers. + * + * A self-loop, if the graph ever carried one, joins equal parities and so + * marks its node as on an odd cycle: no skip, which is the safe direction. + */ + std::vector disc(n, 0), low(n, 0), iter(n, 0); + std::vector dfsPar(n, ODDWHEEL_SEP_NOT_ACTIVE); + std::vector dpar(n, 0); + std::vector onOddCycle(n, 0); + std::vector > estack, blk; + std::vector dfs; + size_t timer = 0; + + for (size_t s = 0; s < n; s++) { + if (disc[s]) { + continue; + } + dfs.clear(); + disc[s] = low[s] = ++timer; + iter[s] = symStart[s]; + dfsPar[s] = ODDWHEEL_SEP_NOT_ACTIVE; + dpar[s] = 0; + dfs.push_back(s); + while (!dfs.empty()) { + const size_t v = dfs.back(); + if (iter[v] < symStart[v + 1]) { + const size_t u = symTo[iter[v]++]; + if (u == v) { + /* Self-loop: an odd closed walk of length 1 through v. */ + onOddCycle[v] = 1; + } else if (!disc[u]) { + estack.push_back(std::pair(v, u)); + dfsPar[u] = v; + dpar[u] = dpar[v] ^ 1; + disc[u] = low[u] = ++timer; + iter[u] = symStart[u]; + dfs.push_back(u); + } else if (u != dfsPar[v] && disc[u] < disc[v]) { + /* A back edge. symTo is deduplicated, so the parent appears + * exactly once and skipping it cannot discard a second, + * genuinely distinct edge. */ + estack.push_back(std::pair(v, u)); + if (disc[u] < low[v]) { + low[v] = disc[u]; + } + } + } else { + dfs.pop_back(); + if (dfs.empty()) { + break; + } + const size_t p = dfs.back(); + if (low[v] < low[p]) { + low[p] = low[v]; + } + if (low[v] >= disc[p]) { + /* p articulates v's subtree (or is the root): everything + * pushed since the tree edge (p,v) is exactly one block. */ + blk.clear(); + while (!estack.empty()) { + const std::pair e = estack.back(); + estack.pop_back(); + blk.push_back(e); + if (e.first == p && e.second == v) { + break; + } + } + bool oddBlock = false; + for (size_t k = 0; k < blk.size(); k++) { + if (((dpar[blk[k].first] ^ dpar[blk[k].second]) & 1) == 0) { + oddBlock = true; + break; + } + } + if (oddBlock) { + for (size_t k = 0; k < blk.size(); k++) { + onOddCycle[blk[k].first] = 1; + onOddCycle[blk[k].second] = 1; + } + } + } + } + } + /* Nothing should be left for this root, but do not let a stray edge leak + * into the next root's blocks if it ever is. */ + estack.clear(); + } + ODDWHEEL_GATE_MARK("blocks") + + /* Certificate 2, and the tally. The exact verdict comes first, so the + * triangle probe only runs on nodes that really do lie on an odd cycle. */ + std::vector > cand; + for (size_t i = 0; i < n; i++) { + if (!onOddCycle[i]) { + gateSkip_[i] = 1; + /* One skip, attributed to the weakest certificate that also reaches + * it, so gateBlockOnly prices exactly what certificate 4 adds. */ + if (bipartite[i]) { + stats_.gateBipartite++; + } else if (peeled[i]) { + stats_.gateNoCycle++; + } else { + stats_.gateBlockOnly++; + } + continue; + } + + if (lbPath[i] != INFW) { + const double lb5 = lbPath[i]; + + cand.clear(); + for (size_t k = spArcStart_[i]; k < spArcStart_[i + 1]; k++) { + const size_t v = spArcTo_[k] - n; + cand.push_back(std::pair(icaActivity_[v], v)); + } + std::sort(cand.begin(), cand.end()); + if (cand.size() > ODDWHEEL_SEP_GATE_TRI_CAND) { + cand.resize(ODDWHEEL_SEP_GATE_TRI_CAND); + } + + bool dominated = false; + for (size_t a = 0; a < cand.size() && !dominated; a++) { + const size_t va = cand[a].second; + for (size_t k = spArcStart_[va]; k < spArcStart_[va + 1]; k++) { + const size_t vb = spArcTo_[k] - n; + if (vb == i || vb == va) { + continue; + } + /* The closing arc vb -> i must exist in the graph the search + * actually walks, so ask the arc list and not the conflict + * graph. spArcTo_ is emitted in ascending order per node. */ + if (!std::binary_search(spArcTo_.begin() + spArcStart_[vb], + spArcTo_.begin() + spArcStart_[vb + 1], + i + n)) { + continue; + } + if (icaActivity_[i] + cand[a].first + icaActivity_[vb] < lb5) { + dominated = true; + break; + } + } + } + if (dominated) { + gateSkip_[i] = 1; + stats_.gateTriangle++; + continue; + } + } + } + ODDWHEEL_GATE_MARK("triangle") +#undef ODDWHEEL_GATE_MARK + stats_.gateSkipped = stats_.gateBipartite + stats_.gateNoCycle + + stats_.gateBlockOnly + stats_.gateTriangle; + stats_.tGate = CoinGetTimeOfDay() - startGate; +} + void CoinOddWheelSeparator::findOddHolesWithNode(size_t node) { const size_t dest = icaCount_ + node; + stats_.spFindCalls++; spf_->find(node, dest); size_t oddSize = spf_->path(dest, tmp_.data()); @@ -229,6 +1013,10 @@ void CoinOddWheelSeparator::findOddHolesWithNode(size_t node) { oddSize--; if (oddSize < 5) { + stats_.oddHolesShort++; + if (recordOutcomes_) { + nodeOutcome_[node] = OUTCOME_SHORT; + } return; } @@ -246,6 +1034,10 @@ void CoinOddWheelSeparator::findOddHolesWithNode(size_t node) { for (size_t j = 0; j <= i; j++) { iv_[tmp_[j]] = 0; } + stats_.oddHolesRepeatedNode++; + if (recordOutcomes_) { + nodeOutcome_[node] = OUTCOME_REPEATED; + } return; } @@ -267,19 +1059,40 @@ void CoinOddWheelSeparator::findOddHolesWithNode(size_t node) { const double rhs = floor(oddSize / 2.0); const double viol = lhs - rhs; if (viol + ODDWHEEL_SEP_DEF_EPS <= ODDWHEEL_SEP_DEF_MIN_VIOL) { + stats_.oddHolesNotViolated++; + if (recordOutcomes_) { + nodeOutcome_[node] = OUTCOME_NOT_VIOLATED; + } return; } - addOddHole(oddSize, tmp_); + const bool stored = addOddHole(oddSize, tmp_); + if (recordOutcomes_) { + nodeOutcome_[node] = stored ? OUTCOME_KEPT : OUTCOME_DUPLICATE; + } } bool CoinOddWheelSeparator::addOddHole(size_t nz, const std::vector &idxs) { // checking for repeated entries if (alreadyInserted(nz, idxs)) { + stats_.oddHolesDuplicate++; return false; } - ohIdxs_.push_back(idxs); + /* Only the first nz entries of idxs describe the hole -- it is the shared + * tmp_ scratch buffer, sized cgSize + 1. Pushing the whole vector made + * oddHoleSize() report cgSize + 1 and oddWheelRHS() report floor(cgSize/2) + * = numCols, with the tail carrying stale (mostly zero-initialised) + * entries. Three consequences, all measured on the 237 replay fixtures: + * - CglOddWheel translated the stale tail into cut coefficients, so + * every hole hit its repeated-column guard and was discarded + * (2528 of 2528, zero cuts ever emitted); + * - alreadyInserted() compares nz against the stored size, which could + * then never match, so the duplicate-hole check never fired (0 hits); + * - searchWheelCenter() filtered candidates on `degree < ohSize` with + * ohSize = cgSize + 1, rejecting every wheel centre (0 found), and + * ran its conflicting() loop over the whole tail. */ + ohIdxs_.push_back(std::vector(idxs.begin(), idxs.begin() + nz)); return true; } @@ -348,15 +1161,19 @@ void CoinOddWheelSeparator::searchWheelCenter(size_t idxOH) { // generating candidates const std::pair rescg = cgraph_->conflictingNodes(nodeSD, tmp_.data(), iv2_.data()); size_t numCandidates = 0; + stats_.wcCalls++; + stats_.wcPool += rescg.first; for (size_t i = 0; i < rescg.first; i++) { const size_t node = rescg.second[i]; //already inserted if (iv_[node]) { + stats_.wcRejInCycle++; continue; } if (cgraph_->degree(node) < ohSize) { + stats_.wcRejDegree++; continue; } @@ -368,14 +1185,18 @@ void CoinOddWheelSeparator::searchWheelCenter(size_t idxOH) { } } if (!insert) { + stats_.wcRejAdjacency++; continue; } //new candidate if (x_[node] >= ODDWHEEL_SEP_DEF_EPS || rc_[node] <= ODDWHEEL_SEP_DEF_MAX_RC) { tmp_[numCandidates++] = node; + } else { + stats_.wcRejCost++; } } + stats_.wcCandidates += numCandidates; if (numCandidates != 0) { size_t sizeWC = 0; @@ -408,6 +1229,8 @@ void CoinOddWheelSeparator::searchWheelCenter(size_t idxOH) { } if (insert) { tmp_[sizeWC++] = selected; + } else { + stats_.wcCliqueDropped++; } } } diff --git a/src/CoinOddWheelSeparator.hpp b/src/CoinOddWheelSeparator.hpp index 6d95607b..002f4abf 100644 --- a/src/CoinOddWheelSeparator.hpp +++ b/src/CoinOddWheelSeparator.hpp @@ -103,6 +103,150 @@ class COINUTILSLIB_EXPORT CoinOddWheelSeparator { **/ inline void setMaxSeconds(double maxSeconds) { maxSeconds_ = maxSeconds; } + /** + * Build the arcs of the auxiliary graph *both* ways and check that they agree, + * reporting the result in Stats::prepareMismatches. + * + * prepareGraph() picks between testing all pairs of active nodes and walking + * each one's neighbourhood, which are equivalent only on a symmetric conflict + * graph -- something addNeighbor() leaves to its callers rather than enforcing. + * This exists to settle that empirically per graph. Off by default: it roughly + * doubles the cost of graph preparation, and the check is additive, so the + * selected path's arcs and timing are the same either way. + **/ + inline void setVerifyPrepare(bool verify) { verifyPrepare_ = verify; } + + /** + * Enable (default) or disable the futility gate. + * + * The gate proves, per active node, that no shortest-path call from it can + * produce a cut, and skips the call. It is a certificate and not a heuristic -- + * see buildFutilityGate() -- so the cut set is unchanged; only Stats::spFindCalls + * and the rejection counters move. The setter exists so that can be *checked* + * rather than asserted, by replaying a fixture both ways and comparing every + * output field. + **/ + inline void setUseFutilityGate(bool use) { useGate_ = use; } + + /** + * Record, per active node, which of the five outcomes its shortest-path call + * reached. Off by default; diagnostic only. Costs one byte per active node and + * one store per call, so it does not perturb what is measured -- the point is + * to be able to *label* nodes for a feature study rather than infer the label + * from aggregate counters, which cannot be done at all (541356 of 643697 calls + * land in one bucket). + * + * Run this with the futility gate OFF, or every skipped node reports + * OUTCOME_NOT_CALLED and the labels are missing exactly where the gate fired. + **/ + inline void setRecordNodeOutcomes(bool record) { recordOutcomes_ = record; } + + /** Outcome codes stored in nodeOutcomes(). */ + enum NodeOutcome { + OUTCOME_NOT_CALLED = 0, /**< the gate skipped it, or the time limit cut the loop short */ + OUTCOME_SHORT = 1, /**< oddSize < 5: a triangle, or no odd walk at all */ + OUTCOME_REPEATED = 2, /**< a column appeared twice, so it is a walk and not a hole */ + OUTCOME_NOT_VIOLATED = 3,/**< a genuine odd hole, but not violated by enough */ + OUTCOME_DUPLICATE = 4, /**< violated, but the same hole was already stored */ + OUTCOME_KEPT = 5 /**< stored in ohIdxs_ */ + }; + + /** + * Per active node outcome, indexed the way icaIdx_ is: ascending column index + * over the doubled graph, filtered by degree >= 2 and x > MIN_FRAC. Empty + * unless setRecordNodeOutcomes(true) was called. + **/ + inline const std::vector &nodeOutcomes() const { return nodeOutcome_; } + + /** Number of active nodes, i.e. the length of nodeOutcomes(). */ + inline size_t activeCount() const { return icaCount_; } + + /** Column (in the doubled graph) of active node i. */ + inline size_t activeColumn(size_t i) const { return icaIdx_[i]; } + + /** + * Counters and per-stage times of the last searchOddWheels() call. + * Filled unconditionally; the whole struct costs a dozen clock reads + * per call, against loops that are quadratic in activeColumns. + * + * The rejection counters sum with oddHolesFound to spFindCalls: every + * shortest-path call either yields a stored odd hole or is discarded by + * exactly one of the four filters. + **/ + struct Stats { + size_t activeColumns; /**< icaCount_: nodes of the doubled graph that are considered */ + size_t arcs; /**< arcs handed to the shortest-path solver */ + size_t prepareMethod; /**< how prepareGraph() found the conflicts: 1 = all pairs, 2 = neighbour walk */ + size_t prepareWalkCost; /**< neighborWalkCost(): what method 2 costs, against activeColumns^2 for method 1 */ + size_t prepareUnsorted; /**< 1 if a neighbour list came back out of order and the walk was discarded */ + size_t prepareVerifyArcs; /**< setVerifyPrepare(): arcs the *other* method produced */ + size_t prepareMismatches; /**< setVerifyPrepare(): arcs in the symmetric difference of the two; must be 0 */ + size_t prepareWalkOnly; /**< setVerifyPrepare(): of those, the ones only the neighbour walk found */ + size_t preparePairOnly; /**< setVerifyPrepare(): of those, the ones only the pairwise loop found */ + size_t spFindCalls; /**< calls to CoinShortestPath::find() */ + size_t oddHolesFound; /**< odd holes stored (== numOddWheels()) */ + size_t oddHolesShort; /**< discarded: cycle shorter than 5 */ + size_t oddHolesRepeatedNode; /**< discarded: a node appears twice in the cycle */ + size_t oddHolesNotViolated; /**< discarded: violation below the threshold */ + size_t oddHolesDuplicate; /**< discarded: same node set already stored */ + size_t wheelCenters; /**< odd holes that received a non-empty wheel center */ + size_t wheelCenterElements; /**< total wheel-center elements over all odd holes */ + + /* searchWheelCenter() attribution. A wheel centre must conflict with every + * node of the cycle, so the candidate pool is the neighbourhood of the + * cycle's *minimum-degree* node -- a correct superset, since a centre + * conflicting with all of C conflicts with that node in particular. Each + * pooled node is then dropped by exactly one of four filters or survives: + * wcPool == wcRejInCycle + wcRejDegree + wcRejAdjacency + wcRejCost + * + wcCandidates + * Only the first three are validity conditions. wcRejCost is a *heuristic* + * gate (x >= EPS or rc <= MAX_RC): admitting one of those nodes would still + * give a valid wheel, it just adds alpha * z*_w = 0 to the measured + * violation. So a large wcRejCost means stronger cuts are being declined, + * whereas a large wcRejAdjacency means the graph simply has no centre. */ + size_t wcCalls; /**< searchWheelCenter() invocations (== odd holes stored) */ + size_t wcPool; /**< nodes returned by conflictingNodes(minDegreeNode), summed */ + size_t wcRejInCycle; /**< dropped: node is itself in the cycle */ + size_t wcRejDegree; /**< dropped: degree < |C|, so it cannot conflict with all of C */ + size_t wcRejAdjacency; /**< dropped: fails to conflict with some node of C */ + size_t wcRejCost; /**< dropped: x < EPS and rc > MAX_RC (heuristic gate, not validity) */ + size_t wcCandidates; /**< survived all four filters, summed over calls */ + size_t wcCliqueDropped; /**< extMethod 2: candidates rejected by the clique test */ + + /* buildFutilityGate() attribution. Each skipped node carries a proof that a + * shortest-path call from it cannot yield a cut, so gateSkipped is time saved + * and never a cut lost. The four reasons are disjoint and gateSkipped is + * their sum. + * + * The first three are not tested in strength order but in *weakness* order, + * on purpose. One certificate -- "some biconnected block containing the node + * is non-bipartite" -- decides "lies on an odd cycle" exactly, and it strictly + * subsumes both the bipartite-component and the outside-the-2-core tests. So + * a single check makes the skip decision, and the two older predicates are + * kept only to attribute it: whichever weaker certificate would also have + * caught this node gets the count, and gateBlockOnly is therefore exactly what + * the block certificate adds over the two it replaced. */ + size_t gateSkipped; /**< active nodes whose shortest-path call was skipped */ + size_t gateBipartite; /**< skipped: the node's component admits no odd closed walk at all */ + size_t gateNoCycle; /**< skipped: outside the 2-core, so on no simple cycle and in no hole */ + size_t gateBlockOnly; /**< skipped: on no odd cycle, and neither weaker certificate saw it */ + size_t gateTriangle; /**< skipped: the lightest odd walk through it is provably a triangle */ + + bool timeLimitReached; /**< searchOddWheels() aborted on maxSeconds_ */ + double tActiveColumns; /**< fillActiveColumns() */ + double tPrepareArcs; /**< prepareGraph(): the (x',y'') conflict scan */ + double tPrepareReverse; /**< prepareGraph(): mirroring them into (x'',y') */ + double tPrepareShortestPath; /**< prepareGraph(): the CoinShortestPath constructor */ + double tGate; /**< buildFutilityGate() */ + double tSearch; /**< the findOddHolesWithNode() loop */ + double tWheelCenter; /**< the searchWheelCenter() loop */ + }; + + /** + * Statistics of the last searchOddWheels() call. + **/ + inline const Stats &stats() const { return stats_; } + private: /** * Select interesting columns that will be @@ -116,6 +260,36 @@ class COINUTILSLIB_EXPORT CoinOddWheelSeparator { **/ bool prepareGraph(double startTime); + /** + * Cost of the neighbour-walk way of finding the conflicts, for comparison + * against the icaCount_^2 of testing every pair. See the .cpp. + **/ + size_t neighborWalkCost() const; + + /** + * Build the (x', y'') arcs into the given arrays, by testing all pairs of + * active nodes (useWalk false) or by walking each one's neighbourhood + * (useWalk true). Both emit the same arcs in the same order; see the .cpp. + * + * sawUnsorted, when not NULL, is set if a neighbour list came back out of + * order, which is the one case where the walk cannot reproduce the pairwise + * order; the caller must then discard its output. Never written false, so a + * caller may reuse one flag across calls. + * + * Returns false if aborted early due to time limit. + **/ + bool buildForwardArcs(bool useWalk, double startTime, + std::vector &arcStart, std::vector &arcTo, + std::vector &arcDist, size_t &arcCap, size_t &idxArc, + bool *sawUnsorted); + + /** + * Mark active nodes whose shortest-path call provably cannot yield a cut. + * Reads the arcs prepareGraph() built; fills gateSkip_. See the .cpp for the + * four certificates and why each one is sound. + **/ + void buildFutilityGate(); + /** * Try to find an odd whole (a most violated) that * contains a given node. @@ -193,6 +367,26 @@ class COINUTILSLIB_EXPORT CoinOddWheelSeparator { **/ size_t spArcCap_; + /** + * buildFutilityGate(): 1 if the search should skip this active node. + **/ + std::vector gateSkip_; + + /** + * Whether to run buildFutilityGate() at all. See setUseFutilityGate(). + **/ + bool useGate_; + + /** + * Per active node outcome, see setRecordNodeOutcomes(). + **/ + std::vector nodeOutcome_; + + /** + * Whether nodeOutcome_ is filled. See setRecordNodeOutcomes(). + **/ + bool recordOutcomes_; + /** * Auxiliary array **/ @@ -234,6 +428,17 @@ class COINUTILSLIB_EXPORT CoinOddWheelSeparator { * Wall-clock time limit for searchOddWheels(), 0 = unlimited. **/ double maxSeconds_; + + /** + * Whether prepareGraph() cross-checks its two ways of finding the conflicts, + * see setVerifyPrepare(). + **/ + bool verifyPrepare_; + + /** + * Counters and per-stage times, see stats(). + **/ + Stats stats_; }; diff --git a/src/CoinParam.cpp b/src/CoinParam.cpp index 7c9a7ab2..8c39144b 100644 --- a/src/CoinParam.cpp +++ b/src/CoinParam.cpp @@ -53,6 +53,8 @@ CoinParam::CoinParam() , shortHelp_() , longHelp_() , display_(displayPriorityNone) + , autoAllowed_(false) + , userSet_(false) { /* Nothing to be done here */ } @@ -88,6 +90,8 @@ CoinParam::CoinParam(std::string name, std::string help, , shortHelp_(help) , longHelp_(longHelp) , display_(displayPriority) + , autoAllowed_(false) + , userSet_(false) { processName(); } @@ -123,6 +127,8 @@ CoinParam::CoinParam(std::string name, std::string help, , shortHelp_(help) , longHelp_(longHelp) , display_(displayPriority) + , autoAllowed_(false) + , userSet_(false) { processName(); } @@ -158,6 +164,8 @@ CoinParam::CoinParam(std::string name, CoinParamType type, , shortHelp_(help) , longHelp_(longHelp) , display_(displayPriority) + , autoAllowed_(false) + , userSet_(false) { processName(); } @@ -186,12 +194,15 @@ CoinParam::CoinParam(const CoinParam &orig) , pushFunc_(orig.pushFunc_) , pullFunc_(orig.pullFunc_) , display_(orig.display_) + , autoAllowed_(orig.autoAllowed_) + , userSet_(orig.userSet_) { name_ = orig.name_; strValue_ = orig.strValue_; definedKwds_ = orig.definedKwds_; shortHelp_ = orig.shortHelp_; longHelp_ = orig.longHelp_; + topic_ = orig.topic_; } /* @@ -230,6 +241,9 @@ CoinParam &CoinParam::operator=(const CoinParam &rhs) shortHelp_ = rhs.shortHelp_; longHelp_ = rhs.longHelp_; display_ = rhs.display_; + topic_ = rhs.topic_; + autoAllowed_ = rhs.autoAllowed_; + userSet_ = rhs.userSet_; } return *this; @@ -380,8 +394,8 @@ std::string CoinParam::printLongHelp() const switch (type_) { case paramDbl: { buffer << "" - << std::endl; + << upperDblValue_ << (autoAllowed_ ? ", or auto" : "") + << ";\n\tcurrent " << valueString() << ">" << std::endl; assert(upperDblValue_ > lowerDblValue_); break; } @@ -417,8 +431,8 @@ std::string CoinParam::printLongHelp() const } } buffer << "" - << std::endl; + << upperIntValue_ << (autoAllowed_ ? ", or auto" : "") + << ";\n\tcurrent " << valueString() << ">" << std::endl; assert(upperIntValue_ > lowerIntValue_); break; } @@ -598,6 +612,7 @@ int CoinParam::restoreDefault(){ case paramKwd: currentMode_ = defaultMode_; currentKwd_ = defaultKwd_; + userSet_ = false; return 0; default: std::cout << "restoreDefault() is illegal for parameter of this type!" @@ -746,7 +761,7 @@ int CoinParam::readValue(std::deque &inputQueue, if (field.empty()){ std::ostringstream buffer; - buffer << "Parameter '" << name_ << "' has value " << intValue_ + buffer << "Parameter '" << name_ << "' has value " << valueString() << std::endl; *message = buffer.str(); return 2; @@ -764,6 +779,11 @@ int CoinParam::readValue(std::deque &inputQueue, } #endif + if (autoAllowed_ && field == "auto") { + value = autoIntValue(); + return 0; + } + char c; std::stringstream ss(field); ss >> value; @@ -836,7 +856,7 @@ int CoinParam::readValue(std::deque &inputQueue, if (field.empty()){ std::ostringstream buffer; - buffer << "Parameter '" << name_ << "' has value " << dblValue_ + buffer << "Parameter '" << name_ << "' has value " << valueString() << std::endl; *message = buffer.str(); return 2; @@ -853,6 +873,11 @@ int CoinParam::readValue(std::deque &inputQueue, } #endif + if (autoAllowed_ && field == "auto") { + value = autoDblValue(); + return 0; + } + char c; std::stringstream ss(field); ss >> value; @@ -972,6 +997,17 @@ int CoinParam::setKwdVal(const std::string newKwd, std::string *message, { assert(type_ == paramKwd); + if (autoAllowed_ && newKwd == "auto") { + currentMode_ = defaultMode_; + currentKwd_ = defaultKwd_; + userSet_ = false; + if (message) + message->clear(); + if (pMode == pushOn){ + pushFunc_(*this); + } + return 0; + } int newMode = kwdToMode(newKwd); if (newMode >= 0) { if (message){ @@ -986,6 +1022,7 @@ int CoinParam::setKwdVal(const std::string newKwd, std::string *message, } currentKwd_ = modeString(newMode); // to get full name currentMode_ = newMode; + userSet_ = true; if (pMode == pushOn){ pushFunc_(*this); } @@ -1016,6 +1053,7 @@ int CoinParam::setKwdValDefault(const std::string newKwd, std::string *message) defaultKwd_ = currentKwd_ = newKwd; defaultMode_ = currentMode_ = newMode; + userSet_ = false; return 0; }else{ @@ -1060,6 +1098,7 @@ int CoinParam::setModeVal(int newMode, std::string *message, ParamPushMode pMode } currentKwd_ = newKwd; currentMode_ = newMode; + userSet_ = true; if (pMode == pushOn){ pushFunc_(*this); } @@ -1067,6 +1106,15 @@ int CoinParam::setModeVal(int newMode, std::string *message, ParamPushMode pMode } } +int CoinParam::setModeValAuto(int newMode, std::string *message, + ParamPushMode pMode) +{ + bool wasUserSet = userSet_; + int rc = setModeVal(newMode, message, pMode); + userSet_ = wasUserSet; + return rc; +} + int CoinParam::setModeValDefault(int newMode, std::string *message) { assert(type_ == paramKwd); @@ -1096,6 +1144,7 @@ int CoinParam::setModeValDefault(int newMode, std::string *message) defaultKwd_ = currentKwd_ = newKwd; defaultMode_ = currentMode_ = newMode; + userSet_ = false; return 0; } @@ -1441,7 +1490,8 @@ int CoinParam::setDblVal(double value, std::string *message, ParamPushMode pMode { assert(type_ == paramDbl); - if (value < lowerDblValue_ || value > upperDblValue_) { + bool isAutoValue = autoAllowed_ && value == autoDblValue(); + if (!isAutoValue && (value < lowerDblValue_ || value > upperDblValue_)) { if (message){ std::ostringstream buffer; buffer << value << " was provided for " << name_; @@ -1455,7 +1505,12 @@ int CoinParam::setDblVal(double value, std::string *message, ParamPushMode pMode if (value != dblValue_) { std::ostringstream buffer; buffer << name_ << " was changed from "; - buffer << dblValue_ << " to " << value << std::endl; + buffer << valueString() << " to "; + if (isAutoValue) + buffer << "auto"; + else + buffer << value; + buffer << std::endl; *message = buffer.str(); } else { message->clear(); @@ -1473,7 +1528,8 @@ int CoinParam::setDblValDefault(double value, std::string *message) { assert(type_ == paramDbl); - if (value < lowerDblValue_ || value > upperDblValue_) { + bool isAutoValue = autoAllowed_ && value == autoDblValue(); + if (!isAutoValue && (value < lowerDblValue_ || value > upperDblValue_)) { if (message){ std::ostringstream buffer; buffer << value << " was provided as default value for " << name_; @@ -1483,13 +1539,9 @@ int CoinParam::setDblValDefault(double value, std::string *message) } return 1; } else { - if (message){ - std::ostringstream buffer; - buffer << name_ << " default was set to " << value << std::endl; - *message = buffer.str(); - } - dblDefaultValue_ = dblValue_ = value; + if (message) + *message = name_ + " default was set to " + valueString() + "\n"; return 0; } @@ -1538,7 +1590,8 @@ int CoinParam::setIntVal(int value, std::string *message, ParamPushMode pMode) { assert(type_ == paramInt); - if (value < lowerIntValue_ || value > upperIntValue_) { + bool isAutoValue = autoAllowed_ && value == autoIntValue(); + if (!isAutoValue && (value < lowerIntValue_ || value > upperIntValue_)) { if (message){ std::ostringstream buffer; buffer << value << " was provided for " << name_; @@ -1551,8 +1604,13 @@ int CoinParam::setIntVal(int value, std::string *message, ParamPushMode pMode) if (message){ if (value != intValue_) { std::ostringstream buffer; - buffer << name_ << " was changed from " << intValue_; - buffer << " to " << value << std::endl; + buffer << name_ << " was changed from " << valueString(); + buffer << " to "; + if (isAutoValue) + buffer << "auto"; + else + buffer << value; + buffer << std::endl; *message = buffer.str(); } else { message->clear(); @@ -1570,7 +1628,8 @@ int CoinParam::setIntValDefault(int value, std::string *message) { assert(type_ == paramInt); - if (value < lowerIntValue_ || value > upperIntValue_) { + bool isAutoValue = autoAllowed_ && value == autoIntValue(); + if (!isAutoValue && (value < lowerIntValue_ || value > upperIntValue_)) { if (message){ std::ostringstream buffer; buffer << value << " was provided as default value for " << name_; @@ -1580,13 +1639,9 @@ int CoinParam::setIntValDefault(int value, std::string *message) } return 1; } else { - if (message){ - std::ostringstream buffer; - buffer << name_ << " default was set to " << value << std::endl; - *message = buffer.str(); - } - intDefaultValue_ = intValue_ = value; + if (message) + *message = name_ + " default was set to " + valueString() + "\n"; return 0; } @@ -1627,6 +1682,55 @@ int CoinParam::upperIntVal() const return(upperIntValue_); } +/* + Methods for the value `auto' of an integer or double parameter. +*/ + +bool CoinParam::isAuto() const +{ + if (!autoAllowed_) + return false; + if (type_ == paramKwd) + return !userSet_; + if (type_ == paramInt) + return intValue_ == autoIntValue(); + if (type_ == paramDbl) + return dblValue_ == autoDblValue(); + return false; +} + +bool CoinParam::isDefault() const +{ + switch (type_) { + case paramKwd: + if (autoAllowed_) + return !userSet_; + return currentMode_ == defaultMode_; + case paramInt: + return intValue_ == intDefaultValue_; + case paramDbl: + return dblValue_ == dblDefaultValue_; + case paramStr: + return strValue_ == strDefaultValue_; + case paramAct: + return true; + default: + return false; + } +} + +std::string CoinParam::valueString() const +{ + if (isAuto()) + return "auto"; + std::ostringstream buffer; + if (type_ == paramInt) + buffer << intValue_; + else if (type_ == paramDbl) + buffer << dblValue_; + return buffer.str(); +} + // Prints parameter options void CoinParam::printOptions(std::string *message) { @@ -1643,6 +1747,8 @@ void CoinParam::printOptions(std::string *message) } buffer << " " << thisOne; } + if (autoAllowed_) + buffer << " auto"; assert(currentMode_ >= 0 && currentMode_ < static_cast< int >(definedKwds_.size())); std::string::size_type shriekPos = currentKwd_.find('!'); @@ -1652,6 +1758,8 @@ void CoinParam::printOptions(std::string *message) current = currentKwd_.substr(0, shriekPos) + "(" + currentKwd_.substr(shriekPos + 1) + ")"; } + if (isAuto()) + current = "auto"; buffer << "\nCurrent setting is " << current << std::endl; if (message == NULL){ @@ -1668,11 +1776,9 @@ void CoinParam::printOptions(std::string *message) std::ostream &operator<<(std::ostream &s, const CoinParam ¶m) { switch (param.type()) { - case CoinParam::paramDbl: { - return (s << param.dblVal()); - } + case CoinParam::paramDbl: case CoinParam::paramInt: { - return (s << param.intVal()); + return (s << param.valueString()); } case CoinParam::paramKwd: { return (s << param.kwdVal()); diff --git a/src/CoinParam.hpp b/src/CoinParam.hpp index d225a0c7..6e755a56 100644 --- a/src/CoinParam.hpp +++ b/src/CoinParam.hpp @@ -385,6 +385,66 @@ class COINUTILSLIB_EXPORT CoinParam /*! \brief Print possible options for a keyword parameter */ void printOptions(std::string *message = NULL); + /*! \name Auto values + + An integer or double parameter may be allowed the value `auto', meaning + that the application resolves the value itself, typically from features + of the instance. `auto' is stored as a sentinel (autoIntValue() or + autoDblValue()) that lies outside every valid range, so no number the + user can type collides with it. The setters accept the sentinel whatever + the range, readValue() maps the word `auto' to it, and valueString() + prints it as `auto'. intVal() and dblVal() return the sentinel itself, so + test isAuto() before using the value. + */ + //@{ + /*! \brief The stored value of an integer parameter set to `auto' */ + static inline int autoIntValue() { return std::numeric_limits< int >::min(); } + + /*! \brief The stored value of a double parameter set to `auto' */ + static inline double autoDblValue() + { + return -std::numeric_limits< double >::infinity(); + } + + /*! \brief Allow (or forbid) the value `auto' */ + inline void setAutoAllowed(bool yes = true) { autoAllowed_ = yes; } + + /*! \brief Whether the value `auto' is allowed */ + inline bool autoAllowed() const { return autoAllowed_; } + + /*! \brief Whether the current value is `auto' + + For a keyword parameter that allows `auto', this is true until the user + sets a value explicitly: the application then picks the value itself (and + may call setModeValAuto() to record its pick without clearing `auto'). + */ + bool isAuto() const; + + /*! \brief Set a keyword parameter on behalf of the application + + Like setModeVal(), but the parameter stays in the `auto' state (isDefault() + remains true, valueString() still prints `auto'). + */ + int setModeValAuto(int value, std::string *message = NULL, + ParamPushMode pMode = pushDefault); + + /*! \brief Whether a keyword parameter was set explicitly by the user */ + inline bool isUserSet() const { return userSet_; } + + /*! \brief Whether the current value matches the default value */ + bool isDefault() const; + + /*! \brief Default mode for a keyword parameter */ + inline int defaultMode() const { return defaultMode_; } + + /*! \brief Default keyword for a keyword parameter */ + inline std::string defaultKwd() const { return defaultKwd_; } + + /*! \brief Current value of an integer or double parameter as text, + `auto' if it is auto */ + std::string valueString() const; + //@} + /*! \brief Add a short help string to a parameter */ inline void setShortHelp(const std::string help) { shortHelp_ = help; } @@ -583,6 +643,12 @@ class COINUTILSLIB_EXPORT CoinParam /// Topic (semantic category, e.g. "Cuts", "Heuristics", "Output") std::string topic_; + + /// Whether an integer or double parameter accepts the value `auto' + bool autoAllowed_; + + /// Whether a keyword parameter was explicitly set (see isAuto()) + bool userSet_; //@} }; diff --git a/src/CoinParamUtils.cpp b/src/CoinParamUtils.cpp index ab016e84..2f454b71 100644 --- a/src/CoinParamUtils.cpp +++ b/src/CoinParamUtils.cpp @@ -187,9 +187,12 @@ int lookupParam(std::string name, CoinParamVec ¶mVec, if (queryCntp && *queryCntp == 999) printMessage = false; - *matchCntp = 0; - *shortCntp = 0; - *queryCntp = 0; + if (matchCntp != 0) + *matchCntp = 0; + if (shortCntp != 0) + *shortCntp = 0; + if (queryCntp != 0) + *queryCntp = 0; /* Is there anything here at all? diff --git a/src/CoinPresolveSingleton.cpp b/src/CoinPresolveSingleton.cpp index 0f426111..dfdeec62 100644 --- a/src/CoinPresolveSingleton.cpp +++ b/src/CoinPresolveSingleton.cpp @@ -611,23 +611,30 @@ slack_singleton_action::presolve(CoinPresolveMatrix *prob, // check everything else a bit later if (acoeff != 1.0) continue; - double currentLower = rlo[iRow]; - double currentUpper = rup[iRow]; - if (coeff == 1.0 && currentLower == 1.0 && currentUpper == 1.0) { - // leave if integer slack on sum x == 1 - bool allInt = true; - for (CoinBigIndex j = mrstrt[iRow]; - j < mrstrt[iRow] + hinrow[iRow]; j++) { - int iColumn = hcol[j]; - double value = fabs(rowels[j]); - if (!integerType[iColumn] || value != 1.0) { - allInt = false; - break; - } + // Leave unit-coefficient integer singletons in an all-integer, + // all-unit-coefficient row alone (packing/covering/assignment-like + // rows: sum x <= k, sum x >= k, or sum x == k, and also equality + // rows of the form col_j - col_i = rhs, which reduce to this same + // pattern once every coefficient has |value| == 1). Substituting + // such a singleton out folds its bounds into the row's rlo/rup and + // silently drops the constraint linking it to the rest of the row; + // B&B can then set the other row variable(s) to a value that + // violates the original row, and postsolve propagates that wrong + // value back into col_i. This protects both the coeff == 1 sum-to-1 + // equality case (the original guard) and its symmetric coeff == -1 + // and non-equality (<=, >=) counterparts. + bool allInt = true; + for (CoinBigIndex j = mrstrt[iRow]; + j < mrstrt[iRow] + hinrow[iRow]; j++) { + int iColumn = hcol[j]; + double value = fabs(rowels[j]); + if (!integerType[iColumn] || value != 1.0) { + allInt = false; + break; } - if (allInt) - continue; // leave as may help search } + if (allInt) + continue; // leave as may help search } if (!prob->colProhibited(iCol)) { double currentLower = rlo[iRow]; diff --git a/src/CoinRational.cpp b/src/CoinRational.cpp index e660e6a7..b5305455 100644 --- a/src/CoinRational.cpp +++ b/src/CoinRational.cpp @@ -26,6 +26,22 @@ bool CoinRational::nearestRational_(double val, double maxdelta, int64_t maxdnom { double intpart; if (floor(val)==val) { + // An integral double can still lie far outside int64_t's range, and the + // conversion is then undefined behaviour -- on x86 it yields INT64_MIN. + // Returning that as a numerator poisons every gcd/lcm derived from it, and + // INT64_MIN % -1 is a signed-division overflow, i.e. SIGFPE. That is how a + // CglGMI cut carrying an rhs of 3.6e300 crashed + // OsiCuts::insertIfNotDuplicateAndClean. + // + // Report "no nearby rational" instead. The constructor turns a false return + // into 0/1, which every caller already handles as "cannot scale" -- this + // path is not otherwise reachable, because the early return here is the + // only one that skips the accuracy check at the end of the function. + // + // Both bounds are exactly representable (they are +/-2^63), and writing the + // test negated also rejects NaN. + if (!(val >= -9223372036854775808.0 && val < 9223372036854775808.0)) + return false; numerator_ = (int64_t) val; denominator_ = 1; return true; diff --git a/src/CoinRowType.hpp b/src/CoinRowType.hpp index b9d821ac..29a583b6 100644 --- a/src/CoinRowType.hpp +++ b/src/CoinRowType.hpp @@ -24,6 +24,7 @@ enum CoinRowType { CoinRowKnapsack, CoinRowIntKnapsack, CoinRowBinPacking, + CoinRowHubImplication, CoinRowFlowBin, CoinRowFlowMixed, CoinRowMixedBin, @@ -38,7 +39,7 @@ inline const char *coinRowTypeName(CoinRowType t) static const char *names[] = { "Packing", "Partitioning", "Covering", "Cardinality", "InvKnapsack", "Knapsack", "IntKnapsack", - "BinPacking", "FlowBin", "FlowMixed", "MixedBin", + "BinPacking", "HubImplication", "FlowBin", "FlowMixed", "MixedBin", "GenInt", "Singleton", "Other" }; return (t >= 0 && t < CoinRowTypeCount) ? names[t] : "Unknown"; @@ -122,6 +123,11 @@ inline CoinRowType classifyRow( return allInt ? CoinRowIntKnapsack : CoinRowKnapsack; if (rhs >= 1.1 && nNeg == 1 && nz >= 2) return CoinRowBinPacking; + // Same "exactly one coefficient of the minority sign" shape as + // BinPacking, but rhs < 1.1 (typically 0) -- "x1+x2+...+xk <= M*y" + // written as "x1+x2+...+xk - M*y <= 0" ("x1 OR x2 OR ... -> y"). + if (rhs < 1.1 && nNeg == 1 && nz >= 2) + return CoinRowHubImplication; } if (nNeg >= 2 && nPos >= 2 && sense == 'E') diff --git a/src/CoinShortestPath.cpp b/src/CoinShortestPath.cpp index d34d263e..47584cbe 100644 --- a/src/CoinShortestPath.cpp +++ b/src/CoinShortestPath.cpp @@ -31,8 +31,13 @@ CoinShortestPath::CoinShortestPath(size_t nodes, size_t arcs, const size_t *arcStart, const size_t *toNode, const double *dist) { nodes_ = nodes; nh_ = new CoinNodeHeap(nodes_); - previous_ = std::vector(nodes_); - dist_ = std::vector(nodes_); + // Start in the unvisited state so clearState() has nothing to undo before + // the first find(); previously each find() established this itself. + previous_ = std::vector(nodes_, SPATH_INFTY_NODE); + dist_ = std::vector(nodes_, SPATH_INFTY_DIST); + recording_ = true; + skipped_ = false; + skipsLeft_ = 0; path_ = std::vector(nodes_); neighs_ = std::vector > >(nodes_); @@ -54,17 +59,62 @@ CoinShortestPath::~CoinShortestPath() { delete nh_; } +/** + * Return dist_/previous_ to the unvisited state. + * + * Both arrays are only ever written for a node that entered the heap, so + * restoring exactly those nodes leaves the arrays *fully* valid rather than + * valid-where-stamped. That is deliberate: distance(), previous(node) and the + * previous() array accessor keep reading the arrays directly and need no + * change, and callers see the same values as before. + * + * The loop this replaces was the second half of the cost of a find() that + * terminates immediately: CoinOddWheelSeparator's active subgraphs are + * extremely sparse -- 6368 arcs over 36814 nodes on bab6, 36 arcs over 3832 on + * fiball -- so most origins have no outgoing arc, yet each call still paid two + * O(nodes_) sweeps plus the heap's own. + * + * Either branch leaves the arrays in the same state, so which one runs is + * invisible to every caller and to the heap order; only the cost differs. + */ +void CoinShortestPath::clearState() { + if (recording_) { + for (size_t i = 0; i < touched_.size(); i++) { + previous_[touched_[i]] = SPATH_INFTY_NODE; + dist_[touched_[i]] = SPATH_INFTY_DIST; + } + } else { + for (size_t i = 0; i < nodes_; i++) { + previous_[i] = SPATH_INFTY_NODE; + dist_[i] = SPATH_INFTY_DIST; + } + // Recording was on and the call outgrew the list: the subgraph is dense + // enough that the replay will not be used, so stop paying for it. + if (!skipped_) { + skipsLeft_ = recordBackoff(); + } + } + + touched_.clear(); + + if (skipsLeft_ > 0) { + skipsLeft_--; + recording_ = false; + skipped_ = true; + } else { + recording_ = true; + skipped_ = false; + } +} + void CoinShortestPath::find(const size_t origin) { assert(origin < this->nodes_); nh_->reset(); - - for (size_t i = 0; i < nodes_; i++) { - previous_[i] = SPATH_INFTY_NODE; - dist_[i] = SPATH_INFTY_DIST; - } + clearState(); dist_[origin] = 0.0; + touch(origin); nh_->update(origin, 0.0); size_t topNode; @@ -86,6 +136,7 @@ void CoinShortestPath::find(const size_t origin) { if (dist_[toNode] >= newDist + SPATH_EPS) { previous_[toNode] = topNode; dist_[toNode] = newDist; + touch(toNode); nh_->update(toNode, newDist); } // updating heap if necessary } // going through node neighbors @@ -96,13 +147,10 @@ void CoinShortestPath::find(const size_t origin, const size_t destination) { assert(origin < this->nodes_); assert(destination < this->nodes_); nh_->reset(); - - for (size_t i = 0; i < nodes_; i++) { - previous_[i] = SPATH_INFTY_NODE; - dist_[i] = SPATH_INFTY_DIST; - } + clearState(); dist_[origin] = 0.0; + touch(origin); nh_->update(origin, 0.0); size_t topNode; @@ -124,6 +172,7 @@ void CoinShortestPath::find(const size_t origin, const size_t destination) { if (dist_[toNode] >= newDist + SPATH_EPS) { previous_[toNode] = topNode; dist_[toNode] = newDist; + touch(toNode); nh_->update(toNode, newDist); } // updating heap if necessary } // going through node neighbors diff --git a/src/CoinShortestPath.hpp b/src/CoinShortestPath.hpp index 5aecedef..37a4d155 100644 --- a/src/CoinShortestPath.hpp +++ b/src/CoinShortestPath.hpp @@ -133,6 +133,77 @@ class COINUTILSLIB_EXPORT CoinShortestPath { * Monotone heap used in Dijkstra's algorithm. **/ CoinNodeHeap *nh_; + + /** + * Nodes whose dist_/previous_ entries were written by the last find(), so + * that the next one restores just those instead of all nodes_. See + * clearState() for why the arrays stay fully valid, which is what keeps + * previous(), previous(node) and distance(node) unchanged. + **/ + std::vector touched_; + + /** + * Whether the find() now in progress is recording into touched_. Cleared by + * touch() when the list grows past fullResetThreshold(), and by clearState() + * for a deliberately skipped call (see skipsLeft_). + **/ + bool recording_; + + /** + * Whether recording was *deliberately* off for the call that just finished, + * which is what separates "this graph is dense" from "we chose not to look". + * Only the former should extend the backoff. + **/ + bool skipped_; + + /** + * Calls remaining with recording deliberately off. + **/ + size_t skipsLeft_; + + /** + * Size of touched_ at which replaying it stops being cheaper than rewriting + * all nodes_ entries, so both touch() and clearState() give up on it. + * + * The full loop writes two words per node sequentially; a replay writes two + * words per entry by random access, which costs several times more each, so + * the crossover sits well below nodes_. A quarter is the conservative choice. + **/ + inline size_t fullResetThreshold() const { return nodes_ / 4; } + + /** + * How many calls to stop recording for once a call has proved the graph too + * dense for the replay to be used. + * + * On a dense active subgraph almost every node is reached, so recording pays + * fullResetThreshold() push_backs and clearState() then does the full loop + * anyway -- the one way this scheme can come out slower than the loop it + * replaces, and measured at roughly 15% on the densest odd-wheel fixtures. + * Backing off spreads that cost over 33 calls, leaving under 1%, while the + * periodic re-probe means a graph that turns sparse is picked straight back + * up. Correctness does not depend on the value: a full rewrite is always a + * valid reset, so this only ever trades one reset strategy for the other. + **/ + inline size_t recordBackoff() const { return 32; } + + /** + * Reset dist_/previous_ to their unvisited values. + **/ + void clearState(); + + /** + * Record that node had its dist_/previous_ entries written. + **/ + inline void touch(size_t node) { + if (!recording_) { + return; + } + if (touched_.size() >= fullResetThreshold()) { + recording_ = false; + return; + } + touched_.push_back(node); + } }; diff --git a/src/CoinStaticConflictGraph.cpp b/src/CoinStaticConflictGraph.cpp index 2fed83c0..f2ffdc2f 100644 --- a/src/CoinStaticConflictGraph.cpp +++ b/src/CoinStaticConflictGraph.cpp @@ -21,10 +21,12 @@ #include #include #include +#include #include "CoinStaticConflictGraph.hpp" #include "CoinDynamicConflictGraph.hpp" #include "CoinCliqueList.hpp" +#include "CoinFileIO.hpp" CoinStaticConflictGraph::CoinStaticConflictGraph ( const CoinConflictGraph *cgraph ) { @@ -43,10 +45,12 @@ CoinStaticConflictGraph::CoinStaticConflictGraph ( const double primalTolerance, const double infinity, const std::vector &colNames, - const std::vector &rowNames) + const std::vector &rowNames, + const double timeLimit) { - CoinDynamicConflictGraph *cgraph = new CoinDynamicConflictGraph(numCols, colType, colLB, colUB, matrixByRow, sense, rowRHS, rowRange, primalTolerance, infinity, colNames, rowNames); + CoinDynamicConflictGraph *cgraph = new CoinDynamicConflictGraph(numCols, colType, colLB, colUB, matrixByRow, sense, rowRHS, rowRange, primalTolerance, infinity, colNames, rowNames, timeLimit); + timeLimitReached_ = cgraph->timeLimitReached(); iniCoinConflictGraph(cgraph); nDirectConflicts_ = cgraph->nTotalDirectConflicts(); totalCliqueElements_ = cgraph->nTotalCliqueElements(); @@ -336,6 +340,346 @@ const std::vector< std::pair< size_t, std::pair< double, double > > > & CoinStat return newBounds_; } +/* ------------------------------------------------------------------------ * + * Serialization + * + * Layout, all little-endian, every size_t widened to uint64_t so a file + * written by one build is readable by another: + * + * char[8] magic "CGRAPH01" + * uint64 size_, nConflicts_, minDegree_, maxDegree_ + * double maxConflicts_, density_ + * uint8 updateMDegree, timeLimitReached_ + * uint64 nDirectConflicts_, totalCliqueElements_ + * uint64[] degree_ (size_ entries) + * uint64[] modifiedDegree_ (size_ entries) + * ragged conflicts_ (size_ rows: count then elements) + * ragged cliques_ (count of cliques, then per clique) + * uint64 |newBounds_|, then per entry: uint64 idx, double lb, double ub + * uint64 |infeasibleImplications_|, then per entry: + * uint64 variableIndex, string variableName, + * (string rowName, int64 rowIndex) x2 [zero, then one] + * where a string is uint64 length followed by that many bytes, no NUL. + * + * nodeCliques_ is deliberately absent: it is a reverse index over cliques_ + * and is rebuilt on load, which also keeps the two from disagreeing. + * ------------------------------------------------------------------------ */ + +static const char COINCG_MAGIC[8] = { 'C', 'G', 'R', 'A', 'P', 'H', '0', '1' }; + +namespace { + +/* Append helpers. Everything accumulates into one buffer and is written with a + * single call: the larger instances carry millions of direct conflicts, and an + * element-at-a-time write is dominated by per-call overhead. */ + +static inline void cgPutU64(std::vector< char > &buf, uint64_t v) +{ + for (int i = 0; i < 8; ++i) + buf.push_back(static_cast< char >((v >> (8 * i)) & 0xFF)); +} + +static inline void cgPutDouble(std::vector< char > &buf, double v) +{ + uint64_t bits; + memcpy(&bits, &v, sizeof(bits)); + cgPutU64(buf, bits); +} + +static inline void cgPutString(std::vector< char > &buf, const std::string &s) +{ + cgPutU64(buf, static_cast< uint64_t >(s.size())); + buf.insert(buf.end(), s.begin(), s.end()); +} + +static inline void cgPutVecU64(std::vector< char > &buf, const std::vector< size_t > &v) +{ + cgPutU64(buf, static_cast< uint64_t >(v.size())); + for (size_t i = 0; i < v.size(); ++i) + cgPutU64(buf, static_cast< uint64_t >(v[i])); +} + +/* Bounds-checked reader over the whole file image. Any read past the end sets + * the failure flag and returns zero, so a truncated or corrupt file fails at + * the end rather than allocating from a garbage length. */ +struct CgReader { + const char *p; + const char *end; + bool ok; + + CgReader(const char *data, size_t len) + : p(data) + , end(data + len) + , ok(true) + { + } + + uint64_t u64() + { + if (!ok || (size_t)(end - p) < 8) { + ok = false; + return 0; + } + uint64_t v = 0; + for (int i = 0; i < 8; ++i) + v |= (static_cast< uint64_t >(static_cast< unsigned char >(p[i])) << (8 * i)); + p += 8; + return v; + } + + double dbl() + { + uint64_t bits = u64(); + double v = 0.0; + memcpy(&v, &bits, sizeof(v)); + return v; + } + + std::string str() + { + uint64_t n = u64(); + if (!ok || (uint64_t)(end - p) < n) { + ok = false; + return std::string(); + } + std::string s(p, p + n); + p += n; + return s; + } + + /* Reads a length-prefixed run of uint64. Checks the length against the + * bytes actually remaining before reserving, so a corrupt count cannot + * turn into a huge allocation. */ + bool vecU64(std::vector< size_t > &out) + { + uint64_t n = u64(); + if (!ok || n > (uint64_t)(end - p) / 8) { + ok = false; + return false; + } + out.resize(n); + for (uint64_t i = 0; i < n; ++i) + out[i] = static_cast< size_t >(u64()); + return ok; + } +}; + +} // anonymous namespace + +int CoinStaticConflictGraph::save( const char *fileName ) const +{ + std::vector< char > buf; + /* Rough pre-size: header plus the two degree arrays plus every conflict and + * clique element, 8 bytes each. Avoids repeated growth on big graphs. */ + buf.reserve(128 + 8 * (2 * size_ + nDirectConflicts_ + totalCliqueElements_ + 2 * size_)); + + buf.insert(buf.end(), COINCG_MAGIC, COINCG_MAGIC + 8); + + cgPutU64(buf, static_cast< uint64_t >(size_)); + cgPutU64(buf, static_cast< uint64_t >(nConflicts_)); + cgPutU64(buf, static_cast< uint64_t >(minDegree_)); + cgPutU64(buf, static_cast< uint64_t >(maxDegree_)); + cgPutDouble(buf, maxConflicts_); + cgPutDouble(buf, density_); + buf.push_back(static_cast< char >(updateMDegree ? 1 : 0)); + buf.push_back(static_cast< char >(timeLimitReached_ ? 1 : 0)); + cgPutU64(buf, static_cast< uint64_t >(nDirectConflicts_)); + cgPutU64(buf, static_cast< uint64_t >(totalCliqueElements_)); + + /* Degrees are stored rather than recomputed: recomputeDegree() is an + * approximation (it overcounts clique membership on purpose), so + * recomputing on load would not reproduce the values BK actually saw. */ + for (size_t i = 0; i < size_; ++i) + cgPutU64(buf, static_cast< uint64_t >(degree_[i])); + for (size_t i = 0; i < size_; ++i) + cgPutU64(buf, static_cast< uint64_t >(modifiedDegree_[i])); + + for (size_t i = 0; i < size_; ++i) + cgPutVecU64(buf, conflicts_[i]); + + cgPutU64(buf, static_cast< uint64_t >(cliques_.size())); + for (size_t ic = 0; ic < cliques_.size(); ++ic) + cgPutVecU64(buf, cliques_[ic]); + + cgPutU64(buf, static_cast< uint64_t >(newBounds_.size())); + for (size_t i = 0; i < newBounds_.size(); ++i) { + cgPutU64(buf, static_cast< uint64_t >(newBounds_[i].first)); + cgPutDouble(buf, newBounds_[i].second.first); + cgPutDouble(buf, newBounds_[i].second.second); + } + + cgPutU64(buf, static_cast< uint64_t >(infeasibleImplications_.size())); + for (size_t i = 0; i < infeasibleImplications_.size(); ++i) { + const BinaryBoundInfeasibility &bi = infeasibleImplications_[i]; + cgPutU64(buf, static_cast< uint64_t >(bi.variableIndex)); + cgPutString(buf, bi.variableName); + cgPutString(buf, bi.fixedToZero.rowName); + cgPutU64(buf, static_cast< uint64_t >(static_cast< int64_t >(bi.fixedToZero.rowIndex))); + cgPutString(buf, bi.fixedToOne.rowName); + cgPutU64(buf, static_cast< uint64_t >(static_cast< int64_t >(bi.fixedToOne.rowIndex))); + } + + /* gzip when available -- these files reach hundreds of megabytes on the + * denser instances and compress well, being mostly sorted indices. The + * reader sniffs the first bytes, so either form loads. */ + CoinFileOutput *out = NULL; + if (CoinFileOutput::compressionSupported(CoinFileOutput::COMPRESS_GZIP)) + out = CoinFileOutput::create(fileName, CoinFileOutput::COMPRESS_GZIP); + else + out = CoinFileOutput::create(fileName, CoinFileOutput::COMPRESS_NONE); + if (!out) + return 1; + + int rc = 0; + /* CoinFileOutput::write takes an int count, so hand it the buffer in + * chunks -- a single graph can exceed 2 GB uncompressed. */ + const size_t chunk = 64u * 1024u * 1024u; + size_t done = 0; + while (done < buf.size()) { + const int n = static_cast< int >(std::min(chunk, buf.size() - done)); + if (out->write(buf.data() + done, n) != n) { + rc = 1; + break; + } + done += n; + } + delete out; + + return rc; +} + +CoinStaticConflictGraph *CoinStaticConflictGraph::load( const char *fileName ) +{ + /* Slurp the whole file: CoinFileInput has no size query and no seek, and + * the reader needs random-free but bounds-checked sequential access. */ + CoinFileInput *in = NULL; + try { + in = CoinFileInput::create(fileName); + } catch (...) { + return NULL; + } + if (!in) + return NULL; + + std::vector< char > buf; + { + const size_t chunk = 4u * 1024u * 1024u; + std::vector< char > tmp(chunk); + for (;;) { + const int n = in->read(tmp.data(), static_cast< int >(chunk)); + if (n <= 0) + break; + buf.insert(buf.end(), tmp.begin(), tmp.begin() + n); + } + } + delete in; + + if (buf.size() < 8 || memcmp(buf.data(), COINCG_MAGIC, 8) != 0) + return NULL; + + CgReader rd(buf.data() + 8, buf.size() - 8); + + CoinStaticConflictGraph *cg = new CoinStaticConflictGraph(); + + cg->size_ = static_cast< size_t >(rd.u64()); + cg->nConflicts_ = static_cast< size_t >(rd.u64()); + cg->minDegree_ = static_cast< size_t >(rd.u64()); + cg->maxDegree_ = static_cast< size_t >(rd.u64()); + cg->maxConflicts_ = rd.dbl(); + cg->density_ = rd.dbl(); + if (!rd.ok || (size_t)(rd.end - rd.p) < 2) { + delete cg; + return NULL; + } + cg->updateMDegree = (*rd.p++ != 0); + cg->timeLimitReached_ = (*rd.p++ != 0); + cg->nDirectConflicts_ = static_cast< size_t >(rd.u64()); + cg->totalCliqueElements_ = static_cast< size_t >(rd.u64()); + + /* size_ gates every array length below, so sanity-check it against the + * bytes on hand before allocating anything sized by it. */ + if (!rd.ok || cg->size_ > (size_t)(rd.end - rd.p) / 8) { + delete cg; + return NULL; + } + + const size_t n = cg->size_; + cg->degree_.resize(n); + for (size_t i = 0; i < n; ++i) + cg->degree_[i] = static_cast< size_t >(rd.u64()); + cg->modifiedDegree_.resize(n); + for (size_t i = 0; i < n; ++i) + cg->modifiedDegree_[i] = static_cast< size_t >(rd.u64()); + + cg->conflicts_.resize(n); + for (size_t i = 0; i < n && rd.ok; ++i) + rd.vecU64(cg->conflicts_[i]); + + const uint64_t nCliques = rd.u64(); + if (!rd.ok || nCliques > (uint64_t)(rd.end - rd.p) / 8) { + delete cg; + return NULL; + } + cg->cliques_.resize(nCliques); + for (uint64_t ic = 0; ic < nCliques && rd.ok; ++ic) { + rd.vecU64(cg->cliques_[ic]); + /* Deserializing must not be a way to build a graph that violates the + * invariant its own conflicting() depends on. Files written before + * CoinCliqueList::addClique started sorting hold unsorted cliques, and + * leaving them that way would make a replayed graph answer conflicting() + * differently from a freshly built one. */ + std::sort(cg->cliques_[ic].begin(), cg->cliques_[ic].end()); + } + + const uint64_t nBounds = rd.u64(); + if (!rd.ok || nBounds > (uint64_t)(rd.end - rd.p) / 24) { + delete cg; + return NULL; + } + cg->newBounds_.reserve(nBounds); + for (uint64_t i = 0; i < nBounds && rd.ok; ++i) { + const size_t idx = static_cast< size_t >(rd.u64()); + const double lb = rd.dbl(); + const double ub = rd.dbl(); + cg->newBounds_.push_back(std::make_pair(idx, std::make_pair(lb, ub))); + } + + const uint64_t nImpl = rd.u64(); + for (uint64_t i = 0; i < nImpl && rd.ok; ++i) { + BinaryBoundInfeasibility bi; + bi.variableIndex = static_cast< size_t >(rd.u64()); + bi.variableName = rd.str(); + bi.fixedToZero.rowName = rd.str(); + bi.fixedToZero.rowIndex = static_cast< int >(static_cast< int64_t >(rd.u64())); + bi.fixedToOne.rowName = rd.str(); + bi.fixedToOne.rowIndex = static_cast< int >(static_cast< int64_t >(rd.u64())); + if (!rd.ok) + break; + cg->infeasibleImplications_.push_back(bi); + } + + if (!rd.ok) { + delete cg; + return NULL; + } + + /* Rebuild the reverse index exactly as every other constructor does, so a + * loaded graph and a built one agree node for node. */ + cg->nodeCliques_.assign(n, std::vector< size_t >()); + for (size_t ic = 0; ic < cg->cliques_.size(); ++ic) { + const std::vector< size_t > &clq = cg->cliques_[ic]; + for (size_t j = 0; j < clq.size(); ++j) { + if (clq[j] >= n) { // clique referring to a node outside the graph + delete cg; + return NULL; + } + cg->nodeCliques_[clq[j]].push_back(ic); + } + } + + return cg; +} + void CoinStaticConflictGraph::iniCoinStaticConflictGraph(const CoinConflictGraph *cgraph) { iniCoinConflictGraph(cgraph); nDirectConflicts_ = cgraph->nTotalDirectConflicts(); diff --git a/src/CoinStaticConflictGraph.hpp b/src/CoinStaticConflictGraph.hpp index d7ffbb56..c059fdef 100644 --- a/src/CoinStaticConflictGraph.hpp +++ b/src/CoinStaticConflictGraph.hpp @@ -62,6 +62,11 @@ class COINUTILSLIB_EXPORT CoinStaticConflictGraph : public CoinConflictGraph * when discounting continuous columns. * @param colNames column names, only used for diagnostic messages when * reporting newly inferred bounds. + * @param timeLimit absolute wall-clock deadline (CoinWallclockTime() basis) + * for the graph-building scan, or -1.0 (default) to + * disable it. Forwarded as-is to the internal + * `CoinDynamicConflictGraph`; see its constructor and + * `timeLimitReached()` for details. **/ CoinStaticConflictGraph( const int numCols, @@ -75,9 +80,18 @@ class COINUTILSLIB_EXPORT CoinStaticConflictGraph : public CoinConflictGraph const double primalTolerance, const double infinity, const std::vector &colNames, - const std::vector &rowNames + const std::vector &rowNames, + const double timeLimit = -1.0 ); + /** + * Returns true if the graph-building scan was aborted early because the + * `timeLimit` passed to the constructor was reached (see + * `CoinDynamicConflictGraph::timeLimitReached()`). The resulting graph is + * still sound, just possibly incomplete. + **/ + bool timeLimitReached() const { return timeLimitReached_; } + /** * Clone a conflict graph. **/ @@ -174,7 +188,7 @@ class COINUTILSLIB_EXPORT CoinStaticConflictGraph : public CoinConflictGraph return true; } - // Check clique conflicts + // Check clique conflicts (also sorted → binary search; CoinCliqueList::addClique) const auto &nc1 = nodeCliques_[n1]; const auto &nc2 = nodeCliques_[n2]; size_t nodeToSearch; @@ -204,7 +218,49 @@ class COINUTILSLIB_EXPORT CoinStaticConflictGraph : public CoinConflictGraph **/ const std::vector< std::pair< size_t, std::pair< double, double > > > &updatedBounds() const; + /** + * Write this graph to a file, so that `load()` can reconstruct it exactly. + * + * Building a conflict graph means re-scanning every constraint, and the + * result depends on the matrix as it stood at that moment -- a graph built + * from a *later* state of the same model is not necessarily the same graph + * (`CglCliqueStrengthening` deletes and adds rows after the graph is built, + * and `OsiSolverInterface::checkCGraph()` only rebuilds when the column + * count changes, so a solver's graph is routinely stale with respect to its + * own matrix -- deliberately, since that is the graph its callers agreed + * on). Rebuilding is therefore *not* a way to recover a specific graph; + * saving it is. This exists so that a stand-alone tool can replay clique + * separation or clique extension on precisely the graph some earlier solve + * used, without redoing that solve. + * + * The format is binary, self-describing (magic + version) and fixed-width + * (every `size_t` is stored as `uint64_t`), so a file stays readable across + * builds and across 32/64-bit boundaries. `nodeCliques_` is not stored: it + * is an index derivable from `cliques_` and is rebuilt on load. + * + * @param fileName destination path, overwritten if it exists + * @return 0 on success, non-zero on any I/O failure + **/ + int save( const char *fileName ) const; + + /** + * Reconstruct a graph written by `save()`. + * + * @param fileName path previously passed to `save()` + * @return a newly allocated graph the caller owns, or NULL if the file + * cannot be read, is not a conflict-graph file, or was written by + * an incompatible version + **/ + static CoinStaticConflictGraph *load( const char *fileName ); + private: + /** + * Empty graph, for `load()` to fill in field by field. Private because a + * default-constructed graph is not a usable one: every base-class scalar is + * left indeterminate until read from the file. + **/ + CoinStaticConflictGraph() { } + /** * Recommended tighter bounds for some variables. **/ @@ -272,6 +328,12 @@ class COINUTILSLIB_EXPORT CoinStaticConflictGraph : public CoinConflictGraph * Elements of the cliques stored explicitly. **/ std::vector > cliques_; + + /** + * Set to true if the graph-building scan was aborted early due to + * reaching the `timeLimit` passed to the constructor. + **/ + bool timeLimitReached_ = false; }; #endif // STATICCONFLICTGRAPH_H diff --git a/src/CoinTime.hpp b/src/CoinTime.hpp index df0f3fb5..3f1285f4 100644 --- a/src/CoinTime.hpp +++ b/src/CoinTime.hpp @@ -88,13 +88,38 @@ inline double CoinGetTimeOfDay() #endif // _MSC_VER -/// Returns elapsed wall-clock time in seconds since the first call. -/// Uses a static local variable (C++11) so the start time is captured -/// exactly once, thread-safely, without requiring C++17 inline variables. +/// Returns absolute wall-clock time in seconds since the Unix epoch -- +/// exactly equivalent to CoinGetTimeOfDay(), just under the name callers +/// throughout COIN-OR expect for wall-clock (as opposed to CPU) time. +/// +/// This intentionally does NOT return "time since first call" any more (an +/// earlier version used a function-local static to capture an implicit +/// per-process baseline on first invocation). That scheme was fragile in +/// two ways: (1) the baseline depended on whichever code path happened to +/// call this function first, which could be well after true process start +/// if the first caller was buried deep in some phase; and (2) in a +/// long-lived process that solves multiple independent models (e.g. a +/// library embedding -- Cbc's C interface, Python-MIP, a service that keeps +/// a solver object alive across requests), the baseline was fixed forever +/// at the very first call in the process's lifetime, so it could not +/// distinguish "time since this solve started" from "time since some much +/// earlier, unrelated solve started". +/// +/// Since every correct use of this function computes a DIFFERENCE between +/// two calls (elapsed = CoinWallclockTime() - startTime, where startTime +/// was itself captured via an earlier CoinWallclockTime() call scoped to +/// the current solve/phase) or an absolute deadline (deadline = value + +/// CoinWallclockTime(), checked later via CoinWallclockTime() >= deadline), +/// switching the baseline to the Unix epoch is a no-op for all such +/// differences/deadlines -- and removes the hidden global state and the +/// (magic-static, thread-safe-init) branch on every call, so this is also +/// a little cheaper. Callers must still take care never to print or +/// otherwise use a single raw call as if it were itself a small "elapsed +/// since solve start" duration -- always diff against an explicit, +/// solve/phase-scoped start timestamp instead. inline double CoinWallclockTime() { - static const double wallclockStart = CoinGetTimeOfDay(); - return CoinGetTimeOfDay() - wallclockStart; + return CoinGetTimeOfDay(); } //############################################################################# diff --git a/src/Makefile.am b/src/Makefile.am index 7e15f950..686c51be 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -18,6 +18,7 @@ lib_LTLIBRARIES = libCoinUtils.la libCoinUtils_la_SOURCES = \ CoinAlloc.cpp \ CoinBuild.cpp \ + CoinBuildInfo.cpp \ CoinDenseVector.cpp \ CoinError.cpp \ CoinFactorization1.cpp \ @@ -108,6 +109,7 @@ includecoindir = $(pkgincludedir) includecoin_HEADERS = \ CoinAlloc.hpp \ CoinBuild.hpp \ + CoinBuildInfo.hpp \ CoinDenseVector.hpp \ CoinDistance.hpp \ CoinError.hpp \ @@ -119,6 +121,7 @@ includecoin_HEADERS = \ CoinHelperFunctions.hpp \ CoinIndexedVector.hpp \ CoinLpIO.hpp \ + CoinMemUsage.hpp \ CoinMessage.hpp \ CoinMessageHandler.hpp \ CoinModel.hpp \ diff --git a/src/Makefile.in b/src/Makefile.in index 7af65b4c..4916a3d9 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -145,10 +145,11 @@ libCoinUtils_la_DEPENDENCIES = $(am__DEPENDENCIES_1) @BUILD_OSLFACTORIZATION_TRUE@ CoinOslFactorization3.lo \ @BUILD_OSLFACTORIZATION_TRUE@ CoinSimpFactorization.lo am_libCoinUtils_la_OBJECTS = CoinAlloc.lo CoinBuild.lo \ - CoinDenseVector.lo CoinError.lo CoinFactorization1.lo \ - CoinFactorization2.lo CoinFactorization3.lo \ - CoinFactorization4.lo CoinDenseFactorization.lo CoinFileIO.lo \ - CoinFinite.lo CoinIndexedVector.lo CoinLpIO.lo CoinMessage.lo \ + CoinBuildInfo.lo CoinDenseVector.lo CoinError.lo \ + CoinFactorization1.lo CoinFactorization2.lo \ + CoinFactorization3.lo CoinFactorization4.lo \ + CoinDenseFactorization.lo CoinFileIO.lo CoinFinite.lo \ + CoinIndexedVector.lo CoinLpIO.lo CoinMessage.lo \ CoinMessageHandler.lo CoinModel.lo CoinStructuredModel.lo \ CoinModelUseful.lo CoinModelUseful2.lo CoinMpsIO.lo \ CoinPackedMatrix.lo CoinPackedVector.lo \ @@ -196,6 +197,7 @@ am__maybe_remake_depfiles = depfiles am__depfiles_remade = ./$(DEPDIR)/CoinAdjacencyVector.Plo \ ./$(DEPDIR)/CoinAlloc.Plo ./$(DEPDIR)/CoinBoundPropagation.Plo \ ./$(DEPDIR)/CoinBronKerbosch.Plo ./$(DEPDIR)/CoinBuild.Plo \ + ./$(DEPDIR)/CoinBuildInfo.Plo \ ./$(DEPDIR)/CoinCliqueExtender.Plo \ ./$(DEPDIR)/CoinCliqueList.Plo ./$(DEPDIR)/CoinCliqueSet.Plo \ ./$(DEPDIR)/CoinConflictGraph.Plo ./$(DEPDIR)/CoinCutPool.Plo \ @@ -277,13 +279,13 @@ am__can_run_installinfo = \ *) (install-info --version) >/dev/null 2>&1;; \ esac am__includecoin_HEADERS_DIST = CoinAlloc.hpp CoinBuild.hpp \ - CoinDenseVector.hpp CoinDistance.hpp CoinError.hpp \ - CoinFactorization.hpp CoinDenseFactorization.hpp \ + CoinBuildInfo.hpp CoinDenseVector.hpp CoinDistance.hpp \ + CoinError.hpp CoinFactorization.hpp CoinDenseFactorization.hpp \ CoinFileIO.hpp CoinFinite.hpp CoinFloatEqual.hpp \ CoinHelperFunctions.hpp CoinIndexedVector.hpp CoinLpIO.hpp \ - CoinMessage.hpp CoinMessageHandler.hpp CoinModel.hpp \ - CoinStructuredModel.hpp CoinModelUseful.hpp CoinMpsIO.hpp \ - CoinPackedMatrix.hpp CoinPackedVector.hpp \ + CoinMemUsage.hpp CoinMessage.hpp CoinMessageHandler.hpp \ + CoinModel.hpp CoinStructuredModel.hpp CoinModelUseful.hpp \ + CoinMpsIO.hpp CoinPackedMatrix.hpp CoinPackedVector.hpp \ CoinPackedVectorBase.hpp CoinParam.hpp CoinPragma.hpp \ CoinPresolveDoubleton.hpp CoinPresolveDual.hpp \ CoinPresolveDupcol.hpp CoinPresolveEmpty.hpp \ @@ -503,31 +505,31 @@ lib_LTLIBRARIES = libCoinUtils.la # List all source files for this library libCoinUtils_la_SOURCES = CoinAlloc.cpp CoinBuild.cpp \ - CoinDenseVector.cpp CoinError.cpp CoinFactorization1.cpp \ - CoinFactorization2.cpp CoinFactorization3.cpp \ - CoinFactorization4.cpp CoinDenseFactorization.cpp \ - CoinFileIO.cpp CoinFinite.cpp CoinIndexedVector.cpp \ - CoinLpIO.cpp CoinMessage.cpp CoinMessageHandler.cpp \ - CoinModel.cpp CoinStructuredModel.cpp CoinModelUseful.cpp \ - CoinModelUseful2.cpp CoinMpsIO.cpp CoinPackedMatrix.cpp \ - CoinPackedVector.cpp CoinPackedVectorBase.cpp CoinParam.cpp \ - CoinParamUtils.cpp CoinPostsolveMatrix.cpp \ - CoinPrePostsolveMatrix.cpp CoinPresolveDoubleton.cpp \ - CoinPresolveDual.cpp CoinPresolveDupcol.cpp \ - CoinPresolveEmpty.cpp CoinPresolveFixed.cpp \ - CoinPresolveForcing.cpp CoinPresolveHelperFunctions.cpp \ - CoinPresolveImpliedFree.cpp CoinPresolveIsolated.cpp \ - CoinPresolveMatrix.cpp CoinPresolvePsdebug.cpp \ - CoinPresolveMonitor.cpp CoinPresolveSingleton.cpp \ - CoinPresolveSubst.cpp CoinPresolveTighten.cpp \ - CoinPresolveTripleton.cpp CoinPresolveUseless.cpp \ - CoinPresolveZeros.cpp CoinRational.cpp CoinSearchTree.cpp \ - CoinShallowPackedVector.cpp CoinSnapshot.cpp \ - CoinWarmStartBasis.cpp CoinWarmStartVector.cpp \ - CoinWarmStartDual.cpp CoinWarmStartPrimalDual.cpp \ - CoinAdjacencyVector.cpp CoinBronKerbosch.cpp \ - CoinCliqueExtender.cpp CoinCliqueList.cpp CoinCliqueSet.cpp \ - CoinConflictGraph.cpp CoinCutPool.cpp \ + CoinBuildInfo.cpp CoinDenseVector.cpp CoinError.cpp \ + CoinFactorization1.cpp CoinFactorization2.cpp \ + CoinFactorization3.cpp CoinFactorization4.cpp \ + CoinDenseFactorization.cpp CoinFileIO.cpp CoinFinite.cpp \ + CoinIndexedVector.cpp CoinLpIO.cpp CoinMessage.cpp \ + CoinMessageHandler.cpp CoinModel.cpp CoinStructuredModel.cpp \ + CoinModelUseful.cpp CoinModelUseful2.cpp CoinMpsIO.cpp \ + CoinPackedMatrix.cpp CoinPackedVector.cpp \ + CoinPackedVectorBase.cpp CoinParam.cpp CoinParamUtils.cpp \ + CoinPostsolveMatrix.cpp CoinPrePostsolveMatrix.cpp \ + CoinPresolveDoubleton.cpp CoinPresolveDual.cpp \ + CoinPresolveDupcol.cpp CoinPresolveEmpty.cpp \ + CoinPresolveFixed.cpp CoinPresolveForcing.cpp \ + CoinPresolveHelperFunctions.cpp CoinPresolveImpliedFree.cpp \ + CoinPresolveIsolated.cpp CoinPresolveMatrix.cpp \ + CoinPresolvePsdebug.cpp CoinPresolveMonitor.cpp \ + CoinPresolveSingleton.cpp CoinPresolveSubst.cpp \ + CoinPresolveTighten.cpp CoinPresolveTripleton.cpp \ + CoinPresolveUseless.cpp CoinPresolveZeros.cpp CoinRational.cpp \ + CoinSearchTree.cpp CoinShallowPackedVector.cpp \ + CoinSnapshot.cpp CoinWarmStartBasis.cpp \ + CoinWarmStartVector.cpp CoinWarmStartDual.cpp \ + CoinWarmStartPrimalDual.cpp CoinAdjacencyVector.cpp \ + CoinBronKerbosch.cpp CoinCliqueExtender.cpp CoinCliqueList.cpp \ + CoinCliqueSet.cpp CoinConflictGraph.cpp CoinCutPool.cpp \ CoinDynamicConflictGraph.cpp CoinKnapsackRow.cpp \ CoinBoundPropagation.cpp CoinTerm.cpp CoinNodeHeap.cpp \ CoinOddWheelSeparator.cpp CoinShortestPath.cpp \ @@ -549,15 +551,16 @@ AM_CPPFLAGS = $(COINUTILSLIB_CFLAGS) # Here list all the header files that are required by a user of the library, # and that therefore should be installed in pkgincludedir. includecoindir = $(pkgincludedir) -includecoin_HEADERS = CoinAlloc.hpp CoinBuild.hpp CoinDenseVector.hpp \ - CoinDistance.hpp CoinError.hpp CoinFactorization.hpp \ - CoinDenseFactorization.hpp CoinFileIO.hpp CoinFinite.hpp \ - CoinFloatEqual.hpp CoinHelperFunctions.hpp \ - CoinIndexedVector.hpp CoinLpIO.hpp CoinMessage.hpp \ - CoinMessageHandler.hpp CoinModel.hpp CoinStructuredModel.hpp \ - CoinModelUseful.hpp CoinMpsIO.hpp CoinPackedMatrix.hpp \ - CoinPackedVector.hpp CoinPackedVectorBase.hpp CoinParam.hpp \ - CoinPragma.hpp CoinPresolveDoubleton.hpp CoinPresolveDual.hpp \ +includecoin_HEADERS = CoinAlloc.hpp CoinBuild.hpp CoinBuildInfo.hpp \ + CoinDenseVector.hpp CoinDistance.hpp CoinError.hpp \ + CoinFactorization.hpp CoinDenseFactorization.hpp \ + CoinFileIO.hpp CoinFinite.hpp CoinFloatEqual.hpp \ + CoinHelperFunctions.hpp CoinIndexedVector.hpp CoinLpIO.hpp \ + CoinMemUsage.hpp CoinMessage.hpp CoinMessageHandler.hpp \ + CoinModel.hpp CoinStructuredModel.hpp CoinModelUseful.hpp \ + CoinMpsIO.hpp CoinPackedMatrix.hpp CoinPackedVector.hpp \ + CoinPackedVectorBase.hpp CoinParam.hpp CoinPragma.hpp \ + CoinPresolveDoubleton.hpp CoinPresolveDual.hpp \ CoinPresolveDupcol.hpp CoinPresolveEmpty.hpp \ CoinPresolveFixed.hpp CoinPresolveForcing.hpp \ CoinPresolveImpliedFree.hpp CoinPresolveIsolated.hpp \ @@ -684,6 +687,7 @@ distclean-compile: @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinBoundPropagation.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinBronKerbosch.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinBuild.Plo@am__quote@ # am--include-marker +@AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinBuildInfo.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinCliqueExtender.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinCliqueList.Plo@am__quote@ # am--include-marker @AMDEP_TRUE@@am__include@ @am__quote@./$(DEPDIR)/CoinCliqueSet.Plo@am__quote@ # am--include-marker @@ -909,6 +913,7 @@ distclean: distclean-am -rm -f ./$(DEPDIR)/CoinBoundPropagation.Plo -rm -f ./$(DEPDIR)/CoinBronKerbosch.Plo -rm -f ./$(DEPDIR)/CoinBuild.Plo + -rm -f ./$(DEPDIR)/CoinBuildInfo.Plo -rm -f ./$(DEPDIR)/CoinCliqueExtender.Plo -rm -f ./$(DEPDIR)/CoinCliqueList.Plo -rm -f ./$(DEPDIR)/CoinCliqueSet.Plo @@ -1027,6 +1032,7 @@ maintainer-clean: maintainer-clean-am -rm -f ./$(DEPDIR)/CoinBoundPropagation.Plo -rm -f ./$(DEPDIR)/CoinBronKerbosch.Plo -rm -f ./$(DEPDIR)/CoinBuild.Plo + -rm -f ./$(DEPDIR)/CoinBuildInfo.Plo -rm -f ./$(DEPDIR)/CoinCliqueExtender.Plo -rm -f ./$(DEPDIR)/CoinCliqueList.Plo -rm -f ./$(DEPDIR)/CoinCliqueSet.Plo