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 makefiles build the C++20 module
smspp.ucblockwhenSMSppMODis not empty (e.g.,make SMSppMOD=1), with clang++ 16 or later [seelib/makefile-modof SMS++] -
the unit test writes in netCDF the Solution of each kind of
NetworkBlock, of aDesignNetworkBlockand of aUCBlockon three nodes, in the natural format and in the compressed one, with theConfigurationof the files*SolCfg-*.txt, and reads it back: the Solution read back has to write in the Block what the one written writes and to write in netCDF what it was read from, on a horizon of one instant and of four, with lines with and without a cost, anACNetworkBlockwith no flow and anECNetworkBlockof one interval and of two -
ConversionUnitBlock: a continuous design ("MaxCapacityDesign" >= 0) takes ramps and start-up and shut-down limits, written per unit of design with big-M terms in the commitment and in the start-up and shut-down indicators, as the extendable units with commitment of the energy-system models; and a unit with a design may be on before the horizon, being then built (the binary design is 1, the integer one at least the modules on before it) and, with a continuous design, of a capacity that covers its InitialPower -
BatteryUnitBlocktakes an infiniteMaxPowerorMinPoweralso with a battery design: the rows of the power stay free on that side with no term in the design, as those of an infinite level already did, and an infiniteConverterMaxPoweris no converter limit; a Store of PyPSA, whose power is only bounded by its levels and by its lines, is written so -
HydroUnitBlockreads the optionalExactFlowToPower, one entry per arc: a single-piece turbine whose entry is nonzero produces exactly the power of its piece, its row being the equality whatever the spillways, as for the hydro storage of PyPSA, where the water released without producing is a flow of its own -
ConversionUnitBlockregistersscale()in the methods factory also asConversionUnitBlock::replicate, the name under which a Block is sized by identical copies -
the unit test checks the boxes of the infeasible arcs on a unit whose run from before the horizon cannot pass a low availability, read with it and given it, and back, by
set_availability() -
the unit test checks the caps (27) through the maximum powers of a run on two units with a varying availability, read with it and given it by
set_availability(), whose continuous relaxation in the pt, DP, SU, SD and SUSD formulations is the integer optimum -
ThermalUnitExtDPSolver::get_dp_stats()returns what the last run of the dynamic program did: the on-states built, split by the trimming, kept and their pieces, the largest number kept at an instant, the pairs of states compared for the domination and how many of them removed a state or a part of its domain; these counters do not depend on the machine -
the documentation of
ACNetworkBlocksays which relaxation each value of the static variablesConfigurationgives: with 0 the SOCP relaxation of the bus injection model, a convex QCP whose quadratic rows are second-order cones; by default its strengthening, whose static rows do not restrict the voltage products, so that a solve that does not separate its McCormick inequalities (a continuous relaxation by a:MILPSolverwithintRelaxIntVars1) has the value of the SOCP relaxation -
the unit test solves a
UCBlockon anACNetworkBlockin the two relaxations, read fromtest/ACBCfg-SOCP.txtandtest/ACBCfg-QC.txt, with a:MILPSolverthat solves a convex QCP (GUROBI or CPLEX): two nodes and no line (each node an island, the known value), a line and a chain (radial, the cones tight at the optimum and the two values equal) and a triangle (the SOCP value not above the strengthened one) -
DCNetworkBlockaccepts hyperarcs together with lines of nonzero susceptance, in the KIRCHHOFF, PTDF and CYCLE formulations: a hyperarc, whose susceptance must be zero, is a controllable flow that enters the network as injections at its start and end nodes, so that a meshed grid obeying the voltage law and the hyperarcs of a multi-energy system (say, a back-pressure CHP feeding an electric and a heat node) can be written in one network; the model of a network without hyperarcs, or with hyperarcs and no susceptance, is the same as before -
the unit test checks a meshed electric network with a congested line and a heat node fed by a hyperarc in the three formulations against an independent optimum
-
ConversionUnitBlock, a unit with one commitment and several generators, each on its own node, coupled by an operating region given by bounds and rows proportional to the commitment: plants that convert energy among several carriers (combined heat and power, electrolysers, heat pumps with commitment), inputs being generators of negative power, with minimum up and down times, start-up and shut-down limits, ramps on combinations of the powers, spinning reserves deployed along directions given by the data, its netCDF format, setters with Modification and its Solution -
ConversionUnitBlock: the coefficients of the operating rows may change with the instant ("OperatingMatrix" with a first dimension indexed over the instants, as the other data), e.g., the coefficient of performance of a heat pump; a file with the matrix of old reads and behaves as before -
ConversionUnitBlock: a generator may deliver its power with a delay of some instants ("Delay"), the rows being written at the instant of operation; the power delivered at the first instants comes from the operation before the horizon ("InitialDelayedPower", 0 by default) and the power of the last instants of operation is delivered after it, unless the delay wraps around the horizon ("CyclicDelay"); the Solution saves the powers delivered after the horizon -
ConversionUnitBlock: a unit of N identical modules committed one by one ("Modules"), the commitment, start-ups and shut-downs being the numbers of modules, integer in 0..N, with the data of one module, the minimum down time rows written with N - u and the modules on before the horizon given by "InitialCommitment"; with N = 1 the rows are those of before -
ConversionUnitBlock: the design variable of the other units ("InvestmentCost", "MinCapacityDesign", "MaxCapacityDesign", with their sign convention): binary (the unit built or not, u <= N x), integer (the number of modules built, each committed by itself, u <= x and the minimum down time rows with x - u, exact and linear) or continuous (the capacity, the data being per unit of it, with the big-M relaxation of the product with the commitment on the largest capacity, documented as weak); its cost in the Objective, its setter and its value in the Solution -
data/gen_multi_energy.pywrites the multi-energy instances (a combined heat and power plant as aConversionUnitBlockand with hyperarcs, a meshed network with a heat node) and prints their known optima; they are inmulti-energy/of the archive ofdata/from version 2026-10-12 -
data/gen_constant.pywrites two busUCBlockwhose ownObjectivehas the constant term of the network (+1000 and -1000) and prints their optima by hand; they are inconstant/of the archive ofdata/from version 2026-10-13, which is that of 2026-10-12 plus this folder -
data/gen_conversion.pywrites the instances that pypsa2smspp makes of the committable links of its test networks, each aConversionUnitBlock(a combined heat and power plant with ramps, a store, a meshed network, a heat pump whose COP changes over time, delays, modules, the modules built and a continuous design), and prints the optimum of PyPSA; they are inconversion/of the archive ofdata/from version 2026-10-14, which is that of 2026-10-13 plus this folder; version 2026-10-15 adds four of them, with a continuous design with ramps and start-up and shut-down limits, with a continuous design and with modules built on before the horizon, and with modules built for the InvestmentBlock form -
the commitment of a unit whose generators share it is exposed through one generator only, and the documentation of
UnitBlock::get_commitment()says so -
the unit test checks
ConversionUnitBlockagainstThermalUnitBlockwith one generator, against the multi-energy instances written with hyperarcs, on a meshed network with a heat node in the three formulations, its continuous relaxation against the optimum, its setters against a unit read afresh, its refused data and its round trip -
json2nc4.jlaccepts--nuclear-shareand--bands-reservewith--nuclear-single, which then writes the nuclear units of the fleet of--nuclear --nuclear-share, with the reserve participation of the data and bands as wide as that reserve, i.e., the units of the fleet as standalone files; the output of the other options does not change -
the unit test checks the continuous relaxations of the pt, SU, SD and SUSD formulations against the integer optimum on units on and off before the horizon, with both ramps, one or none, and the MILP of every formulation of a
NuclearUnitBlock, with and without bands, againstNuclearUnitExtDPSolver -
the unit test checks that a
compute()ofNuclearUnitExtDPSolverthat throws leaves the Solver usable, its value after a change ofInitialPoweragainst a fresh unit, the rows that forbid a modulation step at a start-up instant and the tolerance ofNuclearUnitBlock::is_feasible() -
the C++20 module
smspp.ucblock(src/UCBlock.cppm), which exports every name that the headers declare at namespace scope and re-exportssmspp, so that a source file can writeimport smspp.ucblock;in place of the#includeof the headers; it is built with the CMake optionSMSpp_BUILD_MODULE, as the targetSMS++::UCBlock_module -
the energy carriers of
UCBlock: "NumberCarriers" and "CarrierName", the carrier 0 being the electricity described by all the data as before, and for each other carrier the group "Carrier_c" with its "NumberNodes", its NetworkData and classnames, its "EnergyDemand" and its "PollutantZones"; a carrier with more than one node has one NetworkBlock per time instant, built byUCBlock, unless its group gives them as the carrier 0 does ("NumberNetworks" and "NetworkBlock_n", e.g., oneDesignNetworkBlockover the horizon for a network whose lines have a design), and each carrier has the balance rows (1c) of its nodes [seeget_carrier_balance_constraints()]; the UCBlockSolution saves the Solution of their NetworkBlock and the duals of (1c) with those of the carrier 0, in the group "Carrier_c" -
the outputs of the generators of a
UnitBlock:get_number_outputs(),get_output_carrier()andget_output_efficiency(), by default the outputUnitBlock::kElectricitywith efficiency 1, otherwise read byUnitBlock::deserialize()from "NumberOutputs", "OutputCarrier" and "OutputEfficiency" (constant or per instant, the same for all the generators or one per generator, as the generators of aConversionUnitBlock, each on the carrier of its port, need, the number of the generators being checked byUnitBlock::check_outputs()): the active power of a generator gives the efficiency of each output times itself to the carrier of the output, at the node of the generator on it, a negative efficiency being what the generator consumes, in (1), (1b) and (1c); the standard nameskElectricity,kHeat,kGasandkHydrogen -
"GeneratorNode" indexed over the generators and the carriers, with -1 where a generator has no node on a carrier (and, as before, not needed by a network with no generator at all); such a generator gives nothing to that carrier, and one with no node on the electricity is in no zone of the reserves and of the inertia; its emissions are counted at its node on the first carrier where it has one [see
get_generator_pollutant_zone()andget_storage_pollutant_zone()] -
UCBlock::set_energy_demand(), with a flattened index over the nodes of all the carriers, which also changes the demand of the carriers other than the electricity, and the Modification typeUCBlockMod::eSetEnD -
UCBlock::for_each_balance_term(), the terms of a unit in the balances of all the carriers, each with its coefficient per unit of scale, which is what the linearization of an investment in the unit needs -
"NumberGenerators", the new name of "NumberElectricalGenerators", which is still read
-
deserialize_local_dim(), which reads a dimension of a group and not of the groups above it -
data/gen_carriers.py, the instances with the electricity, the hydrogen, the gas and the heat as carriers made of small PyPSA networks, whose optimum is the reference, and the back-pressure cases and the mesh ofdata/gen_multi_energy.pywith the electricity and the heat as two carriers (me_*_carrier), also with the plant as theConversionUnitBlockwhose generators have their outputs on the electricity and on the heat (me_*_unitcarrier, the extraction-condensing case included); anddata/gen_conversion.py --carriers, which writes the cases of the committable links of pypsa2smspp also with the gas, the electric and the heat buses as three carriers (conversion/*_carriers.nc4) -
the unit test of the carriers: the coefficients of (1c), the optimum by hand on one heat node, on a heat network and on a heat network given as one
DesignNetworkBlock, the duals of (1c) as the prices of the carriers, a Solution without them written back,set_energy_demand(), the scale of a unit, the netCDF group and back, the pollutant zones of a unit with no node on the electricity, the outputs per generator of aConversionUnitBlockand back, and the data that cannot be read
-
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 exception of
ThermalUnitBlockwhen the cost of a Variable that is not in its Objective changes says so, and that the reserve variables are there only with a nonzero cost or when the objective Configuration of the BlockConfig asks for them, as aLagrangianDualSolverneeds -
in the pt, SU, SD and SUSD formulations of
ThermalUnitBlock(and ofNuclearUnitBlock) the arc of a run that no power satisfies (its bounds, start-up and shut-down limits and ramps leave an empty interval at some instant, the runs whose economic dispatch the DP Solvers find infeasible) is fixed to 0 by its box "ArcBound_Const_Thermal", which update_rows() changes with the data; before, such a run could carry a fractional flow in these formulations, whose relaxation could then be below that of the DP one also with the SUSD formulation -
the maximum power rows (27) of the pt, SU, SD and SUSD formulations of
ThermalUnitBlock(and ofNuclearUnitBlock, which inherits them) carry by the ramps every maximum power met along a run, not only the start-up and shut-down limits: the cap of an instant is the least of the maximum powers of the instants of the run plus the ramps from them to it, which is tighter when the maximum power changes in time, e.g., with the availability, and the same as before with constant data; on 1312 random units with a varying availability the continuous relaxation of SU is below the integer optimum on 48 instead of 99, that of pt on 74 instead of 129 -
the documentation of
ThermalUnitBlockcites the SD and SUSD formulations from their published version (AIRO Springer Series 5, 2021) in place of the technical report R. 20-01 of IASI-CNR, whose rows have the same numbers in the two -
the bounding of
ThermalUnitExtDPSolvermay use a bound of a derived unit that depends on the label and on the output (completion_bound(), built byprepare_completion_bound()once more than 256 states are kept at an instant);NuclearUnitExtDPSolvergives the cost-to-go of a relaxation of its rules, computed backwards on a grid of the output, which keeps the stability, the lockout and the counter of the modulations of the day: the values are the same to the last digit, and on the subproblems of the Lagrangian duals without bands the dynamic program takes about half of the instructions it took -
ThermalUnitExtDPSolverprunes the on-states by bounds, as a branch-and-bound does: a state whose least value plus a lower bound on the cost of the instants that follow (each at least the smaller of 0 and the least cost of being on there) exceeds the cost of a feasible schedule already met (one that stays off from an off-state, or that keeps the output of an on-state to the end) is removed, which leaves the optimal value unchanged, and on the instances of the tests the value of each solve is the same to the last digit; the new virtual methodsmin_move_cost()andcan_hold()give the derived units the cost of their moves and whether a label can keep its output, whichNuclearUnitExtDPSolverimplements. On the subproblems of a Lagrangian dual in which a nuclear unit stays off, the dynamic program keeps a few states instead of thousands at an instant -
the unit test checks the bounding on a nuclear unit with long modulations at prices at which it stays off: the value is 0 and no on-state is kept
-
the domination of
ThermalUnitExtDPSolvercompares a state only with the states whose label dominates its own and whose domain meets its own, the answers oflabel_dominates()being asked once per pair of labels, and kept until the parameters are loaded again: the states kept and their value functions are those of before, and the cost of the pruning is no longer that of all the pairs of states (on a unit with long modulations, the domination took 96% of the time and the decoding of the labels alone 43%) -
the documentation of
UCBlockand of the model describes nodes of any carrier (heat, gas) with their units, lines, zones and pollutant budgets, instead of saying that units producing heat are not modelled -
in a network with hyperarcs and lines of nonzero susceptance,
DCNetworkData::get_end_line()holds the (first) end node of every line, andDCNetworkData::get_line_efficiency()returns 1 for a line of nonzero susceptance instead of throwing; a hyperarc with nonzero susceptance is refused byDCNetworkData::deserialize() -
the instances of
data/are downloaded and extracted by the targetsdownload_uc_<fmt>andextract_uc_<fmt>, written as in every module that keeps its instances in the Package Registry, and the marker of the extraction carries the format in its name, so that a tree extracted before extracts once more -
the single-bus thermal instances (
tools/json2netCDF/instances_singlebusand theUC_singlebusdata) start from an initial state consistent with the demand and the reserve at instant 0: the units are committed in merit order while their minimum powers fit the lowest load net of the reserve of the first day, each holds an output with one ramp of room on both sides, and the others are off long enough to start at once; with the reserve, the secondary requirement is capped so that the reserve plus the change of the load in one instant fit within 0.7 times the one-instant ramp of the fleet; every thermal instance with the reserve is feasible over its horizon -
the nuclear single-bus fleets (
UC_singlebus-nuclear, built bygen-nuclear-uc) are made of nuclear and thermal units: the largest units, whose capacity is at least 45% of the fleet's, areNuclearUnitBlockand the othersThermalUnitBlock, which balance the load around the nuclear output; the low and the high band of a nuclear unit are as wide as the primary plus secondary reserve it offers at full output, rather than 30% of its range, and its longest modulation no longer binds (it was 8 instants), the length of a modulation being set by the ramps and the bands; every instance of a day is feasible.json2nc4.jlhas the options--nuclear-share,--bands-reserveand--mod-length 0for this -
json2nc4.jlkeeps the band breakpoints and the deep-decrease threshold of aNuclearUnitBlockat least a tenth of a ramp away from a whole number of ramps from the minimum and the maximum power, and the standalone units it writes enter the horizon on at their minimum power whatever the initial state of the fleet they are taken from, so that the standalone units of1UC_Data/nucleardiffer from the previous ones only in those thresholds -
the ramp rows of the DP formulation of
ThermalUnitBlockfind the power of the previous instant of the run and the commitment of the run without scanning all the powers and all the runs for each row (the same rows) -
the minimum power, maximum power and initial perspective cut rows of the DP formulation of
ThermalUnitBlockfind the commitment and the cut variable of the run without scanning all the runs and all the cuts for each row (the same rows) -
the NetworkData and the NetworkBlock read their dimensions from their own group only [see
deserialize_local_dim()]: a "NetworkBlock_i" group with its demand alone no longer takes the "NumberNodes" of the UCBlock for a NetworkData of its own, and the network of a carrier cannot take the "NumberLines" of the electricity -
the balance rows (1), (1b) and (1') are written and updated by one function of their terms [see
UCBlock::balance_terms()], and a commitment Variable whose fixed consumption is 0 at an instant is no longer in the row of that instant with more than one node
-
expected_dims()andexpected_vars()of theUnitBlock, of theNetworkBlockand of theirNetworkData, and ofUCBlock, are declared also withNDEBUG, as those ofBlockin SMS++, so that the virtual tables do not depend on it -
the C++20 module
smspp.ucblockexportsdeserialize_local_dim()ofNetworkBlock.h, which it did not since the function was added after the interface was written, and its global module fragment includes only the headers that no other one includes -
test/makefilelinks SMS++, whichlib/makefile-cleaves out ofUCBckOBJforlibUCBck.a: the test did not link -
a build of the module alone, with
find_package(), fetches FastFlow, whichThermalUnitDPSolver.cppincludes and which an installed SMS++ does not carry [seecmake/FastFlow.cmakeof SMS++] -
UnitBlock::serialize()declares the efficiencies of the outputs with one entry per instant as an array, not as a function (the most vexing parse, which Clang 21 rejects) -
the makefiles of the tests and of the tools build also with g++: they take the option that stops at the first error and the one of the debugging information, which clang++ and g++ spell differently (
-ferror-limit=1and-glldbagainst-fmax-errors=1and-ggdb), fromSMSppSWERRandSMSppSWDBGoflib/makefile-OSof SMS++ -
scale()ofIntermittentUnitBlock,BatteryUnitBlockandConversionUnitBlockrefuses a scale other than 1 on a unit whose design has more than one module (or, for theConversionUnitBlock, with more than one module), asdeserialize()does: the two are the same fleet, which a unit with both counts twice -
a Block written with
serialize()and read back withdeserialize()has all the data it was read with:DCNetworkBlockwritesKappa(when a constant of a line is not 1),ThermalUnitBlocktheReferenceScheduleandFixToMaximum,BatteryUnitBlockandHydroUnitBlocktheReferenceScheduleand the bounds of the reactive power, andIntermittentUnitBlockandSlackUnitBlockthe bounds of the reactive power; the unit test writes each of them to an image, reads it back and checks that the image of the copy is the same -
DCNetworkBlockSolution,ACNetworkBlockSolutionandBatteryUnitBlockSolutionread their values indexed over the lines, the nodes or the instants whatever their size, and a netCDF scalar gave one value, past whichread()andwrite()went, running over the lines, the nodes or the instants of the Block: a scalar, which their format does not have, is refused bydeserialize()withstd::invalid_argument. AUnitBlockSolutionwith no "NumberGenerators" read a scalar into the first instant only, and the others were 0: it gives the same value to every instant, as it already did with "NumberGenerators". The data of the Blocks that are netCDF scalars were already the same for every line, node or instant; the unit test checks them on the networks, and that each of these values of the Solution is refused, or read, as a scalar -
the distribution factors of the HVDC lines that a
DCNetworkDatakeeps were those of the instant only, and aDCNetworkBlocksharing it with another of the same instant (as those not in aUCBlock, all at the instantInf< Index >()) but with another Tikhonov coefficient got the factors of that coefficient: the coefficient is part of what they are kept for (DCNetworkData::was_DCDF_computed()andcompute_DCDF()take it, 0 by default), and the unit test checks the factor of each coefficient on aDCNetworkDatashared in both orders -
an
ACNetworkBlockwith no "ReactiveDemand", or with no bound given on its reactive injections (as one that is not in aUCBlock), read past empty vectors when generating its constraints: the reactive demand is 0 and the bounds not given are infinite, also on the other side of a bound given afterwards. The data with no default are refused withstd::invalid_argumentwhere they read past them before: "LineReactance" and "LineResistance", mandatory as documented, byACNetworkData::deserialize()when there are lines, "LineRATEA" (with AC lines) and the data of the nodes byACNetworkBlock::generate_abstract_constraints(), before generating anything; "LineMinAngle" and "LineMaxAngle" remain optional -
HydroSystemUnitBlock::deserialize()andHydroSystemUnitBlockSolution::deserialize()looked up each sub-group withNcGroup::getGroup(), which lists all the sub-groups at each call: they list them once, asUCBlockdoes -
the compressed format of the
NetworkBlockSolutionof several networks (that of aUCBlockSolutionand of aDesignNetworkBlockSolution) dereferenced an absent (nullptr) Solution, which the natural format skips; the compressed format has no way to say that a Solution is absent, andNetworkBlockSolution::serialize()throwsstd::invalid_argumentbefore writing anything -
DesignNetworkBlock::get_Solution()passed to its sub-Solution bit 0 of its ownConfigurationand its bits from 3 upward shifted by 3, so that bit 3 (its compressed format) became bit 0 of the sub-Solution and they held their node injections also when bit 0 did not ask for them; only the bits from 4 upward are passed, as documented, and the default 31 is documented as what it is (15 + 16, the sub-Solution with their node injections and their flows) -
DCNetworkBlock::set_dual_prices()was not the inverse ofget_dual_prices()on a line with a design variable: the latter gives the sum of the dual values of the two sides of its flow limit (the upper row, and the lower row or the lower half of the bound), the former wrote the whole price on the bound, which a line with a lower row does not use, so that aDCNetworkBlockSolutionwritten in the network did not read back its prices; the price now goes by its sign to the side that it belongs to, the upper row if it is nonnegative and the lower side otherwise, with 0 on the other side -
DCNetworkBlock, PTDF formulation of a network with lines with susceptance and HVDC lines that is not in aUCBlock: its instant isInf< Index >(), the value that marks the distribution factors of the HVDC lines as never computed, hence they were never computed and the rows of the flows read an empty matrix (an Eigen assertion in Debug, undefined behaviour otherwise); the matrix, which has a row and a column per line, is read where it is instead of being copied for each line -
NetworkBlockSolution::serialize()wrote the node injections of a Solution of one instant, as that of eachDCNetworkBlockof aUCBlock, out of the column of the instant instead of its row, reading past the array for every node but 0: the natural format of aUCBlockSolutionheld garbage, now it holds the injections -
DCNetworkBlockSolution::deserialize()wanted "FlowValue", "AuxiliaryValue" and "DualCost", whichserialize()writes only when the Solution holds them: a Solution with no auxiliary variable (no line with a nonzeroNetworkCost) or with no flow could not be read back in the natural format, andUCBlockSolution::deserialize()lost itsNetworkBlockwithout a word; they are optional, as documented, a missing "NetworkBlock_" group is a missingNetworkBlockas forserialize(), and one that is not aNetworkBlockSolutionan error -
ACNetworkBlockSolution: the natural format wrote "ReactiveFlowTOValue" in place of "ReactiveFlowToValue" and read its three variables as mandatory; the compressed one wrote the reactive injections only with the reactive flows, and with the flows and no injection wrote on a null variable;clone()(and soscale()) lost the auxiliary variables -
DCNetworkBlock::set_power_flow()read past its argument on anACNetworkBlock, whose flows are two per line (the "from" ones first) while aDCNetworkBlockSolutionholds one per line: it writes the firstpf.size()flows -
the compressed
DesignNetworkBlockSolutionread the node injections of its sub-Solution as its own, and once read could not be written again ("NodeInjection" defined twice) -
"EndInstant", an
NcInt, was read in asize_tthroughvoid *byNetworkBlockSolutionandECNetworkBlockSolution, which is right only on a little-endian machine; it is read in anint -
reading a
UCBlockSolutionin the natural format was quadratic in the number ofNetworkBlock, 301400 calls ofnc_inq_grpname()for 548 instants, sinceNcGroup::getGroup()lists all the sub-groups at each call:UCBlockSolution::deserialize(),UCBlock::deserialize()and the two ofDesignNetworkBlocklist them once, and the calls are 550 -
BatteryUnitBlock::get_converter_kappa_linearization(), and with itget_kappa_linearization()when the converter follows the kappa, was NaN with an infiniteConverterMaxPower, the zero dual value of the free converter row being multiplied by the infinite power; the converter rows (10) of a battery with no design variable now exist only at the instants where the converter power is positive and finite, the second index ofget_converter_bounds()running over these instants, so that no free row is handed to the Solver, and the two linearizations skip the other instants -
an infinite
MaxPrimaryPowerorMaxSecondaryPowerof aBatteryUnitBlockgave the reserve bound (9) the right-hand side NaN at kappa 0 (0 times +inf), a row (9d) with an infinite coefficient of the design variable, and a NaNget_kappa_linearization()(the zero dual of the free bound times +inf): the bound is now +inf whatever kappa is, the row (9d) is free with no term in the design, and the zero dual gives no term, as for the other infinite bounds of the unit -
the bounds of the node injections of
UCBlockfollow a change of the power range of a unit also when no Solver is listening, a Solver attached later reading them stale otherwise:UnitBlock::issue_unit_mod()issues the Modification of a unit as usual if anyone reads it and passes it to the father alone otherwise, if it changes the power range, asscale()already did; the setters ofMaxPowerofThermalUnitBlockandIntermittentUnitBlock, of the kappa ofIntermittentUnitBlockandBatteryUnitBlock, of the data ofConversionUnitBlockandscale()use it, and the unit test checks the bounds also with no Solver attached -
UCBlockrecomputes the bounds of the node injections after a change ofMaxPowerorMinPowerof aConversionUnitBlock, which left them as they were: eachUnitBlockModsays bychanges_power_range()whether its type changes a datum of those bounds, andUCBlock::add_Modification()asks that of every unit instead of listing the types of some classes; the unit test compares the bounds after each such change with those of theUCBlockread afresh from the changed data -
the Solution of a
DCNetworkBlockholds the auxiliary Variable V_l >= | F_l | of the lines, which carry the cost of the flows when some line has aNetworkCost: without them, writing the Solution left those Variable at whatever value they had, so that a Solver keeping its solutions as Solution (e.g.,PrimalProximalHeur) wrote back a point whose cost was not the one it reported; they are read, written, scaled, summed, cloned and serialized with the flows (the optional "AuxiliaryValue"), andDCNetworkBlock::set_auxiliary_variable()writes them -
the Solution of a
HydroSystemUnitBlockholds that of itsPolyhedralFunctionBlock, whose epigraph Variable carries the future cost of the water in the linearized representation: without it, writing the Solution left that Variable at whatever value it had, so that a Solver keeping its solutions as Solution (e.g.,PrimalProximalHeur) wrote back a point whose cost was not the one it reported; the Solution is also deleted with its inner ones, and serialized in the optional group "PolyhedralFunctionBlockSolution" -
ThermalUnitBlock(and soNuclearUnitBlock) separated a perspective cut every time the point violated it, also when the same cut, on the same variable and at the same \check{p}, was in the Block already: in the callback of a multithreaded MILP a point comes back many times, and on a unit commitment of 10 units the 71461 cuts of a solve were 31175 distinct ones. A cut is now added only if it is not there already, the solver that has dropped it being the one that gives it again; and the group "PC_cuts_Thermal" is registered once, bygenerate_abstract_constraints(), wheregenerate_dynamic_constraints()added one more group viewing the same list at each call, so that a solver reading the Block anew found each cut once per call made -
DCNetworkBlockandACNetworkBlockaccept a network of several nodes with no line (NumberLines0, with none of the variables of the lines), e.g., nodes of different carriers coupled only by aConversionUnitBlock, where they refused to generate their rows: every node is then an island balanced by its own units, in the PTDF, CYCLE and KIRCHHOFF formulations as in the AC one (the PTDF formulation writes then the node balances of a pure HVDC network, which it wrote only when some line had zero susceptance, while a change of the demand already looked for them); a network with lines gives the same model as before -
ThermalUnitExtDPSolver(and soNuclearUnitExtDPSolver) prunes a state whose domain is narrower than 1e-9 by comparing the values at that point: the steps of exactly the ramp that end at a bound of the output left there states of equal value up to the rounding, which the test piece by piece kept all, and whose number grew at each instant (more than 12 h on 5 of the 25 week-long e7 units of1UC_Data/nuclearwith no reserve price); the optimal values are unchanged -
the maximum power rows of the pt, SU, SD and SUSD formulations of
ThermalUnitBlock(and so ofNuclearUnitBlock) cap every instant of a run by the start-up limit plus the ramps up from the start-up and by the shut-down limit plus the ramps down to the shut-down, the first and the last instant included, where only the limits were used: the two arcs of a run that reaches the end of the horizon, which describe the same schedule, had different caps at its last instant, and the continuous relaxations of the pt, SU and SD formulations could be weaker than that of the DP one; a cap below the minimum power is no longer raised to it, so that a run that the limits and the ramps make impossible at an instant is excluded there -
ACNetworkBlockwithoutLineMinAngleandLineMaxAngleno longer reads out of empty vectors when its rows are generated: the angle differences are then not bounded, there are no rowsAC_angle_bounds_limitandAC_elem_bounds, and the strengthened relaxation, which needs the bounds, is not generated unless a Configuration asks for it, in which case it is refused; one of the two variables without the other is refused when read (ACNetworkData::has_angle_bounds()tells whether they are there) -
ThermalUnitDPSolverandThermalUnitExtDPSolvercompute again the ramps they take for a unit withoutDeltaRampUporDeltaRampDownwhenInitialPowerchanges, so that the move from a newInitialPowerabove everyMaxPoweris free, as in the rows -
ACNetworkBlockreadsLineMinAngleandLineMaxAngleas MATPOWER does: a value at most -360 (respectively at least 360), or NaN, leaves that side of the angle difference unbounded, and 0 for both leaves it unbounded altogether (ACNetworkData::get_angle_difference_bounds()); the bounds of +-360 of the MATPOWER instances made the rowsAC_angle_bounds_limitforce the imaginary part of the voltage product to 0 andAC_elem_boundsits real part above the product of the minimum voltages, and 0 / 0 made the angle difference 0. A line with an unbounded side, or with a range of a whole turn, now has neither group, and one with a range wider than 180 degrees has noAC_angle_bounds_limit(no such row is valid there); two finite bounds in the wrong order are refused when read -
the bounds
AC_elem_boundsofACNetworkBlockare the exact ranges of the real and imaginary parts of the voltage product over the bounds of the angle difference and of the voltages: the boundsin( max - min )of the imaginary part cut feasible points for symmetric bounds wider than +-45 degrees, and the lower bound of the real part was wrong for bounds beyond +-90; the rowsAC_angle_bounds_limitof a bound beyond +-60 degrees are written ascos( phi ) s - sin( phi ) c, valid beyond +-90 too -
the strengthened relaxation of
ACNetworkBlockcovers the AC lines whose bounds on the angle difference are both within +-90 degrees, where its envelopes are valid, and leaves out the others, instead of using the envelopes of any range for every line -
the elementary check of the flows of
ACNetworkBlockno longer reads the name of a line out of an empty vector when the data have noLineName -
the row of
NuclearUnitBlockthat forbids a modulation step at a start-up instant (NoStartUpModulation_Nuclear) ism_t + v_t <= 1, as documented, and no longer the voidm_t - v_t <= 1, which let the MILP take a step at a start-up that the dynamic program does not admit -
NuclearUnitBlock::is_feasible()checks the part ofThermalUnitBlockwith the tolerance it is given, and no longer with the default one ofBlock -
compute()ofThermalUnitDPSolverandThermalUnitExtDPSolver(and so ofNuclearUnitExtDPSolver) releases the mutex of the Solver also when it throws, e.g., on a fixed Variable whose value the dynamic program refuses, andNuclearUnitExtDPSolver::get_var_solution()releases the lock of the Block likewise: the mutex stayed locked, and any other thread asking for the Solver waited forever -
UnitBlock::serialize()writes "ChangeIntervals", the name thatUnitBlock::deserialize()reads, instead of "ChangeInterval"
0.10.0 - 2026-10-09
-
BatteryUnitBlockhas a kappa of its own for the converter (ConverterKappa,set_converter_kappa(),get_converter_kappa(),get_converter_kappa_linearization()), so that storage and converter can be sized apart; without it the converter followsKappaas before -
the unit test checks the continuous relaxation of the SUSD formulation against that of the DP one (
LPRelaxBSCfg.txt,LPRelaxMILPCfg.txt) and the two kappas ofBatteryUnitBlock -
the unit test checks
update_rows()on the rows of a class derived fromThermalUnitBlockandset_init_updown_time()after the generation in the seven formulations, and the harness of the power limits changesInitUpDownTimekeeping and changing the initial state -
ThermalUnitBlockhas the box of the active power,0 <= p[ t ] <=the operational maximum power, as the static group ofBoxConstraintActivePowerBound_thermal, kept up to date byset_maximum_power()andset_availability(): the rows of the commitment already imply it, but a Solver that only reads the boxes (say, aBoxSolverbounding the Objective, asLagrangianDualSolverdoes) needs it to see the power bounded -
the unit test checks the cyclic reservoirs of
HydroUnitBlockafter a change of the inflows and of the initial volume, the design variables ofBatteryUnitBlockandIntermittentUnitBlockwhose two bounds are 1, a battery of several modules with binary variables and reserves, and the refusal of a negativeNetworkCost -
the unit test checks the costs and the rows at instant 0 of the reserves of
ThermalUnitBlockagainst the DP Solvers and a :MILPSolver on random instances, the availability in the DP Solvers, the changes of the maximum power and of the availability, the reactive power of absorbing units, the bounds on the node injections after a scaling, the inertia rows of a unit at two nodes of a zone and the infinite storage bounds of the batteries -
the unit test compares, after a change of
MaxPower,Availability,InitialPowerorInitUpDownTime, the model of every formulation of aThermalUnitBlockand of aNuclearUnitBlockwith the one of the unit read afresh from the changed data, row group by row group, and the dynamic programming Solver with a :MILPSolver (test_power_limits.cpp, the formulations asBlockConfigfilesTUBCfg-*.txtandNUBCfg-*.txt; a reduced matrix by default, the whole one with--power-limits --full); it also checks the shut-down at instant 0 in every formulation and the setters ofThermalUnitBlockwith an unorderedSubset -
the page "The unit commitment model of UCBlock" (
UCBlockModel.h) gives the notation, the conventions on instants, signs and scale factors, the composed model, the capacities and the investment, the Lagrangian dual with the meaning and sign of the dual values, the multistage use and one list of what is not modeled; the headers of every Block and DP Solver of the module state their rows as built, with one equation numbering each, and refer to it -
the unit test covers the network formulations on components, references and pure HVDC networks, the hydro delays, spillways, data checks and inertia, the storage balance, converter and design rows of the batteries, the design reserve rows of the intermittent units, the slack unit, the 3bin and DP maximum power rows and the initial power of
ThermalUnitBlock, the end-of-horizon and band rules ofNuclearUnitBlock, the zones and network constants ofUCBlockand, when a :MILPSolver is built, the sign of the dual values of the linking rows -
data/gen_network_cases.py writes the instances of the edge cases of the network, small PyPSA dispatch networks whose DC lines have a susceptance (parallel lines in the same and in opposite directions, a triangle with a parallel side, DC components joined by a link, nodes reached only by links with losses), together with the optimum of PyPSA as their reference; they are in pypsa-data/ucblock of the data from 2026-10-03
-
the module has a unit test of its own in
test/, which needs nothing but the core SMS++ and builds all of its instances in memory: the three DP Solvers are compared with each other and with a brute force on small units (a horizon of one instant, minimum up and down times cut by the initial state or by the end of the horizon, ramps with the limits of the start-up and of the shut-down, the commitment fixed in some instants, the scale factor, the modulations of a nuclear unit), every UnitBlock and NetworkBlock and aUCBlockgo through a netCDF round trip, and the setters of aThermalUnitBlockand of aUCBlockare checked against aFakeSolver. The CI of the module builds it withSMS++alone and runs it -
the data archive 2026-09-26, which adds to
pypsa-datathe networks of pypsa2smspp that the batteries oftestsread in the form of each module: one scenario of the modular family with the design in the units (ucblock/smspp_mod_t48_s1_b2c_det_ucblock.nc), the modular family as an MSSB (mssb/smspp_mod_t48_s10_b2c_o2_mssb_ucblock.nc) and a TSSB of the thermal family, whose scenarios are unit commitments (tssb-thermal/smspp_tuc_u10_t24_s3_b1.nc4) -
ThermalUnitBlock::is_sol_feasible()reads the schedule out of the:UnitBlockSolutionit is given and checks it against the data of the unit, i.e. the operational bounds of the power, the reserves it takes room for, the ramps with the limits of the start-up and of the shut-down and the minimum up and down times with the state the unit comes from, so that the Variable of the unit are not needed, whichis_sol_feasible_physical()says; a Variable of the schedule that is fixed (start-up, shut-down and reserves included) holds it to its value. Those are the constraints of the unit for an integral commitment, which is what every formulation of it encodes, hence a commitment that is not integral is not declared feasible; a unit that carries something the schedule does not answer for, i.e. a dimensioning variable, the reactive power, a reference schedule, a scale of its own or a fixed Variable of a formulation that the schedule only implies (the commitment differences, the pieces of the power, the cuts, the deviation from the reference schedule), is left to the check of the base class, which goes through the Variable and pays an allocation and two passes over them for each entry of a global pool that is revalidated; so is aNuclearUnitBlock, whose modulation the schedule does not show -
tools/replicate_units, which writes an instance with K times the thermal units of a given one, each copy carrying the data of the original, and multiplies by K what counts the replicated components, i.e., the generators, the storages, the demands and the budgets of the pollutants -
ThermalUnitBlock::set_min_up_down_time()sets the minimum up and down times before the Variable are generated, which is when they can still be set: unlike the other data they decide how many Variable there are, and the method throws once those exist -
the costs of the operating rules of a
NuclearUnitBlock, i.e., those of a downward modulation step and of a deep decrease, can change:set_down_modulation_costs()andset_deep_decrease_costs()do it, each in its Range and Subset form and both in the methods factory, and a change of the corresponding coefficients of the Objective is folded into them, so that the physical representation follows and the DP Solver hears of it. They used to be refused, which stopped any Solver that writes on the Objective of the unit, such as the primal proximal heuristic ofLagrangianDualSolverwith its penalty term -
ThermalUnitBlockreads the optional variableShutDownCost, the cost the unit pays at the instant it goes off, mapped over the time horizon asStartUpCostis and changed withset_shutdown_costs(). A unit that pays nothing to shut down keeps the vector empty and its Objective has no term for the shut-down variables, as before; the two DP Solvers charge the cost on the arc that closes an ON run -
the pollutant budget constraints of
UCBlock: for each zone of each pollutant, the emission of the electrical generators at the nodes of the zone summed over the whole time horizon, i.e., the conversion factorPollutantRho(which may depend on time, and includes the duration of the time instant) times the active power, lies betweenPollutantMinBudgetandPollutantBudget, the two matching a value when they are equal. The constraints are one vector with the zones of all the pollutants one after the other, as the budgets are in the file, they follow the scaling of the units, their duals are in theUCBlockSolution(bit 128), and the two budgets are changed withset_pollutant_budget()andset_pollutant_min_budget(); ifNumberPollutantZonesis not given every pollutant has one zone, and ifPollutantZonesis not given then all the nodes are in it. The constraints are grouped by pollutant, each with as many as its zones (get_pollutant_constraints()[ p ][ z ]), while the budgets and the duals keep the zones of all the pollutants one after the other, as the file does. They also take the levels of the storages into account (PollutantStorageRho, a factor on the level of each storage at each time, the storages of a unit being at the node of its first generator), which is how a limit that charges a storage for the change of its level is written; to this endUnitBlockhasget_number_storages()andget_storage_level(), returning 0 and nullptr unless redefined, asBatteryUnitBlock(its charge),HydroUnitBlock(its reservoirs) andHydroSystemUnitBlock(the reservoirs of all its units) do
-
BatteryUnitBlock::get_kappa_linearization()leaves out the converter rows when the converter has a kappa of its own -
ThermalUnitBlock::set_init_updown_time()after the generation of the Variable takes a value that keeps whether the unit is on before the horizon and the first instant at which it may switch (any two values not smaller thanMinUpTime, or any two not larger than-MinDownTime), the rows being written anew byupdate_rows(), and refuses any other one with the data kept, since it would make Variable appear or disappear -
DCNetworkBlockrefuses a negativeNetworkCost, when read and inset_network_cost(), since it makes the problem unbounded -
the documentation states the dual function of the version that relaxes the network rows, the nodal prices in the general PTDF formulation, the reactive multipliers of the Lagrangian dual, the derivations of the PTDF and CYCLE rows, of the battery binary rows and of the multistage cuts, and uses one notation in all the headers
-
the costs of the reserves of
ThermalUnitBlockare 0 when the data do not give them,PrimaryRhoandSecondaryRhobeing no price, in the Objective and in the DP Solvers; a reserve with cost 0 is in the Objective only if the objectiveConfiguration(bit 0 primary, bit 1 secondary) asks for it, and its cost cannot then become nonzero after the generation -
the reserve deliverability rows of
ThermalUnitBlockare also written at instant 0 for a unit on before the horizon, fromInitialPower, in every formulation and in the DP Solvers, and they followset_initial_power() -
the DP Solvers of
ThermalUnitBlockuse the operational power bounds,Availabilityincluded, and without ramp data a ramp no move reaches -
ThermalUnitBlock::set_maximum_power()andset_availability()update every formulation once the constraints are generated: the rows are written in one place,build_rows(), whichupdate_rows()runs again on the changed data and compares with the rows there are, changing coefficients and sides in one GroupModification,NuclearUnitBlockincluded; the new data are checked first (availability in [0, 1],MaxPowernot belowMinPower, a givenStartUpLimitorShutDownLimitwithin the operational bounds) and restored if the update fails;MaxRampUpStepsandMaxRampDownStepsin the data are refused;update_rows()refuses a change after which no row would be written to a static group that has rows (one written bysize_rows()orpush_row(), as the rows of a derived class are), or a row would be written to a dynamic group that is not generated -
the rows of
ThermalUnitBlockwhose number depends on the power limits are dynamic groups that the setters add to and remove from: the bound rows 5 and 6 of the maximum power of the T formulation (MaxPower5_Const_Thermal,MaxPower6_Const_Thermal) and the ramp rows of several steps of the SUSD formulation (RampUp_SUSD_Const_Thermal,RampDown_SUSD_Const_Thermal); the rows of the T formulation whose Variable depended on the data have them all, with coefficient 0 where the term vanishes -
a
ThermalUnitBlockon before the horizon shuts down at instant 0 only ifInitialPoweris at mostShutDownLimit[ 0 ], whatever the ramps, in every formulation and in both DP Solvers (withoutDeltaRampDownthe 3bin and T formulations andThermalUnitExtDPSolverallowed it for anyInitialPower): the interval ( 0 , 0 ) of the path formulations always exists and the rowShutDownZero_Const_Thermalfixes it (orw_0in 3bin and T withoutDeltaRampDown) to 0 when it is not allowed -
ThermalUnitBlock::set_initial_power()updates every formulation once the constraints are generated, and no longer throws whenInitialPowercrossesShutDownLimit[ 0 ]or the formulation is a path one with ramps -
a
StartUpLimitorShutDownLimitthat the data do not give is the operational minimum power also after a change of the availability, so that an unavailable unit cannot produce -
the reactive power of
ThermalUnitBlockis free in sign, and its bound rows, like those ofSlackUnitBlock, always exist with the reactive power, an absent bound being 0 -
the bounds on the node injections are given to the
NetworkBlockagain when the scale, the kappa or the maximum power of a unit changes, and they include kappa; the rows of the bounds follow them -
the KIRCHHOFF formulation of
DCNetworkBlockfixes one voltage angle in each component of the lines with nonzero susceptance -
the Tikhonov coefficient of
DCNetworkBlockis 0 also in the constructor and inDCNetworkData::get_PTDF(), andDCNetworkBlock::change_active_demand_constraints()is virtual -
DCNetworkData::deserialize()rejects aReferenceNodethat is not a node, and a network with hyperarcs and lines with nonzero susceptance -
a modulation of a
NuclearUnitBlockwithPowerBandscrosses exactly one band boundary, the steps before the last keeping the output in the band of origin, in the rows and inNuclearUnitExtDPSolver -
NuclearUnitBlock::set_initial_power()throws when the new initial power is in another band than the one the rows at instant 0 were built for -
ThermalUnitBlockrejects a negativeFixedConsumption, and its checks ofInitialPoweragainst the ramps at instant 0 throw once, with the name of the method -
HydroUnitBlockrequiresLinearTermwhen some arc is a turbine, requiresStartArcandEndArctogether (and with more than one reservoir), checks their range and self-loops, and rejects an arc withMinFlow< 0 <MaxFlowor that is a turbine at some instant and a pump at another -
HydroSystemUnitBlock::deserialize()rejects a concave future cost of the water -
BatteryUnitBlockrefuses a positiveMinPowerwhen intake and outtake are split, andserialize()writesConverterMaxPoweronly if it was given -
UCBlock::deserialize()refusesPrimaryZones,SecondaryZonesorInertiaZonesabsent with more than one zone of that kind, instead of reading out of bounds -
the
is_feasible()ofThermalUnitBlock,NuclearUnitBlock,HydroUnitBlock,HydroSystemUnitBlock,BatteryUnitBlock,IntermittentUnitBlock,SlackUnitBlock,DCNetworkBlock,DesignNetworkBlock,ECNetworkBlockandOTSNetworkBlockaccept the relative violationBlock::DefaultFeasTolof the core when no Configuration gives a tolerance, instead of none -
the data archive is downloaded by version:
DATA_VERSIONin CMakeLists.txt names the version of the Package Registry to read, and the archive and the marker of its extraction carry it in their name, so that a tree holding an older extraction (the cache of the CI, or a clone extracted before) downloads and extracts again instead of running on the old data; data/upload-nc4 publishes the archive under that version -
the folders of the instances have one sub-folder per kind of problem, and the converters write into the one of the problem they translate, an instance of one kind having landed among those of another
-
the fleet instances carry the rules that follow a start-up and a cycling copy of the load, so that the units they hold are exercised on the whole set of the operating rules and not on the part of it a flat load reaches
-
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
-
a
DCNetworkBlocktakes the KIRCHHOFF formulation where no Configuration says which one it wants, the PTDF one being both larger, since it carries a dense row per line, and the one whose flows a mixed network of AC lines and HVDC links used to get wrong; aSimpleConfiguration< int >of value 0 in the static-variables slot of theBlockConfigstill asks for the PTDF formulation, and one of value 1 for the CYCLE one. A network whose lines all have a zero susceptance is a transport model under either formulation -
a
UCBlockwhoseNumberElectricalGeneratorsis not the number of generators its units have is refused, instead of being read with the number the file states: everything indexed over the generators, fromGeneratorNodeto the emission rates, would be read over the wrong length, and the rows theUCBlockbuilds over them are sized with it, so that the model it gives depends on how far the two numbers are apart -
ThermalUnitBlockhas the new virtualupdate_objective_tail(), with which a derived class makes the coefficients it appends to the Objective follow the scale factor: what includes its header has to be rebuilt -
NuclearUnitBlockkeeps each family of operating rules in a group of its own (the tight rows on the starts of a modulation,ModulationStartsApart,ModulationEndStartsandModulationStepStarted, are no longer mixed withModulationStabilityandModulationMaxLength), and the groups whose number of rows depends on the instant (StartUpStability,BandKeep,BandMove,ModulationEndLink) have the rows of instant t in their entry t -
the setters that change one datum spanning the whole time horizon issue their "abstract" Modification inside a GroupModification, one per setter, rather than one loose Modification per instant: a Solver able to execute a whole set of changes in one operation can then do so, while one that is not takes the group apart and sees exactly what it saw before. So far
SlackUnitBlock::set_active_power_cost,HydroUnitBlock::set_inflow,HydroUnitBlock::update_initial_flow_rate_in_cnstrs,ThermalUnitBlock::set_maximum_power,IntermittentUnitBlock::update_max_power_in_cnstrsandset_kappa,BatteryUnitBlock::update_kappa_in_cnstrs,DCNetworkBlock::change_power_flow_limit_constraints, the four setters of the prices and of the demand ofECNetworkBlock, and the reaction ofUCBlockto the scaling of a unit, where the fourupdate_*_constraintsnow travel in one channel,DCNetworkBlock::set_network_cost, theset_active_power_costofHydroUnitBlockand ofIntermittentUnitBlock,IntermittentUnitBlock::update_objectiveandThermalUnitBlock::update_objective_active_power. The last two wereconst, which opening a channel is not: they are private helpers called only from methods that are not const, and the const is gone
-
the data check of a cyclic reservoir of
HydroUnitBlockasks the total inflow over the horizon to lie between the smallest and the largest total net outflow that the flow bounds allow, a necessary condition, instead of the inflow of each instant to fit the arcs leaving the reservoir, which refused feasible instances; it is made also for a single reservoir withoutStartArcandEndArc -
the dynamic programming Solvers of
ThermalUnitBlockno longer limit the move fromInitialPower, when there is noDeltaRampUp/DeltaRampDown, by the largestMaxPower: a unit on before the horizon withInitialPoweraboveMaxPower(e.g., one unavailable over the whole horizon) was declared infeasible, or given a power above its minimum at 0, while the formulations have no ramp row there -
the SUSD formulation of
ThermalUnitBlockwrites also the single-step ramp rows downwards on the runs by start-up and upwards on those by shut-down (RampDown_Const_Thermal,RampUp_Const_Thermal), as the SU and SD formulations do; without them its continuous relaxation could be weaker than that of the DP formulation -
the setters of the modulation ramps of
NuclearUnitBlockchange the rows throughupdate_rows(), so that the coefficient of the modulation in the ramp rows with the direction of a modulation, the rows of TightRamp and the rows (32) of TightRules follow the new data; a change that adds or removes rows (32) is refused with the data kept, an unorderedSubsetkeeps its values, and theRangeform ofset_modulation_ramp_down()issueseSetModDM -
NuclearUnitExtDPSolver::load_fixings()releases the read lock of the Block also when it throws -
DCNetworkBlockdoes not list "Kappa" among the expected netCDF names, since it does not read it -
the setters of
ThermalUnitBlockwith an unorderedSubsetsorted the indices but not the values, which went to the wrong instants (e.g.,set_maximum_power()with the values 60, 140 at the instants 4, 1 set 60 at 1): the pairs are sorted together -
NuclearUnitExtDPSolverrefuses a fixed band of the output, deep drop, deep low, modulation start or modulation end, whose fixings its labels cannot honor, instead of giving a schedule that may violate them, save the fixings at 0 of the first instant of a unit initially off;NuclearUnitBlockhas the accessors of the band, modulation start and modulation end Variable -
HydroUnitBlock::set_inflow()andset_initial_volume()add the initial volume to the water balance of instant 0 only when it is nonnegative, as the generation does: a cyclic reservoir no longer gets the negative marker in its balance after a change of its inflows -
a design variable of
BatteryUnitBlock,IntermittentUnitBlockorDesignNetworkBlockwhose two bounds are 1 is fixed to 1 by its bound row; it was only bounded in [ 0 , 1 ], so the asset could be left out -
the binary rows and the reserve bounds of a
BatteryUnitBlockwhose battery design may exceed one module capped its power and reserves at those of one module: the binary rows are written with the largest design, and each reserve is bounded by the reserve of the modules built, a row with the design variable (Primary_Design_Battery,Secondary_Design_Battery), besides the bound of the largest design; an integer design may have a minimum above 1 -
the cost of the secondary reserve of
ThermalUnitBlockset after the generation landed on the wrong variable when the unit pays to shut down -
the inertia and reserve rows of
UCBlockwere updated with wrong positions for a unit with generators at several nodes of a zone -
BatteryUnitBlockwith one instant and the cyclic level put the level twice in the row, and an infinite storage bound gave an infinite or NaN coefficient to the design variable (NaN bound with kappa 0) -
ACNetworkBlockgave the flows to the Solvers twice -
ThermalUnitBlocksized the rows fixing the commitment over the horizon instead of the instants it is fixed -
the PTDF and CYCLE formulations of
DCNetworkBlockimpose the balance of each component of the lines with nonzero susceptance, so that power does not move between components with no line carrying it, andis_feasible()checks these rows -
the PTDF formulation of
DCNetworkBlockkeeps the column of aReferenceNodethat is not the lowest node of its component -
DCNetworkData::get_PTDF()throws when the reduced susceptance matrix cannot be factorized, and without arguments it does not read the susceptances of a pure HVDC network out of bounds -
a change of the demand after generation updates the node balances of the CYCLE formulation of a pure HVDC network, does not write out of bounds in the KIRCHHOFF formulation of a mixed network, and updates the active balances of an
ACNetworkBlockinstead of crashing -
OTSNetworkBlockputs the cost of each flow in its Objective once -
DCNetworkBlock::get_dual_prices()gives a line with a design variable the sign of the dual of a bound -
a negative
UphillFlowofHydroUnitBlockdoes not remove the withdrawal of the arc from the water balance of its start reservoir -
the flow-to-power row of a single-piece turbine of
HydroUnitBlockis an equality only if a spillway with the same start and end reservoirs and the same delays can take its water at every instant, and the turbine has no positiveMinFlowand no ramp rows -
HydroUnitBlock::set_initial_volume()throws if it would switch the cyclic closure of a reservoir on or off after the constraints are generated -
a
HydroSystemUnitBlockwithout a PolyhedralFunctionBlock does not crash when its Variable are generated, andget_polyhedral_function_block()returns nullptr;get_hydro_unit_block()rejects the indexget_number_hydro_units()in debug builds -
a change of the
InertiaPowerof aHydroUnitBlock, also inside aHydroSystemUnitBlock, changes the coefficients of the inertia rows of theUCBlock; adding an inertia term to a unit that had none when the rows were generated throws -
with only
ExtractingBatteryRhogiven, the intake of aBatteryUnitBlockenters the storage balance with the sign of a charge -
BatteryUnitBlock::set_initial_storage()refuses, after the constraints are generated, a change of sign ofInitialStorage, andset_cost()with an unsorted Subset gives each value to the index it comes with -
ConverterMaxPowerbounds the converter of aBatteryUnitBlockalso when the battery has no design variable, if the datum is given, and a converter design variable with no battery design variable is in its rows -
an
IntermittentUnitBlockwith a design variable has the reserve rows, with the capacity of the design variable in them -
get_kappa_linearization()ofBatteryUnitBlockandIntermittentUnitBlockthrows with a design variable instead of returning a wrong value -
SlackUnitBlockdoes not readActivePowerCostout of range for the cost of the reactive power when the datum is absent, and does not throw in aUCBlockwith inertia zones when the unit has noMaxInertia -
the 3bin maximum power rows of
ThermalUnitBlockare counted and built right with a horizon of one instant or with the commitment fixed over the whole horizon, and withMinUpTime1 a start-up at instant 0 is capped byStartUpLimitand the last power before a shut-down by the shut-down limit not above the maximum power, as in the other formulations and in the DP Solvers -
in the DP formulation of
ThermalUnitBlocka run of one instant is capped by both the start-up and the shut-down limit -
the
InitialPowerof aThermalUnitBlockon before the horizon is raised toMinPower[0], also when the unit is unavailable at 0, before the numbers of ramp steps are derived from it; a value aboveMaxPower[0]gives a warning -
ThermalUnitBlock::set_initial_power()andset_init_updown_time()recompute the numbers of ramp steps from the initial power of the SUSD formulation -
a modulation of a
NuclearUnitBlockcut by the end of the horizon cannot start upwards from the highest band or downwards from the lowest one, so that the MILP agrees withNuclearUnitExtDPSolver -
the operating-rule rows of a
NuclearUnitBlockoff before the horizon ignoreInitialPower, andset_solution()does not end a modulation that covers a horizon shorter thanMaxModulationLength, and takes a full-ramp step at the last instant that leaves the band of origin as the last step of a modulation -
with a single primary, secondary or inertia zone, a node whose zone index is not smaller than the number of zones is in no zone, as with several zones
-
scaling a unit at a node in no reserve or inertia zone does not read and write past the rows of the zones
-
the
ConstantTermof aNetworkBlockgroup of aUCBlockis not overwritten whenNetworkConstantTermsis absent, and in the bus case the constant terms of the network are kept in the Objective of theUCBlock -
the overall balance of the PTDF formulation of DCNetworkBlock counts the losses of the HVDC lines, as the CYCLE one does: an HVDC line with an efficiency below 1 was forced to carry nothing, and a node reached only by such lines shed its whole demand
-
the CYCLE formulation of DCNetworkBlock handles parallel DC lines: the spanning forest and the fundamental cycles are built on the lines rather than on the pairs of nodes, so that of two parallel lines one is in the forest and the other closes a cycle with it, while both used to be taken as tree edges and forced to carry the whole flow between their nodes (on a PyPSA network with two parallel lines the optimum was 5.6e-3 too high); also, an HVDC line with an efficiency other than 1 enters the flow of a tree edge with the efficiency at its end node, as in the nodal balance
-
NuclearUnitExtDPSolverignored a modulation (or a downward modulation, or a deep decrease) fixed to 1 at an instant in which the unit is off, since the fixings of those Variable were only checked on the moves of an on unit: a modulation at t needs the unit on at t and not starting up there (m_t <= u_t and m_t <= 1 - v_t), which is now what such a fixing imposes, and the DP no longer goes below the MILP when the tester fixes the modulations (TUDPS_FIXMOD) -
BatteryUnitBlock::get_kappa_linearization()reads the binary rows with the right sizes, sums the power rows under a reserve, and counts the intake and outtake bounds only on their upper side -
the cost setters of a
ThermalUnitBlock(set_linear_term(),set_quad_term(),set_const_term(),set_startup_costs(),set_shutdown_costs(),set_reactive_linear_term()and those of the spinning reserves) write in the Objective the scale factor times the cost, as the Objective is generated. They wrote the cost of one copy, so that on a unit with aScaleother than 1 the Objective no longer agreed with the data after a change -
the minimum up (down) time of a
ThermalUnitBlockis bounded by the horizon plus the number of instants the unit has been on (off) before it, both when it is read and whenset_min_up_down_time()sets it, so that the unit stays as it is for as long as the minimum time says. The bound was the horizon plus one, which freed a unit that had been on (off) for more than one instant before the minimum time was over: a unit off since 3 instants with aMinDownTimeof 4 could start at the first instant of a horizon of one -
ThermalUnitDPSolver.hcan be included where FastFlow is not seen: the constructor, which needs the destructor of theff::ParallelForshould it throw, is defined in the.cppas the destructor is -
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 -
the scaling of a unit rewrites the rows of that unit also while a
LagrangianDualSolveris attached: the index of the unit was asked to its father, which is then theLagBFunctionholding the unit alone, so that every unit was taken for unit 0; the index is now looked up among the units of theUCBlock -
the reserve band of a unit that is on for a single period, i.e., starts up at t and shuts down at t+1, is capped by the smaller of the start-up and the shut-down limit in both
ThermalUnitDPSolverandThermalUnitExtDPSolver, in their solutions as in their values, and so is that of a unit that is on before the horizon and shuts down at the first instant; the cap was that of the start-up alone, and the two DPs could give a value below the optimum -
ThermalUnitExtDPSolverbuilds the solution with the shut-down cap only where it bites, i.e., where it is below the maximum power, and restricts the value function to the domain of the shut-down before adding its term at the first instant, which was read at a point where the band was still uncapped -
the formulations of
ThermalUnitBlockwith time-varying minimum and maximum power are exact: in the T formulation the ramp rows weigh the state of the previous instant with the minimum power of the right instant, every coefficient of the max-power rows is measured against the maximum power of the row, and a single on period is capped by the smaller of the start-up and shut-down limits (the sign test was inverted and took the larger); in the pt, DP, SU, SD and SUSD formulations every shut-down cap is the limit of the instant the unit shuts down at, and the start-up limit of the SD ramp-up no longer reads one instant past the horizon -
in the design MIP of a unit with reactive power, the off-bounds of the reactive power are multiplied by the design variable, so that a unit that is not built produces no reactive power, as in the DPs
-
ThermalUnitDPSolverwith time-varying minimum and maximum power no longer writes out of bounds in the energy sweep when the lower end of the domain lies more than a ramp-down below the previous one, and a domain that is empty or below the shut-down cap makes the run infeasible instead -
the dominance test of
ThermalUnitExtDPSolverbetween two value functions is linear in their pieces, and the profiling switches of the solver are read from the environment only whenTUEDPS_PROFILEis set -
ThermalUnitExtDPSolver(and soNuclearUnitExtDPSolver) reads the label of the initial state as an off-state throughshut_label()when the unit shuts down at the first instant or restarts from an initial off period, since the on-code of that label and the off-code a shut-down produces are different in general; the label was used as it was, i.e., as an on-code, and a unit whose initial state has no off-code does not take those arcs -
the PTDF matrix of a
DCNetworkBlockis no longer perturbed by a Tikhonov term on the diagonal (f_tikhonov_coeffis 0 by default), the reduced Laplacian being nonsingular once a reference node per connected component is removed. The perturbation broke the conservation of the flows: the PTDF rows, the nodal balance of the nodes an HVDC line touches and the overall balance are then no longer consistent, and together they pin a relation among the injections that has nothing to do with the network, so that on a network of both kinds of lines the PTDF formulation gave an optimum far from that of the KIRCHHOFF one, and could even declare the problem infeasible. The two now agree, on a network of 8 countries as on a triangle -
two lines that join the same two nodes sum their susceptances in the PTDF matrix, instead of the second one overwriting the entry of the first
-
ThermalUnitBlockaccepts aMinUpTime(MinDownTime) of one instant more than the horizon, which says that the unit, being on (off) before it, never switches within it: the commitment is then fixed at every instant and there is no start-up or shut-down variable. The bound was the horizon itself, so that the last instant was free however long the minimum time -
the step that fetches the data archive of this module says what went wrong when it goes wrong: the download is checked, an archive that did not arrive is removed instead of being left on disk for the build to take for the real one, and the message names the URL. A server that answers with an error page used to leave a file of a few bytes there, which made the next build fail while extracting it, with the message of
tarand no mention of the download -
scaling a
ThermalUnitBlockafter its Objective has been generated rewrites every term that carries the scale factor, i.e., also those of the shut-down, of the primary and secondary reserves, of the perspective cuts and of the reactive power, and in aNuclearUnitBlockthose of the downward modulation steps and of the deep decreases, which used to keep the old factor; the model of a unit scaled after being built is now the same as that of a unit scaled before -
a
BatteryUnitBlockreads theReferenceScheduleits file declares: the variable was among the ones it expects and all the machinery was there, the deviation variables, the rows and the term of the Objective, but the datum was never deserialized, so that the schedule of a battery was thrown away in silence while a thermal or a hydro unit followed the one it is given. The AC instances carry one on each of their 15 batteries, as they do on their 153 thermal units, hence the two kinds of unit now behave the same way -
a
BatteryUnitBlockthat follows a reference schedule refuses to have its kappa changed, i.e. to be resized: whether the profile a resized battery is asked to follow is the same one in absolute terms or one resized with it is not settled, and leaving the profile where it is would answer it in silence.get_reference_schedule()gives the schedule, empty where there is none -
the reactive node injection constraints of a
UCBlockcarry the reactive power of the generators and nothing else, as their documentation says: they used to carry the fixed consumption too, which is an ACTIVE power, on the commitment variables. Whether a unit that is off also absorbs reactive power is not settled; were it to, it would call for a datum of its own -
the fixed consumption of a unit that is off raises the right-hand side of the node injection constraints at a single node, as it already did with more than one node and as the setter of the demand already recomputed it: the generation lowered it instead, so that a unit consuming while off made the others generate less, and merely writing the demand back into the
UCBlockchanged the model. The value of the plan4res instances that carry a fixed consumption moves by 8 to 12%. The formula in the documentation, which wrote the consumption as a positive term of the injection, follows -
an
ACNetworkBlockand anOTSNetworkBlockrefuse a kappa on one of their lines,DCNetworkBlock::set_kappa()being virtual now and their override throwing: they build their own rows and not the power flow limit ones the kappa is written into, so that sizing a line of theirs used to reach rows that are not there. What supporting it would take is written where they refuse it. The method that writes the kappa into the rows refuses as well when there are none, so that any other derived class is told rather than left to write where there is nothing -
the dynamic programming Solvers refuse a unit that has a reference schedule, of which they have no term: they used to answer for a unit that pays nothing to depart from its schedule, i.e., a value that is not the one of the Objective, and silently.
ThermalUnitBlock::get_reference_schedule()gives the schedule, empty where there is none -
the deviation from the reference schedule of a
ThermalUnitBlockor of aBatteryUnitBlockis weighed with the scale factor, as every other term of their Objective: the schedule is that of one unit, from which each of the copies deviates on its own, so that the fleet pays the scale factor times what one copy pays; it used to be weighed with 1, so that the Objective of a scaled unit was neither the cost of one copy nor that of all of them. The guard that refuses a change of those coefficients now asks them to be the scale factor. AHydroUnitBlockhas no scale factor, hence its own deviation is unchanged, and the reference schedule of aBatteryUnitBlockwhose kappa changes is left as it is [seeset_kappa()] -
a change of the coefficients of the Objective of a
ThermalUnitBlock, as a dualizing Solver makes, is divided by the scale factor before being stored in the costs of the unit, which are those of one copy; they used to be stored as they are, i.e., multiplied by the scale factor, and a scaling made witheModBlckwent the same way through the start-up and fixed costs -
the dynamic programming Solvers of the
ThermalUnitBlockand of theNuclearUnitBlockreport the value of all the copies of the unit, i.e., the scale factor times that of the one copy they solve for, which is the value of the Objective; the Solution they produce is still that of one copy, as the Variable of the unit are -
the reactive node injection constraints of a
UCBlockfollow the scale factor of a unit as the active ones do, and a scaled unit with a fixed consumption at a single node gets the fixed consumption, and no longer the scale factor, as the coefficient of its commitment -
scaling a unit of a
UCBlockwith a single primary, secondary or inertia zone no longer reads the zone of its generators out of an empty vector, which crashed -
IntermittentUnitBlock::scale()tells theUCBlockeven when no Solver is listening, as the thermal unit and the battery do, so that the rows carrying the scale factor are rewritten anyway -
the ramps of a
ThermalUnitBlockthat change over time are read as the documentation says in every formulation and Solver:DeltaRampUp[ t ](DeltaRampDown[ t ]) bounds the increase (decrease) of the power from t - 1 to t, fromInitialPowerif t = 0. The 3bin and pt formulations, the reserve deliverability rows and the three dynamic programming Solvers usedDeltaRampUp[ t - 1 ]for that step, so thatDeltaRampUp[ 0 ]bound both the first two steps and the formulations disagreed with each other; the T formulation mixed the two indices in its ramp-down rows, and the formulations whose rows span several steps (SUSD, the bounds of T, the maximum powers of the interval formulations) multiplied one ramp by the number of steps instead of summing the ramps of the steps. With constant ramps nothing changes -
IntermittentUnitBlock::check_data_consistency()refused aMinCapacityDesignabove 1 whenMaxCapacityDesignis negative, as if the design were binary, while that design is an integer between 0 and|MaxCapacityDesign|(e.g., the number of modules of a modular asset): it now asks only thatMinCapacityDesignbe at most|MaxCapacityDesign| -
NuclearUnitBlock::set_solution()derived the start of a modulation and the three indicators of a deep decrease, and left the end of a modulation and the band of the output as they were, so that a Solver that writes the canonical Variable alone left them with the values of whoever had written them before: the schedule that came out then brokeModulationEndby one and, since the band rows are relaxed byMaxPower - MinPowerwhile a modulation travels,BandPowerby that much. Both are derived now, the band by a sweep over the three of them, since it only changes where a modulation ends and a band that holds the output at one instant may leave none at the next -
the band of a
NuclearUnitBlockcould move against the direction of the modulation that moves it, the rows asking only that it change by one: where the landing power is a breakpoint, and both bands hold it, a modulation upwards could therefore leave the unit a band lower, which costs nothing while a downward one is paid for. The move now follows the direction, as it does in the dynamic program, the two having disagreed by up to1.2e-4on the instances of the battery.has_modulation_direction()is true whenever the output is banded, the direction Variable being what says which way the band goes -
at the last instant of the horizon a
NuclearUnitBlockwith the bands could take a modulation step that was neither a full ramp nor landed in a band: the full ramp of a step is imposed through the modulation of the next instant, which is not there, while the end of the modulation, which the band rows are relaxed without, was free. The step was therefore a last one for the ramp and not for the band, and on one instance of the battery the unit kept a lower output for the whole day and then modulated by a fraction of the ramp at its end,5e-6below what the dynamic program, where a modulation either ends in a band or goes on by the full ramp, could reach. At the last instant the next modulation is now replaced by the one that goes on past the horizon, i.e.,m_{T-1} - e_{T-1}, a single-step modulation always ends there, andset_solution()marks as ended a modulation whose last step is not a full ramp -
the cuts that
ThermalUnitBlockandNuclearUnitBlockseparate carried a constant term of 1 in theirLinearFunction,eNoModhaving been passed to its constructor, where it is the constant, rather than toset_function(). TheMILPSolverreads the coefficients and not the constant, hence the rows it got were right, but every check done on theBlock,is_feasible()comprised, saw each separated cut violated by 1 at any solution -
ACNetworkBlockregistered"DCNetworkBlock::set_active_demand"again, with an adapter casting toACNetworkBlock, so that which of the two registrations survived depended on the order of the static initialization, and a call by name on aDCNetworkBlockcould go through a cast to a class it is not.DCNetworkBlockregisters it, and it reaches anACNetworkBlockas well -
DCNetworkBlockscaled its flow limits byC_v_scalonly in the box of the lines without a design variable, and lost the factor there too as soon asset_kappa()rewrote them: the rows of the lines with a design variable now carry it as well, and keep it throughset_kappa(). The default factor is 1, which is why nothing showed -
DCNetworkBlockgave a line with a design variable its lower row only when kappa times the minimum flow was not zero, so that a line starting with a zero kappa and a nonzero minimum flow was left with the lower half of the box, i.e., with a nonnegative flow, andset_kappa()had no row to write the new lower limit in: the row now depends on the minimum flow alone, as the documentation ofv_design_min_rowalready said -
BatteryUnitBlock::get_max_power_constraints()andget_max_outtake_binary_constraints()returned the second element of the first row of their two-row group rather than the first element of the second, so thatget_kappa_linearization()read, for every instant, the multiplier of the row of the instant after it, and the coefficient the InvestmentFunction gets for a battery under investment was that of the wrong rows. The coefficient is now also read off one side at a time: where kappa is 0 the two rows pin the variable and both multipliers can be nonzero on what is a single equality, and summing the two terms made the linearization steeper than the function it linearizes -
HydroUnitBlockgave an arc that is neither a turbine nor a pump at some instant (no flow allowed) a single flow-to-power row, but then went on as if it had one per piece: with more than one piece on that arc, the rows of the arcs after it at that instant took the linear and constant terms of other pieces.FlowActivePower_Constis now grouped by instant and arc, the rows of an arc being its own -
DCNetworkBlockhad a flow definition row for every line, and left those of the HVDC lines without a Function, whichis_feasible()cannot compute;ACNetworkBlockhad the two product-of-voltages variables for every line, and used them only on the DC lines;DesignNetworkBlockhad a bound for every design variable, and left empty those of the variables fixed to 1. They now have them only where they are used -
UCBlock::is_feasible()answered false on an optimal solution: it checked the sub-Block with no Configuration, i.e., with tolerance 0 whatever the tolerance of the UCBlock, andDCNetworkBlock::is_feasible()checked the overall balance constraint also in the formulations that do not generate it, where a constraint with no Function cannot be computed. The sub-Block ofUCBlock,DesignNetworkBlockandHydroSystemUnitBlockare now checked with the tolerance and type of violation of their father, unless their BlockConfig has a Configuration of its own, and the overall balance only where it exists -
SecondaryZones,InertiaZonesandNumberPollutantZoneswere only read if the demand of the previous family of constraints was in the file -
UCBlock::add_Modificationlooked for the scaling of a unit only at the first level of the Modification it is given: the scaling of several units can arrive inside one GroupModification, and there the rows that use the Variable of those units, i.e., the node injection and the demand ones, were left untouched
0.9.0 - 2026-09-13
-
the operating rules of a nuclear unit in the
NuclearUnitBlock: modulations of up toMaxModulationLengthsteps at the full ramp with the last one free, the stability ofModulationTimeinstants that follows a modulation and theStabilityAfterStartUpinstants that follow a start-up, the daily limits (DayLength,ModulationsPerDay,StartUpsPerDay,DeepDecreasesPerDay), the deep decreases (DeepDecreaseThreshold,DeepDecreaseGradient,DeepDecreaseCost), the cost of a downward modulation step (DownModulationCost) and the splitting of the output intoPowerBands, each written only when the data asks for it, so that a file of the previous model keeps its meaning -
a tight version of the rules, selected by two further bits of the same integer
Configurationthat chooses the formulation (TightRules,TightRampandTightCuts), the third of which gives the tight rows by separation, ingenerate_dynamic_constraints(), rather than writing them -
NuclearUnitExtDPSolverrewritten as a labelled run-length dynamic program over theThermalUnitExtDPSolver, which grows the hooks the labels need (on_moves(),shut_label(),idle_label(),start_labels(),label_dominates(),trim_domination())
0.8.0 - 2026-09-12
-
get_Solution()for the three dynamic programming Solver of the ThermalUnitBlock, which fill the ThermalUnitBlockSolution straight out of the schedule the DP has found, without writing anything into the Block and therefore without requiring any Variable to exist; the recovery of the schedule, which get_var_solution() shares, is factored intorecover_schedule() -
accessors and setters to the parts of the solution saved in
UnitBlockSolutionand to the dimensioning variable saved inThermalUnitBlockSolution -
the factor the flow limits of a DCNetworkBlock are scaled by is readable, since whoever reads their duals needs it
-
an auxiliary Variable, and the rows that fence it, are only generated when the data asks for them: the intake and the outtake level of the
BatteryUnitBlockwhen the storing and the extracting efficiency disagree at some time instant, the linearisation of the flow cost of theDCNetworkBlockand of theOTSNetworkBlockwhen some line is priced, the minimum power row of theIntermittentUnitBlockand the maximum and minimum power rows of theHydroUnitBlockwhen the unit produces reserve. Where the rows are not generated, what they reduce to is a bound on the active power, and it is stated as a bound -
the flow-to-power relation of a
HydroUnitBlockarc is an equality, rather than the concave outer approximation a piecewise arc needs, when the arc has a single piece with no constant term and the reservoir it leaves has a spillage outlet: the flow can then be substituted away -
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
-
UnitBlockSolution::write()threw on a generator having no Variable for a part the Solution carries, whileread()skips it: since what a Solution is made of is now decided by the data, that part can be there while the Variable are not, and writing on what is there is the only reading of it consistent with the other direction -
HydroSystemUnitBlocksaid it has the primary and the secondary reserve whenever the enclosing UCBlock asks for them, whatever the HydroUnitBlock it is made of have: it now answers for the units, which need not agree -
the dynamic programming Solver of the ThermalUnitBlock crashed on a unit whose power domain collapses to a single point at some time instant, say min_power == max_power: the sliding minimum dropped the zero-width piece, the value function came out empty, and the caller read that as no piece at all. The point is now emitted as a zero-width piece, which the rest of the machinery already handles, and a transition that really is infeasible closes the run instead of being walked into
-
the documentation of the formulations of the ThermalUnitBlock, which numbered the p_t one 3 and the dynamic programming one 2, the other way round with respect to ptForm and DPForm and to what set_variables() does
-
the intake and outtake bounds of a BatteryUnitBlock were held in a LB0Constraint, whose LHS is fixed at zero by the type: right for the pair of one-sided fences on the intake and the outtake, wrong once the pair is folded onto the signed active power, where the bound is two-sided and its charging side is negative. Generating the constraints of a battery in the folded form threw "cannot change LHS in a LB0Constraint"; they are now a BoxConstraint, which the split form uses with its default LHS of zero
0.7.0 - 2025-12-12
-
DesignNetworkBlockSolution
-
[big] DesignNetworkBlock allowing to do design of lines across the time horizon
-
simple stochastic case in csv2netcdf
-
constant term to UCBlock
-
design variables in Solution for BatteryUnitBlock, IntermittentUnitBlock, ThermalUnitBlock
-
MinCapacityDesign for IntermittentUnitBlock and BatteryUnitBlock
-
capacity expansion for Intermittent and Battery units
-
[big] "compact" NetworkBlockSolution to avoid issue with netCDF files being very slow when writing many small sub-group
-
[huge] hyperarcs in DCNetworkBlock, our quick-and-dirty solution to multi-energy modelling
-
[big] efficiency of lines in DCNetworkBlock
-
HydroUnitBlockSolution, BatteryUnitBlockSolution, HydroSystemUnitBlockSolution
-
cycling for hydro (volume at the end of the interval equal to that at the beginning)
-
add ActivePowerCost to HydroUnitBlock
-
Include LinearTerm to IntermittentUnitBlock
-
DCNetworkBlockSolution
-
UCBlockSolution, UnitBlockSolution, NetworkBlockSolution
-
type of ActivePower_Bound_Const for slack unit
-
allow negative IntermittentUB and negative loads
-
major data handling upgrade whereby instances are no longer included in the repo but can be downloaded
-
refactored messy HydroUnitBlock constraints building
-
checked the value of delta ramp-up/down
-
moved node injection bound constraints to NetworkBlock
-
UCBlock::set_active_power_demand methods
-
separated battery design from converter design in BatteryUnitBlock
-
IntakeOuttake_Design_Battery constraint in BatteryUnitBlock
-
Demand Battery constraints in BatteryUnitBlock
-
issue in secondary and inertia demand constraints
-
get_min_power() and get_max_power-89 in HydroUnitBlock
-
bug in DCNetworkBlock::change_power_flow_limit_constraints()
-
catastrophic blunder in NuclearUnitBlock
-
bugs when getting AC/DC lines
-
issue in HydroUnitBlock when having 0 pieces
-
is_cooperative in ECNB
-
cleaned up factory issue in NetworkBlock
-
objective in ECNB
-
many minor fixes
0.6.3 - 2024-02-29
-
design variables in ThermalUnitBlock, BatteryUnitBlock, IntermittentUnitBlock
-
tools/csv2netCDFfrom Energy Community Julia codebase -
data/nc4/EC_Datatest data sets -
ECNetworkBlock
-
adapted to new CMake / makefile organisation
-
NetworkBlock can now span multiple time instants
-
updated Julia and nc4 files with the stochastic logic
-
test/ moved to ThermalUnitBlock_Solver in test repository
-
useless test_package
-
bug in
ThermalUnitBlock::update_objective_start_up()in which thev_start_upvector was being accessed at a wrong index -
bugs when retrieving and checking constraints in BatteryUnitBlock
-
bug in ThermalUnitBlock::update_objective_start_up()
-
separation of Perspective Cuts in ThermalUnitBlock
-
the 3bin formulation including the start-up and shut-down limits cnstrs even if the ramp ones are not present
-
default value for f_MinDownTime
-
too many minor others to list
0.6.2 - 2023-05-17
-
NuclearUnitBlock (didactic)
-
is_feasible() to BatteryUnitBlock, SlackUnitBlock
-
IntermittentUnitBlock::set_BlockConfig()
-
updated is_feasible() in HydroUnitBlock, DCNetworkBlock, IntermittentUnitBlock, ThermalUnitBlock, and BatteryUnitBlock
-
removed "battery_type" from BatteryUnitBlock and check if negative prices may occur
0.6.1 - 2022-07-01
-
UnitBlock can be scaled (replicated)
-
BatteryUnitBlock, IntermittentUnitBlock, and ThermalUnitBlock implement scale()
-
BatteryUnitBlock and IntermittentUnitBlock can have their minimum and maximum power and storage levels scaled (set_kappa())
- improved UCBlock abstract constraints code
-
serialization of BatteryUnitBlock, HydroUnitBlock, IntermittentUnitBlock, NetworkBlock, ThermalUnitBlock, UCBlock
-
deserialization of DCNetworkBlock and NetworkBlock
0.6.0 - 2021-12-08
-
ThermalUnitDPSolver
-
Option to add spinning reserve variables to the Objective of ThermalUnitBlock
-
is_feasible() to UnitBlocks
-
bugs in deserialization
-
bug in the Objective of ThermalUnitBlock
-
bugs in Constraints
-
bugs in methods that are used to retrieve data
-
bugs in methods to change the physical representation
0.5.0 - 2021-05-02
- too many fixes to list
0.4.1 - 2020-09-16
- generation of abstract Constraint in UCBlock
0.4.0 - 2020-09-16
- support for Hydro[System]UnitBlock.
0.3.1 - 2020-07-17
- a bugged test file
0.3.0 - 2020-07-15
-
methods for changing data after deserialization
-
ThermalUnitBlock unit tests
-
all the UCBlock codes
- some getter methods for ThermalUnitBlock data
0.2.0 - 2020-03-06
- HydroSystemUnitBlock
- minor bugs
0.1.0 - 2020-02-06
- First test release.