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.
0.6.0 - 2026-11-03
-
a change of the weights of a component [see
Block::set_weight()] after the Lagrangian Dual is formed (handles_weight_change(), false withiBCopy, whose copies do not follow the original): that of the Objective reaches theLagBFunctionof the component, and that of the Variable has the Lagrangian terms of the component written again from the relaxed Constraint with the new weight (reweight_components()), e.g., when aUCBlockreplicates one of its units while the Solver is attached -
the weight of the Objective of a
Block[seeBlock::set_objective_weight()]: withRecursivetheLagBFunctionof a component has the product of the weights of the Block relaxed above it, as an ancestor of the component multiplying its Objective and not the Lagrangian term, the constant of each relaxed Block enters with its weight, the copy of a component (iBCopy) has its weight, the dual valuesget_dual_solution()writes in a component are multiplied by its weight, which itsSolverdoes not see, andPrimalProximalHeurevaluates the objective of each sub-Block and of its nested Block with their weights, the value of each component solved alone in the consensus recovery included -
the weight of the Variable of a
Block[seeBlock::set_variable_weight()]: the coefficient of aVariableof a component in a relaxed Constraint enters its Lagrangian term multiplied by the product of the weights from itsBlockup to the closest one that also contains the Constraint, in the tree as it is before the decomposition (LagrangianDualSolver::variable_weight()), when the Lagrangian terms are built and when the relaxed Constraint change; the copy of a component (iBCopy) has the weight of the original -
the makefiles build the C++20 module
smspp.lagrangiandualsolverwhenSMSppMODis not empty (e.g.,make SMSppMOD=1), with clang++ 16 or later [seelib/makefile-modof SMS++] -
PrimalProximalHeur::add_solution(), which records the point the Variable of the Block currently hold as a solution, after checking that it satisfies the rows of the Block; withintRecoveryMIPStartthe best solution so far is given as a MIP start to theMILPSolverof the primal recovery, and the reason why a solver takes none is logged -
PrimalProximalHeurparameterintPenConstrBins, which applies the proximal penalty also to the binary Variable that only appear in the Constraint of a sub-Block (say, routing Variable whose cost is not in the objective), adding them to its Objective Function; a sub-Block that does not accept them (say,ThermalUnitBlock) throws, hence the default (0) keeps the penalty on the binary Variable of the Objective only -
LagrangianDualRelaxationSolverchooses the variable to branch upon by the components of the core (BranchingRule.hof SMS++): its candidates are described asBranchCandidate(the convexified solution as the point, the Lagrangian solution as the second point, the sub-Blocks as the blocks), strong branching isStrongBranchingon the children it solves, eLearned isLearnedBranching; the choices are the same as before -
LagrangianDualRelaxationSolvergives the estimates of the children of each branching (it is aBranchEstimator, seeBranchingRule.hof SMS++), for a best-estimate search such as that of BranchAndXSolver: withintBranchEstimate1 they are those of the pseudocosts of the variables (PseudoCosts), learned from the bound of each child solved right after its parent and from the children of strong branching, a variable with no observation taking the prior ofstrEstimateModel(LearnedPseudoCosts) or the mean of the others; with the default 0 it gives none, and nothing changes -
the C++20 module
smspp.lagrangiandualsolver(src/LagrangianDualSolver.cppm), which exports every name that the headers declare at namespace scope and re-exportssmspp.milpsolver, so that a source file can writeimport smspp.lagrangiandualsolver;in place of the#includeof the headers; it is built with the CMake optionSMSpp_BUILD_MODULE, as the targetSMS++::LagrangianDualSolver_module.LagrangianDualRelaxationSolverML, which needs Torch, is not in it
-
the README leaves the channels that distribute SMS++ to the installation guide of the wiki (Installing-SMS++), keeping the names of the packages of the module
-
the pipeline builds the module also with its makefiles, after CMake, and runs on a change of the makefiles too [see
ci/makefileof the umbrella] -
SMSpp_BUILD_MODULEno longer stops the configuration when the C++20 module cannot be built, i.e., with a CMake older than 3.28, a generator other than Ninja or Visual Studio, a compiler other than Clang 16 (withclang-scan-deps) or MSVC 19.34, or a module it imports that is not there: the option is turned off with a warning that says why, and the build goes on with the headers [seecmake/CheckModule.cmakeof SMS++] -
the Modification of the relaxed constraints reach the multiplier of a dynamic constraint in constant time, rather than by walking the list of the dynamic multipliers at each access, which made a batch of changes quadratic in the number of dynamic constraints
-
on Windows, the executables that copy their DLLs next to themselves are linked one at a time (a job pool of Ninja), since two copies of the same DLL into one directory collided (e.g., with the clang++ of MSYS2)
-
with the compilers of MinGW (MSYS2) the target asks whoever links it for the symbol that forces the module in, as with MSVC and on macOS: the linker imports a DLL only for the symbols asked for, so that a program that used none had the module left out and its classes missing from the factory
-
PrimalProximalHeurfinds its defaultRecoveryCfg.txtin an installed tree: the file is installed inshare/LagrangianDualSolver, and when it is not after the filename prefix of the Configuration it is looked for there, relative to the shared library, before the source tree of the build, whose path a moved or packaged library (the conda-forge one, say) does not have; the error says where it was looked for -
vintWhichPushCostgaveintPushCostToOwnerto the wrong components, every second one being skipped and the last ones read past the end, and an index given twice gave the opposite value to the components listed after it -
PrimalProximalHeursaves a Block with no Solution of its own as aColVariableSolution, and compares a recovered point with the bound of the Lagrangian Dual up to the accuracy that dual was solved with -
LagrangianDualSolver::get_dual_solution()solves a component again at the multipliers only ifcompute()has not stopped on a time or iteration limit, within the time it has left, and never again for a component whose Solver had no dual solution even after that (e.g., an integer MILP): a tool writing the solution with-Ore-solved every integer component with its old time limit, which doubled the time of a run stopped ondblMaxTime -
PrimalProximalHeur::get_Lagrangian_initial_status()is the status of the Lagrangian Dual also when there is no binary static Variable, whose only solve was not recorded: it stayed kUnEval, whichLagrangianDualRelaxationSolver::compute()then returned (e.g., on the unit commitments with no binary variable, such as the EC ones) -
PrimalProximalHeurcounts the constant term of theObjectiveof the Block it is attached to in the value of its solutions: it added up those of the sub-Blocks alone, while the bounds of the Lagrangian Dual include it, so that with a constant (e.g., the network constant terms a busUCBlockkeeps in its ownObjective) a solution was either reported at a value that is not its own or, being then below the Lagrangian bound, thrown away by the primal recovery
0.5.1 - 2026-10-10
- LagrangianDualRelaxationSolverML links on macOS: the index of the branching variable is read as int64_t, the type libtorch instantiates there.
0.5.0 - 2026-10-09
-
the unit test checks the conditional bound of the Lagrangian Dual when the Objective of the relaxed Block has a constant term, in a minimization and in a maximization problem, with bounded and unbounded components and after a change of the constant
-
str_LDSlv_ISCfg, the file of a ComputeConfig that is given to the inner Solver when it is created, and again when it is re-created by a change ofstr_LDSlv_ISName, or when the parameter changes; it is the way to set the parameters of the inner Solver that a derived class keeps for itself, asPrimalProximalHeurdoes withintMaxIteranddblRelAcc. Within a differential ComputeConfig of this Solver it is applied first, and the parameters forwarded to the inner Solver override it; a file that cannot be read or holds no ComputeConfig throws -
test/, the unit test of the module (LagrangianDualSolver_unit_test): an inner Solver defined there records the Lagrangian Dual it is given, which is checked against the class comments for every type of relaxed constraint, withint_LDSlv_NNMult0 and 1, in a minimization and in a maximization problem, through changes of the sides that keep or change the type, dynamic constraints added and removed, andintRecursive -
LagrangianDualRelaxationSolver, the RelaxationSolver that solves the Lagrangian Dual of each node of a Branch-and-Bound (as in BranchAndXSolver), with the primal recovery ofPrimalProximalHeur, and branches on a variable that is fractional in the convexified solution of the Lagrangian Dual (whichPrimalProximalHeurkeeps, seeget_Lagrangian_convexified_solution()), the most fractional one (intBranchStrategy0) or the best by strong branching among theintStrongCandsmost fractional ones (1), whose datastrStrongLogwrites in CSV for a branching rule to be learned from them, or the best among the same candidates for a model learned from those data (2,strBranchModel, a perceptron read from a text file), either with new dual pairs of the bounds of the variable (intApplyStrategy0) or by fixing it in its sub-Block (1), the columns of the global pool that the fixing purges coming back at the unfixing;vstrBranchGroupsrestricts the candidates to the variables of the named groups of static Variable of the sub-Blocks;LagrangianChangeis the Change it applies. Its bounds of the relaxation are, on the side of the relaxation, that of the Lagrangian Dual of the original objective, and, on the other, the best between the other bound of that Lagrangian Dual and the value of the best solution of the heuristic; its true bounds are those ofPrimalProximalHeur -
LagrangianDualRelaxationSolverML, in the libraryLagrangianDualSolverMLthat is only built where Torch is: withintBranchStrategy3 (eOnline) it branches by strong branching on the firstintLearnNodesnodes, keeping the features of the candidates and their scores in theGlobalInformationof the search, so that the workers of a parallel Branch-and-Bound share them, then fits on them the model ofintBranchStrategy2 (intHidden,intTrainEpochs,dblLearnRate,intLearnSeed), which all of them use from then on and which can be written tostrOnlineModelfor another run; the headers ofPrimalProximalHeurandLagrangianDualRelaxationSolverare installed -
get_var_solution()andget_dual_solution()take aSimpleConfiguration< std::map< std::string , Configuration * > >, the type of the "meta" Configurations, that names the sub-Block whose solution is written by class: only the sub-Block whose classname() is a key are dealt with, each with the Configuration its class is mapped to, and the keyrelaxedasksget_dual_solution()for the duals of the relaxed constraints too, as an invalid index does in the index-based forms; a configuration so names the components whose solution is needed for instances whose components are numbered differently -
the list of the sub-Block this Solver is told to skip, which
get_excluded_blocks()gives, is handed to the inner Solver as it is: any sub-Block excluded here is a sub-Block of the Lagrangian dual or of one of its descendants, so the inner Solver has to skip it as well -
intRecursive, with which the decomposition does not stop at the children of the Block: a child having the shape the Block must have, i.e., no Variable and no Objective of its own and sub-Block of its own, is decomposed in turn, its linking Constraint relaxed together with those of the Block and its own children taking its place as components. The descent goes on as deep as the shape holds, so that the components are the leaves of the decomposable part of the tree and the multipliers those of every level -
PrimalProximalHeurgives a feasible solution when its relaxed Constraint tie copies of a decision, x_a - x_b = 0, as the non-anticipativity ones of a two-stage problem do: the copies are fixed to their mean, rounded if integer, or to the value of one of them that is fixed already, and the components, independent then, are solved alone with the Solver ofstrRecoveryBSCby the calling thread andintMaxThreadmore
-
intMaxThreadofPrimalProximalHeurrefers to the heuristic, asintMaxIter,dblRelAccanddblMaxTimedo, and it is no longer passed to the inner Solver: it is the number of threads that the consensus recovery spawns besides the calling one, 0 (the default) solving the components one at a time -
the cutoffs
dblUpCutOffanddblLwCutOffare passed to the inner Solver as they are, a value of the Lagrangian Dual beyond one of them certifying the same of the problem, and thekCutOffof the inner Solver is returned as it is, byPrimalProximalHeur(which then stops, on the unpenalized iteration, the only one the cutoffs are set on) and byLagrangianDualRelaxationSolvertoo. The conditional bound of the Lagrangian Dual Block is the value of the sub-Blocks with the opposite sense on the box of their variables, by aBoxSolver(box_bound()), beyond which the Lagrangian Dual proves the problem empty, when that is finite (say, binary knapsacks, or thermal units with the box of their power);PrimalProximalHeuralso stops if the unpenalized Lagrangian Dual is unbounded or infeasible -
the BlockSolverConfig of each sub-Block is given to its LagBFunction, which applies it at its first
compute(): a sub-Block that the inner Solver never computes, as the easy components of a BundleSolver, gets no Solver, andget_dual_solution()leaves its dual values as the inner Solver has written them, instead of overwriting them with those of a Solver that has never solved it -
a sub-Block without a BlockSolverConfig is never computed by its LagBFunction, which is told that it has no inner Solver even if the sub-Block has some: if the inner Solver asks for it, i.e., it does not handle it as an easy one, an exception is thrown
-
the class comments of LagrangianDualSolver.h describe the present behaviour only, write the Lagrangian Dual, the sign of the multipliers and the dual of a ranged constraint in Doxygen LaTeX, and say which constraints and which changes of them are refused; the doc of
int_InnerS_WVarSCfgandint_InnerS_WDualSCfgsays which solution each retrieves, the other way round than it did -
the check that the Block has no Variable of its own asks it for its groups, and the dictionaries of the relaxed Constraint are filled one run at a time, the vectors of
boost::anythey used to read not being there any more -
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
intInnerMaxIteranddblInnerRelAccofPrimalProximalHeur, theintMaxIteranddblRelAccof the inner Solver, which are now in the ComputeConfig of the inner Solver, given withstr_LDSlv_ISCfg
-
a relaxed constraint with two different finite sides (or with LHS > RHS) is refused with
std::invalid_argumentalso withint_LDSlv_NNMult0, which relaxed it as the equality on its RHS and returned that bound as the optimum, and also when it is added as a dynamic constraint -
a change of the sides of a relaxed constraint, or its relaxation, that changes its type (equality, <=, >=, free) is refused by
compute()withstd::logic_error, again at each call until the type is back: it was taken as a change of the coefficient of a multiplier whose sign was made for the old type, e.g., a minimum budget given to a pollutant constraint of a UCBlock (set_pollutant_min_budget()) relaxed the constraint at the wrong side andcompute()returnedkOKat the old optimum; a change that keeps the type is taken whatever its sequence of Modification (two sides moved one after the other through a ranged state threw), and a relaxed constraint keeps a zero coefficient when its sides change -
the constant term of the Objective of the Block whose constraints are relaxed (with
intRecursive, of all of them) is the constant term of the Objective of the Lagrangian Dual, also after it changes, and entersbox_bound(): it was left out of the dual function and of the bounds, which differed from those of a MILPSolver by it (e.g., the constant terms of the NetworkBlock that a UCBlock with a single bus keeps in its Objective) -
with
intRecursivethe Modification of the relaxed constraints of the Blocks below the root were discarded -
an exception while the Modification are processed left the Block and the Solver locked, so that the next
compute()could not lock the Block -
a relaxed constraint that is free or relaxed when the Solver is attached, or when it is added as a dynamic constraint, crashed
-
removing dynamic constraints: a subset of them that is not a range removed all the dynamic multipliers, a range of them one too few, with
intSparseLagPairsthe dual pairs of the LagBFunction were removed by the index of the multiplier among all of them rather than among their own, and a constraint added and removed before the samecompute()was given a multiplier all the same -
in a Branch-and-Bound, a node of
LagrangianDualRelaxationSolverstarts from the Lagrangian multipliers its father ended at, which itsLagrangianChangecarries, the inner Solver restarting from them, rather than from those of the node solved last: after an infeasible node, whose multipliers grow without bound, the next ones were solved from there and their Lagrangian Dual stopped on bounds of -1e11 and below -
has_var_solution()gives the components the Modification kept aside for them, asget_var_solution()does, before asking the inner Solver for its dual solution: those of a Solver that has changed the components meanwhile (say,PrimalProximalHeurfixing some Variable and freeing them) may take away linearizations the primal solution is made of -
the primal recovery of
PrimalProximalHeur(recover_primal()) fixes the binaries it rounds only if they are free, and un-fixes only those it has fixed: it used to un-fix all of them, so that, the sub-Blocks not being copied (int_LDSlv_iBCopy0), the variables fixed by the instance or by a Branch-and-Bound were freed, with no Modification, after the first primal recovery -
a component given back to its father is always told that someone listens to it, the LagrangianDualSolver: when this happened within
set_Block(), before the LagrangianDualSolver was among the Solver of its Block, the component was told that nobody did, and it issued no Modification -
a dynamic
FRowConstraintadded to the Block while the Solver is attached no longer needs the Block to be reloaded: the Constraint removed by aBlockModRmvare read by reference (taking them by value calls the copy constructor ofConstraint, which throws by design), the two dictionaries that give the multiplier of a dynamic constraint and the constraint of a multiplier are indexed from the first dynamic constraint and not from the first constraint of all, they learn the new row before it is looked at, and a row that is "infinitely loose" or relaxed leaves the others to be dealt with instead of ending the loop over the added ones -
compute(),get_var_solution()andget_dual_solution()unlock the components that the Block, locked by the LagrangianDualSolver itself, keeps locked with it, and lock them again after: when whoever callscompute()has locked the Block and given its own id to the Solver, as theInvestmentFunctiondoes, the LagBFunction of a component, which locks it with its own id, could not, and the inner Solver returnedkErrorat the first evaluation -
get_dual_solution()asks the dual solution of a component to the Solver its LagBFunction uses, and if that Solver has none at the moment (its Objective having been put back aftercompute()) the component is solved again at the multipliers of the solution, where its duals belong; a component whose Solver cannot give duals at all (e.g., a dynamic programming) leaves its Constraint as they are, which a reader such as aBendersBFunctionhas to check -
when a linking constraint grows, the Modification the added dual pairs of every
LagBFunctionissue go in the channel the branch opens, as the one of the objective of the Lagrangian Dual already did: they are one change of the Lagrangian Dual and whoever observes it has to see them together, which is what the branch that removes them, and the one that adds a Variable to a constraint that is already there, do -
compute()holds the components before it processes the outstanding Modification, not after: those of the Variable of a component look for it through the Lagrangian Dual, which is its father only while it is held, and threw "Variable belonging to wrong Block" -
a
NBModificationof a sub-Block no longer empties the list of the Modification waiting to be processed: the Lagrangian dual discards it anyway, itsLagBFunctiontaking care of it, while the list lost the changes of the coefficients of the relaxed constraints issued before it. AnInvestmentFunctionthat scaled the units of a stage and then removed its cuts left the Lagrangian dual with the old coefficients, above the integer optimum -
several LagrangianDualSolver (a PrimalProximalHeur included) attached to the same Block work together: with
int_LDSlv_iBCopy 0each of them evicted the sub-Block into its own LagBFunction when it was attached and kept them there, so that the one attached later found them in the tree of the first ("Variable belonging to wrong Block") or took them from it, and the BlockSolverConfig of its LagBFunction, applied in differential mode, replaced the inner Solver of the first on the same sub-Block, the first then computing its bound with the Solver of the second. The components are now held only within compute(), get_var_solution() and get_dual_solution() (the Modification they issue meanwhile are handed to the LagBFunction when they are held again), the BlockSolverConfig of the LagBFunction is applied in additive mode, and each LagBFunction is told the position of the inner Solver its configuration has registered (intInnrSlvr). On a TSSB of thermal units the dual of the scenarios, the nested and the recursive dual and the PrimalProximalHeur now give the bound each gives alone, whatever the others attached and their order -
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 change to a relaxed constraint, such as a coefficient rewritten by the scaling of a unit, reaches the right Lagrangian term also when
intSparseLagPairsis on (the default): eachLagBFunctionthen holds only the dual pairs of the constraints its sub-Block appears in, numbered among themselves, while the term was looked up by the index of the constraint among all the relaxed ones, which rewrote the wrong term or crashed; the term is now found through its multiplier, and a sub-Block that enters a relaxed constraint for the first time gets the dual pair it lacked -
has_var_solution()answers false after acompute()that returnedkErrororkBlockLocked: it asked the inner Solver whether it had a dual solution, which a bundle has (the multipliers of its master problem) also when the evaluation of a component failed, and the caller that believed it hadget_var_solution()throw "no coefficients stored" on the component whose linearizations were never computed -
PrimalProximalHeurrecords a recovered point only if it is one: a point is discarded when its value beats the bound the Lagrangian Dual gives, no feasible point being able to do that, and when it violates the rows of the Block by more than a relative 1e-6. The branch that had none of these checks is the one without static binary Variable to put the proximal term on, where the heuristic skips its loop altogether and records whatever the consensus recovery leaves in the Block; on a unit commitment instance translated from PyPSA it reported an upper bound below its own lower bound and handed over a point violating the dualised rows by 5781, which the scaling of the rows of the master problem made visible by moving the trajectory. Note thatBlock::is_feasible()is not the question here, passing as it does over the relaxed Constraint, which are exactly the ones a recovered point can violate -
int_par_idx2str()namesintRecursivetoo, the array of the names having stopped one short of the parameters and answered with the name of another one -
PrimalProximalHeurcounted, as the cost of a point, only the objectives of the sub-Block of its Block and not those of the Block nested into them, e.g., the HydroUnitBlock of a HydroSystemUnitBlock, so that the value it reported was not that of the solution it gave; the objectives of the whole subtree of each sub-Block are now copied and evaluated -
the initial multipliers of a relaxed Constraint that is reversed (a >= one in a minimization, with
int_LDSlv_NNMult) were read from its dual without changing its sign, whileget_dual_solution()writes them with the sign changed, so that a warm start, and that ofPrimalProximalHeurfrom the duals of the relaxation, gave these multipliers the wrong sign; they are now read back as the multipliers that were written -
PrimalProximalHeurlooks for the BlockSolverConfig of its recovery after the filename prefix of all Configuration, where it then opens it -
with
intRecursivethe components are given back to their own fathers when the Solver is detached, rather than to the root, which is not the father of a component taken below it and made the detach read past the end of its sub-Block -
makefile-c and makefile-s bring in MILPSolver, which PrimalProximalHeur needs, rather than leaving $(MILPSINC) to the including makefile
0.4.0 - 2026-09-13
intLogVerbofPrimalProximalHeuris one composite value, v + 4 * w, carrying the verbosity of the heuristic and that of its inner Solver
0.3.0 - 2026-09-12
-
the first iteration of PrimalProximalHeur is not penalized, so that the bound it produces is the one of the original problem: get_lb() / get_ub() report it and it is what the accuracy the heuristic is asked for is measured against. intUseWarmStartPSol asks for the previous behaviour, i.e. that the primal solution of the warm start be the proximal center of the first iteration
-
the parameters of PrimalProximalHeur named after the base Solver refer to the heuristic itself: intMaxIter is the number of its iterations and dblMaxTime the time it is given as a whole, out of which each call to the inner Solver gets what is left; the inner Solver is given intInnerMaxIter and dblInnerRelAcc (was dbl_LDSRelAcc), and the configuration files of the Solver of the warm start and of the primal recovery are strWarmStartBSC (empty, i.e. no warm start, by default) and strRecoveryBSC
-
the trace of PrimalProximalHeur is compiled in and silent by default, intLogVerb turning it on at runtime, rather than the other way around
-
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 warm start of PrimalProximalHeur asked the auxiliary Solver for the dual solution whatever happened to it: a relaxation stopped by the time limit has none, and the query threw. What the Solver has is asked for, and if the duals are not there the multipliers stay where set_Block() put them, exactly as with no warm start
-
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.2.0 - 2025-12-12
-
[huge] PrimalProximalHeur Lagrangian-based math-heuristic
-
put_State() and get_State() methods
-
Configuration for get_[var/dual]_solution() of the InnerSolver can now be set
-
[big] managing of intPushCostToOwner parameter of LagBFunction
-
adapted to new un_any_count thing
-
added missing parameter initialization
-
flawed handling of inner Configuration
-
missing actual setting of int_InnerS_W*Cfg in set_par()
-
right solver called in get_dual_solution()
-
avoided static vectors prone to static initialization fiasco
-
a bunch of stupid bugs
0.1.3 - 2024-02-28
- adapted to new CMake / makefile organisation
-
exploiting the new "father of LagBFunction" mechanism to make Modification from sub-Bloch to reach their original father (instead of UpdateSolver)
-
tentative solution for Modification issues (just ignore them)
0.1.2 - 2022-06-28
-
locking the Solver inside compute()
-
bunch of minor fixes
0.1.1 - 2021-05-02
Minor point release to avoid the master branch to become too stale:
-
significant improvements in handling Configurations
-
Lagrangian variables now initialized with dual variables from the FRowConstraint
- fixed a number of issues (get_lb/ub exchanged, computation of solutions)
0.1.0 - 2021-05-02
Initial release
- Initial release.