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