All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
-
the primal master problem hosts the Block, when its coordinates are all ColVariable of the Block itself and the Block has Constraint other than the box [see
MasterProblemBlock::set_hosted_Block()]: the Solver of the master then has these Constraint as they are, of any type it can read (e.g., aFRowConstraintwith aDQuadFunction) and possibly on Variable of the Block that are not coordinates, the feasible set being the projection of the one of the Block, and their Modification reach it directly. A point the master has not written is checked on eachRowConstraintof the Block, with the other Variable at the value of the last master; the dual master keeps the linear rows.linear_constraints_testalso adds a ball and a Variable that is no coordinate, for the BundleSolver with the primal master -
linear
Constraintin the Block: aFRowConstraintwith aLinearFunctionof theColVariablethat the components have, which the master problem has as rows (the primal one) or as multipliers in the aggregate (the dual one, also with easy components). What is minimized is the function plus the indicator of the set they define, i.e., a point out of the set has value + INF while the linearizations of the components there are kept, so that a starting point out of it is left at the first serious step; the points of the master problem are in it by construction, and the others are checked within 1e-9 relative. The first master is solved with the empty components given their fictitious bound, rather than skipped, and an empty master is the set being empty, or under the trust region being out of reach, which enlarges the region as the vertical linearizations do. Any Modification of them (sides, coefficients, a row added, removed, relaxed or enforced) has them read again once the Variable have changed, and given anew to the master problem, the values at the stability centre and at the trial point being then computed again, since the indicator may have changed there -
a lower bound that needs no test on z*: at each master problem of the proximal method or of the trust region, the model at the trial point minus the Hölder term sum_i | s_i | max | Lambda_i - y*_i | over the bounds of the variables, s being the subgradient of the model plus the indicator of the box that the master gives (
box_lower_bound()). It is finite when every variable with s_i != 0 has finite bounds, the largest one is kept as the global bound, andIsOptimal()stops when the value at the centre is within the accuracy of it, whateverdblNZEpsis -
intMLModelandintMLHiddenofBundleSolverML: the recurrent core of the network (0 = none, the feed-forward network, 1 = RNN, 2 = GRU, 3 = LSTM) and its hidden size, so that the cores already inNetcan be chosen from the configuration; changing either rebuilds the network, seeded withintMLSeedif that is >= 0, so that the initial weights do not depend on the order in which the parameters are set. The state of the core is cleared at the beginning of eachcompute(), andBackward()replays the recorded iterations of a recurrent core in their order, with the gradient truncated at each window [seeintMLWindow] -
intMLLossandintMLFeaturesofBundleSolverML, whose defaults 0 keep the loss and the features it had. WithintMLLoss= 1 each prediction of t is paired with the step it has produced, and the loss is the mean of c log t, c being the cosine of the angle between the new subgradient and the step, which decreases t where the step went too far and increases it where it did not go far enough, whatever the sign of the function. WithintMLFeatures= 1 the features are dimensionless (the values of the function relative to its value at the center, the step-size through the decrease the model predicts, the norms through their ratios), and the network gives the fraction tau of the current estimate of the gap that the decrease predicted by the model has to be, t = tau ( Sigma* + D_t*( z* ) ) / || z* ||^2, rather than t itself, so that one network applies to instances of different size and scale; weights trained with the other features do not apply -
the heuristic of t 5, selected by the bit 10 of
inttSPar1both after a serious and after a null step: t is multiplied by exp( - beta c ), c being the cosine of the angle between the new subgradient and the step from the stability center to the trial point, and beta the newdbltSPar4(0.3 by default), so that t increases when the step was too short and decreases when it was too long -
the C++20 module
smspp.bundlesolver(src/BundleSolver.cppm), which exports every name that the headers declare at namespace scope and re-exportssmspp.milpsolver, so that a source file can writeimport smspp.bundlesolver;in place of the#includeof the headers; it is built with the CMake optionSMSpp_BUILD_MODULE, as the targetSMS++::BundleSolver_module. The headers ofBundleSolverML, which needs Torch, are not in it
-
the cutoff that is the global lower bound of the master is no longer called a conditional bound (
f_cutoff_LB, formerlyf_cond_LB), the conditional bound being the one ofBlock::get_valid_lower_bound(); the bound that z* = 0 gives is that of the last iteration because the upper bound of a Lagrangian function is the value of the primal solution of that iteration, as the comment now says -
the bits of
intDoEasyare renumbered with no gap, bit 1 being free since its use was dropped: +1 the easy components, +2 (formerly +8) their reduced costs, +4 together with +2 (formerly +12) also the dual values of their rows, +8 (formerly +16) no size Variable, +16 (formerly +32) the sized copies; a configuration written for the old bits has to be translated (e.g., 13 is now 7, 45 is now 23) -
the prediction of
BundleSolverMLno longer builds an autograd graph
-
with
intIntVars1 theParallelBundleSolverevaluates the components on its threads: the method with integer Variable evaluated them in a loop of its own, which no derived class redefines, and now does it byInnerLoop()with the new argumentall, which evaluates every hard component collecting the linearizations with no stop on a serious or null step; withalltheParallelBundleSolverconsumes the evaluations in the order of the sequential version, hence it takes the same iterations.integer_testhas several polyhedral components, and theParallelBundleSolveras a second Solver ofBSPar-int.txt -
the tests are registered with ctest also in a fresh build of the umbrella, where
include(CTest)was commented out and the tests were built but never run -
a master Solver that fails on the master itself, rather than on the bundle, made
compute()run untilintMaxIter: the recovery deleted an item, the master was solved without it, and the new trial point gave the item back. This happens, e.g., with an active-set QP Solver that stops at its iteration limit where it cannot tell that the master is empty. Nowcompute()stops withkErroronce the master has failed in more iterations since the last serious step than the items the bundle can hold -
a true global lower bound [see
set_valid_lower_bound()] within the accuracy of the value at the centre now stopscompute()withkOK, andget_lb()/get_ub()give the largest of it and of the bound found by the algorithm, while the latter used to hide the former as soon as it was finite, however worse -
a serious step accepted a point worse than the stability centre when some hard component had no item in the bundle: the targets were then infinite, which is right when the value at the centre is not known yet, but the recovery from failures of the master problem can empty the bundle in the middle of a call. On a two-stage stochastic unit commitment the Lagrangian Dual moved its centre from 3.81e9 to -3.78e12 and stopped there; the upper target is now the value at the centre whenever this is known, so that only an improving step is serious
-
the recovery from a numerical failure of the master problem declared it unrecoverable as soon as the bundle had only items of the current iteration (the new linearizations and the aggregated ones, not removable until the next one), as at the first master of a call or right after a serious step: these are now removed as well, the model without them being still a model of the function, and only an empty bundle, whose master is the stability centre itself, is left. On
pHydro_18of plan4res the Lagrangian Dual ended inkErrorat the 149-th iteration, its master failing on GUROBI's dual simplex right after a serious step -
the time limit (
dblMaxTime) did not hold while the master problem was failing: the recovery, which removes an item from the bundle and solves the master again until it works or the bundle is empty, did not look at the time, and gave the master Solver the time left even when this was negative, which GUROBI refuses, keeping its previous limit. On an AC unit commitment over 24 hours the Lagrangian Dual stopped at 600 seconds went on for 200 more in the recovery and ended inkError; it now stops withkStopTimeas soon as the time is up, and the time left that the master Solver and the components are given is never below zero -
NeedsG1()did not ask for the new subgradient when the heuristic of t 4 is used after a serious step only, as it compared the bits 6 and 7 ofinttSPar1with 172 rather than with 192: the heuristic then read a G1 that had not been computed -
the scalar product < g , d* > of the new subgradient and the direction that the heuristics of t 2 and 3 use (
inttSPar1) and thatBundleSolverMLtakes as a feature, which was z* d* plus the product of each new subgradient with the stability center rather than with d*
0.6.0 - 2026-10-09
-
the master problem can be stabilized by a level instead of a proximal term (
intMPStbl1, pure level) or by both (intMPStbl2, doubly stabilized): the level is the value at the stability centre minus an expected decrease Delta, which is ( 1 -dblLStabM) times the gap to a reliable lower bound when there is one, and is shortened bydblLStabMafter the null steps thatintMnNSCallows; with no reliable lower bound Delta is seeded by a proximal probe of the master (or isdblLStabDlttimes the value at the centre), in pure level it is enlarged bydblLStabIncrafter the serious steps thatintMnSSCallows, provided that the last one has decreased the function by at least Delta /dblLStabIncrand that the model error at the step, relative to the model value, is at mostdblLStabSmall, and it is enlarged bydblLStabIncrsquared when small serious and null steps alternate; an empty level set is a lower bound, and in the doubly stabilized mode each serious step multiplies t by one plus the multiplier of the level row; with the primal master in the displacement form, a gap closed by empty levels whose aggregate certificate is not yet within the tolerance restarts the target from the scale-based Delta at most once per centre -
the non-easy components can be aggregated before the master problem sees them (
dblCmpAggr): with 0 every component is a component of the master problem, with 1 all of them are summed into one, and with x in between there are round( 1 / x ) groups of about the same size, each aC05SumFunctionthat is computed by computing all its members and whose linearizations are the sums of theirs;intCmpAggrRulesays how the components are assigned to the groups (at random with the seedintCmpAggrSeed, by scale, by similar or by spread supports), so that the same configuration gives the same groups, and fewer components give a smaller master problem and a coarser model; each group draws the combinations of linearizations it hands out withintCmpAggrSeedtold apart by its position, and takes the threads it spends on its members from the pool of the parallel solver driving it [seeC05SumFunction::set_member_runner()], evaluating one of them in the thread that waits for the others -
the 0-th component of the sum-function may be a separable quadratic one, b . x + ( 1 / 2 ) sum_j rho_j x_j^2 written as a
DQuadFunction(what a regularised risk has in front of the sum), besides a linear one: the master problem carries it in its stabilization [seeMasterProblemBlock::set_zeroth_quadratic()], the gradient of the component being b_j + rho_j x_j, and it need not span all the Variable; a linear one, also after a quadratic one in the master that is recycled across Block [seeCreateMPB()], leaves the master with no quadratic term -
bit 4 (+16) of
intDoEasy: the dual master problem does not scale the easy components by a size Variable, i.e., the mass lambda of the lower model is fixed to 1 and the global lower bound has no effect there, which gives a master that may be cheaper to solve; without it every easy component whose inner Block owns or takes a size Variable is scaled [seeMasterProblemBlock::use_easy_size_variables()]. It is read when the Block is set, as which components are easy -
bit 5 (+32) of
intDoEasy: an easy component whose inner Block neither owns nor takes a size Variable is scaled through a sized copy of its inner Block that the master has in its place [seeMasterProblemBlock::use_easy_mirrors()]; off by default, so that such a component is not scaled and lambda stays fixed to 1, and of no effect with bit 4 -
BundleSolverMLlistens tointMaxThread: at the beginning of eachcompute()the number of threads of Torch is set to max( 1 , intMaxThread ), hence to one with the default 0, so that the predictions of the network, and with them the whole trajectory, are the same from a run to the next; with more threads the order of the floating-point sums may change them -
the trust region (intMPStbl == 4) without integer Variable, i.e., the stabilized cutting-plane method with the box || x - x_bar ||_inf <= t, whose master is linear: D*_t( z* ) is t || z* ||_1, the heuristic t is not used and t only changes by the significant increases and decreases, a serious step counting only if d* reaches the side of the box. As long as no point of the domain is known each master starts from tMinor and the box is enlarged only as much as the vertical linearizations ask, which keeps the point close to the centre; the master solved once without it tells an empty domain from a far one, and vertical linearizations are never deleted; with every component easy the master, which is then the problem itself, is solved without the box, so that a side of the box is not taken for an optimum
-
BundleSolverlistens todblUpCutOffanddblLwCutOffofSolver: in the concave case it stops as soon as the best value found is at leastdblUpCutOff, or as soon as the bound of the model certifies that the optimal value is at mostdblLwCutOff, and vice versa in the convex case, returningkCutOff; with the default (infinite) cutoffs nothing changes. With the dual master, the cutoff that a point reaches (dblUpCutOffin the concave case,dblLwCutOffin the convex one) is also the global lower bound of the master, when it is above the true one, as whoever set it does not care where the optimum is beyond it: in a Branch-and-Bound, the incumbent stabilizes the dual of the node. The cutoff is not a bound: what the master proves is recorded as a global bound, and reported byget_lb()/get_ub(), only if it is above the cutoff, the level target is never put beyond it, and an optimality test passed with the value at the stability centre within the accuracy of the cutoff ends withkCutOffrather thankOK; a master that fails with the bound is solved again without it, which then stays out untilcompute()returns -
a component that gains an "active" Variable the master already has, e.g., a
LagBFunctiongiven the dual pair of a multiplier that other components also have, keeps the master right: a hard component is reset, so that its cuts are reloaded from its global pool through the new local-to-global map, which gives them their coefficient on that coordinate, and an easy one gets its terms in the coupling row of the coordinate [seeMasterProblemBlock::add_easy_coupling()], with the local index the Variable has at the end of the batch; if the coordinate is nonzero in the stability centre, the value of the component there is declared unknown, as for a removal -
a component that gains an "active" Variable no longer needs the master problem to be built anew: for each cut in the bundle,
process_outstanding_Modification()asks the component that owns it, with the local indices of that component, for its coefficients on the new coordinates alone, which strong quasi-additivity makes valid, and gives them toMasterProblemBlock::add_vars()indexed by bundle slot. A coordinate is born at zero in the stability centre as in the point of every cut, hence nothing of the algorithm is reset. TheBlockModADof a dynamic Variable or Constraint added to the Block is dropped, since the change of the components reaches the Solver as aFunctionModVars -
intIntVars: with 1 the integer Variable of the C05Function are kept integer, i.e., what is minimized is the function over the integer points of its domain, by the stabilized cutting-plane method of van Ackooij, Frangioni and de Oliveira (Comput. Optim. Appl. 65, 2016): a proximal (mixed-integer quadratic) or trust-region (mixed-integer linear) master, and the master without stabilization, whose bound is a global lower bound, when the stabilized one sees nothing better than the stability centre even after its region has been enlarged by degrees: with the proximal term t is multiplied bydblmxIncrup todbltMaior, with the trust region the region grows by the fractiondblIntRadof the whole one, every Variable with a finite box moving by that fraction of its width and the others by t, until after ceil( 1 /dblIntRad) enlargements it is the whole one, as in the stabilized Benders' method of Baena, Castro and Frangioni (Manag. Sci. 66, 2020); with the trust region the binary Variable, the integer ones with box [ 0 , 1 ], get in place of it the local branching constraint [seeMasterProblemBlock::set_local_branching()], whose radius is the same fraction of their number, and a region in which the centre is optimal is excluded with the reverse constraint whenever the trust region has not restricted the other Variable while exploring it;integer_testhas instances with all, half and a third of the Variable binary. The method is run by the main loop ofcompute()itself, which callsinteger_direction(),integer_trial_point()andinteger_step()where the continuous method callsFormD(),continuous_trial_point()andcontinuous_step(); with the level (intMPStbl1) it is the level method of the same paper, whose levels follow its rule (19) withdblLStabMand which solves the cutting-plane master each time a level set is empty, whose bound is exact (the hybrid variant of the paper) [seeinteger_level_direction()]; the default 0 keeps the continuous relaxation that was always minimized. The testinteger_testcompares it with the same problem written out as one mixed-integer program -
vstrNoEasy, the classname() of the components that must never be treated as easy, in addition to those whose index is invintNoEasy: the class of a component is that of the first nested Block of itsC05Function(the inner Block of aLagBFunction), or that of theC05Functionitself if it is a Block with no nested Block, so that a configuration names the hard components of any instance without knowing their position; which components are easy is a concept of this Solver -
the tests of this directory and of the OSBDO examples carry the label of the module, so that the pipeline, which selects with
ctest -L <module>, runs them: they were built and never run -
test/osbdo, the four examples of OSBDO (multicommodity flow, supply chain, intersection of convex sets and federated learning) solved byBundleSolverin the same form, the agents asBendersBFunctionorLogisticFunctionand the coupling as an easyLagBFunction, each on an instance out of a verbatim copy of the generator of OSBDO and checked against a reference value, the whole problem as one LP or QP where there is one
-
a change of the linear part of a component no longer throws away the rows the master problem holds for it: a
C05FunctionModLinmoves every linearization by the same delta and leaves the constant of each of them where it is, hence the deltas that reach the solver between two solves of the master are summed, one dense vector per component, and handed toMasterProblemBlock::shift_cuts()in one call, together with the referencef_k( x_bar ), which moves bydelta . x_bar; a component that is being reset anyway, an easy one, or a delta that speaks of aVariablethis solver does not know, all fall back to asking the component again. The values of the sequence of re-optimizations of the facility location do not move in any of the four forms of the master problem, primal or dual by iterate or displacement -
the noise reduction has a global memory: it sets t to dblmxIncr times the largest among t and the values it has set since the last serious step, and it stops the solver when that value is already dbltMaior, so that the decreases of t in a sequence of null steps can no longer undo it and let it happen forever; the two noise reductions, before the stopping test and after an evaluation that gives neither a serious nor a null step, are one method
-
the makefile asks for
-O3 -DNDEBUGand nothing else, the macro of the patch forboost::anyon macOS having no reason to be there since there is noboost::anyleft in the core -
whoever links the module keeps it: the classes of a module register themselves in the factory from a static initialiser, and a linker that drops what looks unused takes the registration away with it, so the target now tells whoever links it to keep the symbol that forces the module in, and on ELF, where naming the symbol is not enough, the library as a whole
-
the master of the tests asks Gurobi for its least numerical care and not for none of it, and declares the residual zero on the scale of the model: the extra care is paid at every one of the thousands of solves of a run
-
the tests of the ML variant link
SMS++::BundleSolverML, which is whereBundleSolverMLnow lives -
the configuration that is installed no longer looks for NDOSolver/FiOracle, which the library does not link any more
-
the three places where the parallel loop names a failing component write the log at the same verbosity, and the log says which status the component that stopped the loop returned, a stop with no reason having left whoever read it to guess among the components
-
ParallelBundleSolverkeeps its threads alive and wakes up when an evaluation ends, rather than starting and joining a thread per component at every iteration, and never lets a thread wait on one that has already finished -
the master problem is a
MasterProblemBlock, i.e., a Block of the model solved by whichever Solver is attached to it, in place of theMPSolverhierarchy of the previous versions: what the bundle asks of it is said through the abstract representation and the Modification, so that the master is built, solved and changed as any other Block, and the parameters that only the old hierarchy understood are refused where they no longer mean anything. With the MPSolver hierarchy go the NDOSolver/FiOracle submodule and the Osi and Clp requirements: a BundleSolver needs the core library and MILPSolver, and the Solver of the master is the one the BlockSolverConfig thatstrMPBSolverCfgpoints at attaches -
the Modification that
MasterProblemBlockissues in a loop travel in one channel: the shift of the constant of every cut at a move of the reference [seeset_reference()], the refresh of the box, the linear part of the 0-th component and the quadratic term of every z at a change of t. A Solver able to write a whole set of coefficients, or of sides, in one operation then does that instead of one call per cut, per variable or per coupling row -
the fifth loop of
process_outstanding_Modification()asks whether any of the Variable a Modification speaks of is still "active" in the component it comes from, that component being reloaded from its global pool whatever has changed. It used to actualise the names against the first component, "which is fairly taken as a representative since all the C05Function have the same active Variable", into a range that nothing read: an assumption that no longer holds when the Lambda is sparse, in sixty lines whose only effect was to decide whether the component is reloaded at all -
BundleSolverML is a library of its own, SMS++::BundleSolverML: Torch is some hundreds of megabytes of shared objects, and a program linking BundleSolver paid the loading of every one of them at each start, 0.2 s per process on our machines, whether or not the ML variant was ever used. Whoever wants that variant links the new library, which brings BundleSolver along with it
-
the global lower bound of the dual master, which is in the frame of the stability centre, is translated again whenever the centre moves, rather than only when the bound changes: once the multiplier of its row is free in the Solver of the master [see
MasterProblemBlock::set_global_LB()], a translation made with a centre where the function was lower cuts off the optimum -
with an empty bundle and t at
dbltMinorthe master is solved again with the previous t before a verdict of the easy components is trusted: the master of a Bundle with easy components and t atdbltMinoris badly scaled, and a master Solver may declare it empty or unbounded when it is not (the first master of a Lagrangian dual of a unit commitment instance, whose objective was only 1e-10 times the squared norm of z, was declared unbounded by Gurobi with FeasibilityTol 1e-8, and the dual reported unbounded) -
has_dual_solution()answers false when a Modification of a component has taken away from its global pool, before the nextcompute(), a linearization of the bundle with a nonzero multiplier, since the dual solution is made with it, and when no linearization of a component has a nonzero multiplier, as a master problem that failed leaves them, since there is then no combination to give -
the global lower bound that a null aggregate subgradient certifies is
UpFiLmb + min( v^* , 0 ): with an inexact oracle (or by rounding) the linearization errors may be negative andv^*positive, and thenUpFiLmb + v^*is above the function at the current point; a Lagrangian Dual over inexact components reported it as the upper bound of the dual, below the lower one. The stop that the null subgradient certifies is the same as before -
FormD()takes an empty or unbounded master problem that the easy components could explain (an empty region of theirs, or a direction of negative cost the coupling term does not see) as their answer only at the first master solved after they changed, or without stabilization: since neither depends on the stability centre or on the bundle, with a proximal, trust-region or level master any later one is a numerical error in disguise, and so is one that follows a failure of the master Solver in the same call. With easy components, a master that Gurobi called unbounded after failing on numerical difficulties made the Lagrangian dual of pHydro_4 report the problem infeasible, and an empty easy region was taken for a numerical error; with the level, whose pure master cannot be unbounded, an empty easy region that the relaxation of the level cannot fix is reported as infeasible rather than as an error or a low precision -
IsOptimal()no longer certifies a point with an empty bundle when t was at tMinor already, so that Prevt had not been set -
a BundleSolver detached from a Block and attached to another one, or to the same one again, gives the linear part of the 0-th component to its new master problem: the flag telling that the master already had it survived the change of Block, so that the new master was left without it, which gave wrong directions and a wrong bound; a
LagrangianDualSolvermoved to another Block, which carries its inner Solver along, ended its nextcompute()for an inexact oracle -
intDoEasykeeps all its bits: it was stored in abool, so that the dual values and reduced costs of the easy components (bits 4 and 8) were never kept andget_var_solution()always refused to give them; also, setting it after the Block, as a BlockSolverConfig does, now decides whether those of the next solves of the master are kept -
the aggregate subgradient
G1has one entry per Variable rather than one per component: it was sized with the number of components, so that with the sparse Lambda the sum of the subgradients wrote past its end and corrupted the heap whenever it is needed, which withBundleSolverML(whoseNeedsG1()is always true) is always; with the dense Lambda it was only truncated, and its norm, a feature of the network, was wrong -
a Variable removed from a component and given back to it in the same batch stays in the master: its global index was still queued for removal although its reference count was positive again, so that the local-to-global map pointed to another Variable ("duplicate global Variable in a component", or a corrupted heap)
-
an easy component that loses a coordinate added in the same batch, not yet in the master, no longer asks the master to drop its terms from a coupling row that does not exist; and the local-to-global maps of the easy components are given to the master also when a component has only gained a coordinate it already had
-
a stability centre already beyond the conditional lower bound, as left by a previous call that stopped there as unbounded, is reported unbounded rather than optimal when nothing has changed in between: the test of the conditional bound came after that of optimality, which with exact easy components fires at the first iteration
-
removing coordinates while the algorithm runs works, the master shrinking through
MasterProblemBlock::remove_vars(): theBlockModADof a removal is dropped as that of an addition, the components reporting the change themselves; in the dense path the removed coordinates leaveLamVcblr, which kept pointers to destroyed Variable, and when one of them is nonzero at the stability center the values there are declared unknown, the function after the removal being the one before with that coordinate at zero, so that the next finite evaluation becomes the center (the bundle stopped at a wrong value); in the sparse path the linear part losing a Variable is handled throughLambda2Idx, and a Variable added and removed in the same batch is no longer passed to the master, which never had it -
a
FunctionModVarscoming from a Function that is not a component, the linear part being one, no longer reads the arrays that hold one entry per component with the indexInf< Index >()thatget_index_of_component()returns for it: it happened inprocess_outstanding_Modification(), both where the values of the components are refreshed and where the local-to-global map of a sparse Lambda is extended, and either one was a segmentation fault. The linear part is marked out of date instead, so that the master receives its coefficients on the new coordinates -
BundleSolver::get_var_solution()readIsEasy, which is empty when no component is easy, up to the number of components, hence out of its range whenever the Configuration of the solution (e.g., aGetInnerVarSolConfigof theLagrangianDualSolver) asked for the easy components of a problem without any; it now finds nothing to give, and an index given explicitly is rejected as that of a component that is not easy -
on macOS a program linking the module lost the classes the module registers in the factories when the linker dropped the library, as it does under
-dead_strip_dylibs, which conda sets: the target now asks the linker for the symbol that forces the module in (-u), which ld64, unlike the ELF linker, counts as a use of the library -
a problem whose components are all easy is solved instead of being refused: the master problem then carries the exact model of each of them, hence it is the problem itself once the proximal term is out of the way, and one solve of it gives the answer.
tis therefore taken to its maximum, not to its minimum as it is while the bundle of a hard component is empty: with a finitetthe master is the stabilized problem and not the problem, and on one of the PyPSA networks it gave a dual value of 3.1e9 against the 2.4e11 of the reference. The rule that collapses t while the bundle is empty, the error raised when no component is found to evaluate and the norm of the aggregate, which is divided by a mass that is then zero, all apply to the components that are hard and are skipped when there is none -
the combination of linearizations given back to a component when its bundle is full is divided by the mass its diagonal rows carry, lambda minus the share of its individual lower bound, rather than by 1 - r, so that it is a convex combination also with a level row or an individual lower bound; with all the mass on the lower bounds the item is replaced without any aggregation
-
a full bundle frees an item in base that is a vertical row only when fewer than two diagonal ones are in base, the aggregate being made of the diagonal rows alone and the multiplier of the vertical one being otherwise lost
-
is_subgradient_global()asks the master by the hard component and the global name of the item, as the other helpers do, rather than by the position in the pool, which with easy components made every row vertical -
the duals of an easy component, and the reduced costs of its columns, are the ones of the last solve of the master: the sub-Block of an easy component is a Block of the model, which any Solver may write into between that solve and the question, so what it held when asked was not what the master had left there, as was already the case for its primal. They are saved only when
intDoEasysays that they will be asked for -
a master that the Solver could not solve, and whose bundle held no item to remove, was the end of the run: it is now solved once more with t as it was before the empty bundle brought it down to its minimum, where the quadratic term vanishes against the data of the easy components and leaves the master a badly scaled problem
-
the part of an easy component in the solution of a LagrangianDualSolver was whatever its inner Block held when the solution was asked for: the Solver of the master writes there after each solve, but the inner Block is a Block of the model, which any other Solver may write into after it, so that the solution could mix the multipliers of the master for the hard components with the solution of another Solver for the easy ones. The master now saves what it writes there, and
get_dual_solution()writes it back [seeMasterProblemBlock::restore_easy_primal()] -
a variable change of one component that is not wrapped in a
GroupModification(a "naked"FunctionModVars) threw when there were more components, and the throw only hid what went wrong behind it. It is now processed per component, and three things it needs have been put right: (i) with the sparse representation of the Variable the linearizations reloaded into the master were read as dense, which put their coefficients on the wrong global Variable; (ii) a component that stops depending on some of its Variable, while the others still do, has its linearizations reloaded from its global pool (a reset of that component only, the others being no longer reset), and its value in the stability centre is marked unknown if one of those Variable is not 0 there; (iii) an easy component has its Lagrangian terms dropped from the coupling rows of the Variable it no longer has, through the newMasterProblemBlock::drop_easy_coupling(), and the master is given the new local-to-global maps of the easy components -
the component index passed to
MasterProblemBlock: it keeps a bundle only for the "hard" components and indexes them by their position among those, while BundleSolver counts the components of Fi globally, the "easy" ones included. With no easy component the two numbers coincide, so the difference showed up only underintDoEasy, where it addressed the wrongPolyhedralFunctionBlockor ran past the last one; the translation is now made explicit byhard_k()and, for the easy components, byeasy_k() -
a Modification changing the linearizations of a component without changing its Variable only asked the Solver of the master to read the subgradients again, but the master holds copies of them and so gave back the stale ones; the component is now reset, which reloads its linearizations from its global pool as they are
-
when a component had used up its share of the bundle with constraints (vertical linearizations, which are never removed), a new one was still put in a free spot elsewhere in the bundle, beyond the global pool of the component, and the C05Function then refused to store it with "invalid linearization name"; this happens when a Lagrangian subproblem is unbounded over and over, and now the solve ends with kError and says that the bundle of that component is full of constraints
-
a component answering kLowPrecision stopped the whole solve with an error: that code sorts after kError among the return codes, and the two places that read the status of a component only asked whether it was at least kError. A component saying kLowPrecision has found a solution and says it could not prove it optimal, which is inexact information and not a failure, so it is now let through and used as such
0.5.0 - 2026-09-12
- BundleSolverML, a BundleSolver variant whose step-size t is predicted by a neural network (Torch) trainable online across solves, with shared weights among multiple instances and TorchScript model save / load; it is only built if Torch is available.
- the version of the module is the git tag of its repository, or the VERSION.txt of a release tarball, and the shared library carries it: its SONAME is major.minor while the major is 0, and it is installed with an RPATH relative to itself, so that an installed tree keeps working wherever it is moved
-
the documentation of the int parameters, which did not say that intMaxThread is ignored, the implementation being sequential
-
BPar5 and RstAlgPrm were never initialized in the BundleSolver constructor, leading to nondeterministic behavior (up to complete stalls of the algorithm) whenever dblBPar5 / intRstAlg were not explicitly set by the ComputeConfig.
-
the package configuration file finds the libraries the module links, so that a project using the installed module needs nothing more than find_package()
0.4.5 - 2025-12-12
-
check for when all components are easy
-
temporary solution to management of putting an inconsistent BundleSolverState (see comments to intFrcLstSS)
-
useful option to get_var_solution() for duals/reduced costs of all easy components
-
support for reading primal and dual solutions out of easy components
-
CHECK_BAD_F (warnings to show possible oracle issues)
-
algo information getters
-
Heuristic() are protected and virtual (so that we can develop new ML-based ones)
-
support to un_any_thing_count_*
-
adapted to new standard organization of makefiles
-
avoided const vectors prone to static initialization fiasco
-
deactivated useless warnings
-
huge logical flaw whereby a problem that was empty due to easy components making it so was not properly recognised as being so
-
allow to terminate in exactly one iteration
0.4.4 - 2024-02-27
-
makefiles and Cmake files updated to new global SMS++ scheme
-
documentation updated accordingly
-
default OSISolver is Gurobi
-
improved BundleSolverState by keeping all upper and lower values for each component
-
fixed sloppy setting of OSI parameters
-
fixed
has_*_solution()andhas_*_feasible()
0.4.3 - 2023-05-23
-
handling the constant term Modification for hard components
-
handling constant term for "easy" components
-
support for OsiGrbSolverInterface
- better printing in case of errors in a Fi
-
missing check of max number of iterations in one case
-
issues in put_State()
-
issue in one case of process_outstanding_Modification (adding Linearization was not properly managed)
-
management of numerical errors in the MPSolver
-
typos and spaces
0.4.2 - 2022-06-28
- added new parameter intTrgtMng controlling different aspects of how models are used to pass targets and accuracy to the C05Function and to forecast the value (using the Lipschitz_constant)
-
generate the abstract representation in set_Block()
-
minor improvements in logging and comments
-
quite significant rehaul of t heuristics: reshuffled the bits in tSPar1 (basically keeping compatibility with develop branch) and added the "reversal form of the poorman's quasi-Newton update". This requires the computation of the norm of the "aggregated representative subgradient", doing which called for some nontrivial changes in the computation of Alfa1 and ScPr1
-
ParallelBundleSolver now properly counts the computed components; previously those of the ramp-down phase were not, which may lead to getting an error if the last step was not a SS and intFrcLstSS was true
-
fixed get_dual_solution() when there is no easy component
0.4.1 - 2021-12-07
-
improved Modification handling (no over-reacting to easy ones, important bugfix)
-
better log, printing times for each component in verbosity 4 and higher
-
fixed several flaws
-
improved namespace handling and similar stuff
0.4.0 - 2021-05-02
Several major improvements:
-
restructured stopping conditions, with a new one on the norm (INF, 1 or 2) of zStar, possibly relative to subgradient ; tStar anyway remains the driver of t-strategies.
-
BundleSolver now detects when "zStar == 0" (see previous point) and uses this to algorithmically produce globally valid lower bounds.
-
BundleSolver now checks if the objectives are (all) convex or concave and adapts accordingly (by sneakily converting the concave problem internally into a convex one and adapting the results on the fly when they go out).
-
Box constraints are now handled, either by means of NNConstraint/BoxConstraint or as NN constraints "inherent" in the ColVariable.
-
Changes in the costs or RHS/bounds in easy components now are allowed and managed.
-
Changes in the 0-th component now allowed.
-
Added hooks for perspective handling of changes in easy components.
-
Complete rehaul of Modification in "easy" components.
-
Better bad termination reporting.
-
Allowing to switch away Cplex.
-
Added ComputeConfig for hard and easy and NoEasy.
-
Added individual Configurations.
- Many fixes throughout the code, too many to list.
0.3.0 - 2020-09-16
- Support for better modification framework.
- Minor fixes.
0.2.0 - 2020-03-06
- Minor fixes.
0.1.2 - 2020-03-02
- Minor fixes.
0.1.1 - 2020-02-17
- Minor bug in CMake configuration file.
0.1.0 - 2020-02-10
- First test release.