From 81bdcb6e9e5bf03154fafb1ab4fc1f793c9e3b20 Mon Sep 17 00:00:00 2001 From: Pedro Giffuni Date: Sat, 29 Aug 2026 12:32:33 -0500 Subject: [PATCH 1/2] solver: Initial/current Calc solution Use the Coin methods to store the initial values based on the spreadsheet. These can be a useful optimization for Calc, especially for repeated Solver runs where the user changes one input and runs Solver again. Developed with help from AI (ChatGPT and Code Copilot) --- main/sccomp/source/solver/solver.cxx | 29 +++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/main/sccomp/source/solver/solver.cxx b/main/sccomp/source/solver/solver.cxx index a607b9e751..53334aee71 100644 --- a/main/sccomp/source/solver/solver.cxx +++ b/main/sccomp/source/solver/solver.cxx @@ -332,14 +332,16 @@ void SAL_CALL SolverComponent::solve() aCellsHash[aCellAddr].reserve( nVariables + 1 ); // constraints: right hand side } - // set all variables to zero - //! store old values? - //! use old values as initial values? - std::vector::const_iterator aVarIter; - for ( aVarIter = aVariableCells.begin(); aVarIter != aVariableCells.end(); ++aVarIter ) + // Save the current values of the changing cells for use as a possible + // initial solution, then reset the cells to zero for model construction. + std::vector aInitValues(nVariables); + for (nVar = 0; nVar < nVariables; ++nVar) { - lcl_SetValue( mxDoc, *aVarIter, 0.0 ); + // read current value of the variable cell and store as initial value + aInitValues[nVar] = lcl_GetValue( mxDoc, aVariableCells[nVar] ); + lcl_SetValue( mxDoc, aVariableCells[nVar], 0.0 ); } + std::vector::const_iterator aVarIter; // read initial values from all dependent cells ScSolverCellHashMap::iterator aCellsIter; @@ -546,6 +548,19 @@ void SAL_CALL SolverComponent::solve() NULL, NULL, NULL ); nResult = CoinLoadInteger( hProb, pColType ); + // Supply the current variable values as a possible initial solution. + // Pass the spreadsheet values unchanged; CoinMP/CBC is responsible for + // validating the supplied initial solution. + if ( nResult == SOLV_CALL_SUCCESS && !aInitValues.empty() ) + { + int nLoadRc = CoinLoadInitValues( hProb, aInitValues.data() ); + if ( nLoadRc != SOLV_CALL_SUCCESS ) + { + // Non-fatal: initial values are an optimization hint only; proceed without them. + OSL_TRACE("CoinLoadInitValues failed: %d\n", nLoadRc); + } + } + delete[] pColType; delete[] pMatrixIndex; delete[] pMatrix; @@ -569,7 +584,7 @@ void SAL_CALL SolverComponent::solve() { // report invalid model - maStatus = lcl_GetResourceString( RID_ERROR_INVALIDMODEL ); + maStatus = lcl_GetResourceString( RID_ERROR_INVALIDMODEL ); CoinUnloadProblem(hProb); return; } From abe1c6b1e2d18abf1a44b96905f4668a2da4c6eb Mon Sep 17 00:00:00 2001 From: Pedro Giffuni Date: Sun, 30 Aug 2026 01:11:51 -0500 Subject: [PATCH 2/2] solver: make use of range constraints CoinMP supports the MPS-style range constraints: L row: lower <= expression <= upper G row: lower <= expression <= upper R row: lower <= expression <= upper This makes possible through some trickery to reduce the number of equations, which is useful for very big systems with many constraints. The feature was assisted with AI (Code Copilot and ChatGPT). --- main/sccomp/source/solver/solver.cxx | 134 ++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/main/sccomp/source/solver/solver.cxx b/main/sccomp/source/solver/solver.cxx index 53334aee71..f7fee96ec5 100644 --- a/main/sccomp/source/solver/solver.cxx +++ b/main/sccomp/source/solver/solver.cxx @@ -40,6 +40,8 @@ #include #include #include +#include +#include #include @@ -470,6 +472,117 @@ void SAL_CALL SolverComponent::solve() } } + // Try to combine complementary <= and >= rows with identical coefficients + // into a single ranged row 'R' with RANGE = upper - lower. + // We do this before building the column-wise matrix. When a pair is + // merged we zero out the coefficients of the removed row so it is + // ignored when building the sparse column representation. + // Allocate a row-indexed range array up-front (one entry per original row). + double* pRangeValues = new double[nRows]; + for (size_t i = 0; i < nRows; ++i) pRangeValues[i] = 0.0; + + // Two-pass approach: first collect best lower/upper per coefficient + // signature (exact bitwise signature). Second, convert compatibles to + // ranged rows. This avoids online erase/replace semantics and ensures + // decisions are based on the original model. + struct RowPair { size_t lowerIdx; double lowerVal; size_t upperIdx; double upperVal; }; + const size_t npos = static_cast(-1); + std::unordered_map< std::string, RowPair > rowMap; + rowMap.reserve(nRows * 2); + + // Pass 1: populate rowMap with tightest lower (max G) and tightest + // upper (min L) for each coefficient signature. + for (size_t i = 0; i < nRows; ++i) + { + char ti = pRowType[i]; + if ( ti != 'L' && ti != 'G' ) + continue; + + const char* data = reinterpret_cast(&pCompMatrix[i * nVariables]); + size_t len = (size_t)nVariables * sizeof(double); + std::string sig; + sig.assign(data, len); + + auto it = rowMap.find(sig); + if ( it == rowMap.end() ) + { + RowPair rp; rp.lowerIdx = npos; rp.upperIdx = npos; rp.lowerVal = 0.0; rp.upperVal = 0.0; + std::pair::iterator, bool> res = rowMap.insert(std::make_pair(sig, rp)); + it = res.first; + } + + RowPair &rp = it->second; + if ( ti == 'L' ) + { + double v = pRHS[i]; + if ( rp.upperIdx == npos || v < rp.upperVal ) + { + rp.upperIdx = i; + rp.upperVal = v; + } + } + else // 'G' + { + double v = pRHS[i]; + if ( rp.lowerIdx == npos || v > rp.lowerVal ) + { + rp.lowerIdx = i; + rp.lowerVal = v; + } + } + } + + // Pass 2: perform conversions for entries that have both bounds. + size_t nMergedRows = 0; + for (auto &kv : rowMap) + { + RowPair &rp = kv.second; + if ( rp.lowerIdx == npos || rp.upperIdx == npos ) + continue; + + size_t idxLower = rp.lowerIdx; + size_t idxUpper = rp.upperIdx; + double lower = rp.lowerVal; + double upper = rp.upperVal; + + if ( lower <= upper ) + { + // make upper the ranged row and mark lower as removed + pRowType[idxUpper] = 'R'; + pRHS[idxUpper] = upper; + pRangeValues[idxUpper] = upper - lower; + + pRowType[idxLower] = 'N'; + pRHS[idxLower] = 0.0; + + ++nMergedRows; + OSL_TRACE("Solver: merging rows %lu (G %.17g) and %lu (L %.17g) into ranged row %lu [%.17g, %.17g]\n", + static_cast(idxLower), lower, + static_cast(idxUpper), upper, + static_cast(idxUpper), lower, upper); + } + else + { + // invalid (contradictory) bounds: leave rows unchanged + OSL_TRACE("Solver: contradictory bounds for coeff-signature - lowerRow=%lu (%.17g) upperRow=%lu (%.17g); leaving rows unchanged\n", + static_cast(idxLower), lower, + static_cast(idxUpper), upper); + } + } + + // After Pass 2 produce a summary trace (rows merged, ranged rows created, + // rows eliminated). This is the default diagnostic; per-row traces are + // emitted above and can be enabled/disabled by adjusting trace levels. + int nRangeCount_tmp = 0; + int nEliminated = 0; + for (size_t i = 0; i < nRows; ++i) + { + if ( pRowType[i] == 'R' ) ++nRangeCount_tmp; + if ( pRowType[i] == 'N' ) ++nEliminated; + } + OSL_TRACE("Solver: %lu input rows, %d ranged rows created, %d rows eliminated, %lu merged pairs\n", + static_cast(nRows), nRangeCount_tmp, nEliminated, static_cast(nMergedRows)); + // Find non-zero coefficients, column-wise int* pMatrixBegin = new int[nVariables+1]; @@ -482,6 +595,8 @@ void SAL_CALL SolverComponent::solve() int nBegin = nMatrixPos; for (size_t nRow=0; nRow