From e228c41d3ae3f1f0a7bb282a88d244820fbd519d Mon Sep 17 00:00:00 2001 From: Davide Fioriti Date: Fri, 20 Feb 2026 01:24:58 +0100 Subject: [PATCH 1/3] Dumb tssb info --- pypsa2smspp/transformation.py | 244 +++++++++++++++++++++++++++++++++- 1 file changed, 243 insertions(+), 1 deletion(-) diff --git a/pypsa2smspp/transformation.py b/pypsa2smspp/transformation.py index 64bdca0..6a99ee7 100644 --- a/pypsa2smspp/transformation.py +++ b/pypsa2smspp/transformation.py @@ -13,7 +13,7 @@ import xarray as xr import os from pypsa2smspp.transformation_config import TransformationConfig -from pysmspp import SMSNetwork, SMSFileType, Variable, Block, SMSConfig +from pysmspp import SMSNetwork, SMSFileType, Attribute, Dimension, Variable, Block, SMSConfig from pypsa2smspp import logger from copy import deepcopy import pysmspp @@ -899,6 +899,15 @@ def convert_to_blocks(self): sn = SMSNetwork(file_type=SMSFileType.eBlockFile) master = sn index_id = 0 + + if False: # check if TSSB problem + name_id = 'TwoStageStochasticBlock' + sn = self.convert_to_twostagestochasticblock(master, index_id, name_id) + + # InnerBlock for UC is inside StochasticBlock + master = sn.blocks[name_id] + name_id = 'InnerBlock' + index_id += 1 # ----------------- # Check if investment problem @@ -926,6 +935,239 @@ def convert_to_blocks(self): self.sms_network = sn return sn + def convert_to_twostagestochasticblock(self, master, index_id, name_id): + """ + Adds a TwoStageStochasticBlock to the SMSNetwork, which is used for stochastic problems. + + Parameters + ---------- + master : SMSNetwork + The root SMSNetwork object + index_id : int + ID for block naming + name_id : str + Name for the TwoStageStochasticBlock + + Returns + ------- + SMSNetwork + The updated SMSNetwork with the TwoStageStochasticBlock added. + """ + + ScenarioSize = 48 # For DiscreteScenarioSet + NumberScenarios = 9 # For DiscreteScenarioSet + NumberDataMappings = NumberScenarios # for StochasticBlock + + PathDim = 5 # for AbstractPath + TotalLength = 10 # for AbstractPath + + SizeDim_perScenario = 2 # for StochBlock + SizeElements_perScenario = 4 # for StochBlock + PathDim2 = 9 # for AbstractPath in StochasticBlock + + pool_weights = ( + sn_benchmark.blocks["Block_0"] + .blocks["DiscreteScenarioSet"] + .variables["PoolWeights"] + .data + ) + + scenarios = ( + sn_benchmark.blocks["Block_0"] + .blocks["DiscreteScenarioSet"] + .variables["Scenarios"] + .data + ) + + path_element_indices = ( + sn_benchmark.blocks["Block_0"] + .blocks["StaticAbstractPath"] + .variables["PathElementIndices"] + .data + ) + + path_range_indices = ( + sn_benchmark.blocks["Block_0"] + .blocks["StaticAbstractPath"] + .variables["PathRangeIndices"] + .data + ) + + set_size = ( + sn_benchmark.blocks["Block_0"] + .blocks["StochasticBlock"] + .variables["SetSize"] + .data + ) + + set_elements = ( + sn_benchmark.blocks["Block_0"] + .blocks["StochasticBlock"] + .variables["SetElements"] + .data + ) + + sn.add( + "TwoStageStochasticBlock", + "Block_0", + id="0", + NumberScenarios=NumberScenarios, + DiscreteScenarioSet=Block( + block_type="DiscreteScenarioSet", + ScenarioSize=ScenarioSize, + NumberScenarios=NumberScenarios, + Scenarios=Variable( + "Scenarios", + "double", + ("NumberScenarios", "ScenarioSize"), + scenarios, + ), + PoolWeights=Variable( + "PoolWeights", + "double", + ("NumberScenarios",), + pool_weights, + ), + ), + StaticAbstractPath=Block( + PathDim=Dimension("PathDim", PathDim), + TotalLength=Dimension("TotalLength", TotalLength), + PathElementIndices=Variable( + "PathElementIndices", + "u4", + ("TotalLength",), + path_element_indices, # important to have missing values! only ones does not work + ), + PathGroupIndices=Variable( + "PathGroupIndices", + "str", + ("TotalLength",), + np.array( + [ + "0", + "x_thermal", + "1", + "x_intermittent", + "2", + "x_battery", + "2", + "x_converter", + "3", + "x_intermittent", + ], + dtype="object", + ), + ), + PathNodeTypes=Variable( + "PathNodeTypes", + "c", + ("TotalLength",), + np.tile(["B", "V"], TotalLength // 2), + ), + PathRangeIndices=Variable( + "PathRangeIndices", + "u4", + ("TotalLength",), + path_range_indices, # important to have missing values! only ones does not work + ), + PathStart=Variable( + "PathStart", + "u4", + ("PathDim",), + np.array(range(0, 10, 2), dtype=np.uint32), # ignored missing values + ), + ), + StochasticBlock=Block( + block_type="StochasticBlock", + NumberDataMappings=NumberDataMappings, + SetSize_dim=SizeDim_perScenario * NumberDataMappings, + SetElements_dim=SizeElements_perScenario * NumberDataMappings, + FunctionName=Variable( + "FunctionName", + "str", + ("NumberDataMappings",), + np.repeat( + np.array(["UCBlock::set_active_power_demand"], dtype="object"), + NumberDataMappings, + ), + ), + Caller=Variable( + "Caller", + "c", + ("NumberDataMappings",), + np.repeat(["B"], NumberDataMappings), + ), + DataType=Variable( + "DataType", + "c", + ("NumberDataMappings",), + np.repeat(["D"], NumberDataMappings), + ), + SetSize=Variable( + "SetSize", + "u4", + ("SetSize_dim",), + set_size, + ), + SetElements=Variable( + "SetElements", + "u4", + ("SetElements_dim",), + set_elements, + ), + AbstractPath=Block( + PathDim=Dimension("PathDim", PathDim2), + TotalLength=Dimension("TotalLength", 0), # Unlimited + PathGroupIndices=Variable( + "PathGroupIndices", + "str", + ("TotalLength",), + np.array([], dtype="object"), + ), + PathElementIndices=Variable( + "PathElementIndices", + "u4", + ("TotalLength",), + [], # ignored missing values (masked array) + ), + PathRangeIndices=Variable( + "PathRangeIndices", + "u4", + ("TotalLength",), + [], # ignored missing values + ), + PathStart=Variable( + "PathStart", + "u4", + ("PathDim",), + np.repeat([0], PathDim2), # ignored missing values + ), + PathNodeTypes=Variable("PathNodeTypes", "c", ("TotalLength",), []), + ), + Block=Block( + id=Attribute("id", "0"), + filename=Attribute("filename", "EC_CO_Test_TUB.nc4[0]"), + ), + ), + ) + + # ----------------- + # TwoStageStochasticBlock dimensions (currently empty, but can be extended) + # ----------------- + kwargs = self.dimensions.get('TwoStageStochasticBlock', {}) + + # ----------------- + # Add TwoStageStochasticBlock itself + # ----------------- + master.add( + "TwoStageStochasticBlock", + name_id, + id=f"{index_id}", + **kwargs + ) + + return master + def convert_to_investmentblock(self, master, index_id, name_id): """ Adds an InvestmentBlock to the SMSNetwork, including the From 3a2c71fd1d08c844070b8d401cbbe34084b09a3f Mon Sep 17 00:00:00 2001 From: Davide Fioriti Date: Fri, 27 Feb 2026 17:45:29 +0100 Subject: [PATCH 2/3] add test stochastic --- test/conftest.py | 3 ++ test/networks/pypsa_stoch_load.nc | Bin 0 -> 108829 bytes test/test_stochastic.py | 70 ++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 test/networks/pypsa_stoch_load.nc create mode 100644 test/test_stochastic.py diff --git a/test/conftest.py b/test/conftest.py index bf8b953..7809b6f 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -64,3 +64,6 @@ def get_test_cases(inputs_dir = HERE / "configs" / "data" / "test"): test_cases = get_test_cases() +def get_network(fp: Path | str) -> str: + """Helper to load a network from a .nc file.""" + return str(Path(HERE) / "networks" / fp) diff --git a/test/networks/pypsa_stoch_load.nc b/test/networks/pypsa_stoch_load.nc new file mode 100644 index 0000000000000000000000000000000000000000..b215352ecbfb9673e7d9bb1404528cedbbc81ff2 GIT binary patch literal 108829 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 None: + """ + UCBlock regression test: + - build network from Excel + - solve reference with PyPSA + - run full SMS++ pipeline in one call (config-driven) + - compare objectives + """ + + n = pypsa.Network(fp) + + # Work on a copy for reference solve + network = n.copy() + + # ---- (1) PyPSA optimization (reference) ---- + network.optimize(solver_name="highs") + + try: + obj_pypsa = float(network.objective + getattr(network, "objective_constant", 0.0)) + except Exception: + obj_pypsa = float(network.objective) + + # ---- (2) SMS++ pipeline (ONE CALL) ---- + transformation = Transformation() + n = transformation.run(network, verbose=False) + + obj_smspp = float(transformation.result.objective_value) + + # If you want to ensure UCBlock is used, either: + # (a) enforce run.mode: ucblock in the YAML used here, OR + # (b) if you expose transformation.last_mode_used, check it: + # + # if hasattr(transformation, "last_mode_used") and transformation.last_mode_used != "ucblock": + # pytest.skip(f"Not UCBlock mode for this case (mode={transformation.last_mode_used}).") + + assert obj_smspp == pytest.approx(obj_pypsa, rel=REL_TOL, abs=ABS_TOL) + + +def test_stochastic_network(fp=get_network("pypsa_stoch_load.nc")): + """ + Uses a dedicated YAML config that forces UCBlock mode (recommended). + """ + run_tssb(fp) + + +if __name__ == "__main__": + test_stochastic_network() From 0f1838591d82f66ddc9a3e2e6d55404fcd4e6c5a Mon Sep 17 00:00:00 2001 From: Davide Fioriti Date: Fri, 27 Feb 2026 21:52:44 +0100 Subject: [PATCH 3/3] Improve structure of tssb parts --- pypsa2smspp/transformation.py | 378 +++++++++++++++++++--------------- 1 file changed, 208 insertions(+), 170 deletions(-) diff --git a/pypsa2smspp/transformation.py b/pypsa2smspp/transformation.py index 6a99ee7..bb48877 100644 --- a/pypsa2smspp/transformation.py +++ b/pypsa2smspp/transformation.py @@ -934,36 +934,16 @@ def convert_to_blocks(self): # Save final self.sms_network = sn return sn - - def convert_to_twostagestochasticblock(self, master, index_id, name_id): - """ - Adds a TwoStageStochasticBlock to the SMSNetwork, which is used for stochastic problems. - - Parameters - ---------- - master : SMSNetwork - The root SMSNetwork object - index_id : int - ID for block naming - name_id : str - Name for the TwoStageStochasticBlock - - Returns - ------- - SMSNetwork - The updated SMSNetwork with the TwoStageStochasticBlock added. - """ - ScenarioSize = 48 # For DiscreteScenarioSet - NumberScenarios = 9 # For DiscreteScenarioSet - NumberDataMappings = NumberScenarios # for StochasticBlock - PathDim = 5 # for AbstractPath - TotalLength = 10 # for AbstractPath - - SizeDim_perScenario = 2 # for StochBlock - SizeElements_perScenario = 4 # for StochBlock - PathDim2 = 9 # for AbstractPath in StochasticBlock + def build_tssb_scenario_set(self): + """ + Build a DiscreteScenarioSet for a TSSB (two-stage stochastic block) structure. + This helper loads a benchmark network from ``fp_tssb`` and extracts the scenario + data from the DiscreteScenarioSet block to create a new in-memory scenario set. + """ + # TODO: this shall be completely revised to create the scenario set from the data of the StochasticNetwork, not from a benchmark file. The current implementation is just a placeholder. Demand shall be aggregated by node and time and scenarios shall be created from the aggregated demand profiles. pool_weights shall be created from the probabilities of the scenarios. + sn_benchmark = SMSNetwork(fp_tssb) pool_weights = ( sn_benchmark.blocks["Block_0"] @@ -979,176 +959,234 @@ def convert_to_twostagestochasticblock(self, master, index_id, name_id): .data ) - path_element_indices = ( - sn_benchmark.blocks["Block_0"] - .blocks["StaticAbstractPath"] - .variables["PathElementIndices"] - .data - ) + ScenarioSize = scenarios.shape[1] + NumberScenarios = scenarios.shape[0] - path_range_indices = ( - sn_benchmark.blocks["Block_0"] - .blocks["StaticAbstractPath"] - .variables["PathRangeIndices"] - .data + dds_block = Block( + block_type="DiscreteScenarioSet", + ScenarioSize=ScenarioSize, + NumberScenarios=NumberScenarios, + Scenarios=Variable( + "Scenarios", + "double", + ("NumberScenarios", "ScenarioSize"), + scenarios, + ), + PoolWeights=Variable( + "PoolWeights", + "double", + ("NumberScenarios",), + pool_weights, + ), ) - set_size = ( - sn_benchmark.blocks["Block_0"] - .blocks["StochasticBlock"] - .variables["SetSize"] - .data - ) + return dds_block - set_elements = ( - sn_benchmark.blocks["Block_0"] - .blocks["StochasticBlock"] - .variables["SetElements"] - .data + + def build_tssb_abstract_path(self): + """ + Build an AbstractPath for a TSSB (two-stage stochastic block) structure. + """ + # TODO: extract this from unit types depending on expandability, accounting for x_battery/x_converter. For ThermalUnitBlocks, x_thermal is mapped and similarly for the other units + variables = [ + "x_thermal", + "x_intermittent", + "x_battery", + "x_converter", + "x_intermittent", + ] + locations = ["0", "1", "2", "2", "3"] + + path_group_indices = np.array( + [str(item) for pair in zip(locations, variables) for item in pair], + dtype="object", ) - sn.add( - "TwoStageStochasticBlock", - "Block_0", - id="0", - NumberScenarios=NumberScenarios, - DiscreteScenarioSet=Block( - block_type="DiscreteScenarioSet", - ScenarioSize=ScenarioSize, - NumberScenarios=NumberScenarios, - Scenarios=Variable( - "Scenarios", - "double", - ("NumberScenarios", "ScenarioSize"), - scenarios, - ), - PoolWeights=Variable( - "PoolWeights", - "double", - ("NumberScenarios",), - pool_weights, + path_node_types = np.tile(["B", "V"], len(variables)) + + TotalLength = len(variables) * 2 + PathDim = len(variables) # for AbstractPath + + def mask_by_node_type(arr, path_node_types): + return np.ma.masked_array(arr, mask=path_node_types == "B") + + path_element_indices = mask_by_node_type(np.zeros(TotalLength), path_node_types) + path_range_indices = mask_by_node_type(np.ones(TotalLength), path_node_types) + + abstract_path_block = Block( + PathDim=Dimension("PathDim", PathDim), + TotalLength=Dimension("TotalLength", TotalLength), + PathElementIndices=Variable( + "PathElementIndices", + "u4", + ("TotalLength",), + path_element_indices, # important to have missing values! only ones does not work + ), + PathGroupIndices=Variable( + "PathGroupIndices", + "str", + ("TotalLength",), + np.array( + path_group_indices, + dtype="object", ), ), - StaticAbstractPath=Block( - PathDim=Dimension("PathDim", PathDim), - TotalLength=Dimension("TotalLength", TotalLength), - PathElementIndices=Variable( - "PathElementIndices", - "u4", - ("TotalLength",), - path_element_indices, # important to have missing values! only ones does not work + PathNodeTypes=Variable( + "PathNodeTypes", + "c", + ("TotalLength",), + path_node_types, + ), + PathRangeIndices=Variable( + "PathRangeIndices", + "u4", + ("TotalLength",), + path_range_indices, # important to have missing values! only ones does not work + ), + PathStart=Variable( + "PathStart", + "u4", + ("PathDim",), + np.arange(0, TotalLength, 2, dtype=np.uint32), # ignored missing values + ), + ) + + return abstract_path_block + + + def build_tssb_stochastic_block(self, TimeHorizon=24, NumberNodes=2, block=None): + """ + Build a StochasticBlock for a TSSB (two-stage stochastic block) structure. + """ + # TODO: this requires some minimal adaptations to properly link the required inputs with the input network. Moreover, the additional link is to properly link "block" that it will become the ucblock populated in the following steps. The current implementation is just a placeholder with dummy values. + NumberDataMappings = 1 # only demand suppored for now + + set_size_demand = [0, 0] + set_elements_demand = [0, TimeHorizon * NumberNodes, 0, TimeHorizon * NumberNodes] + function_name_demand = ["UCBlock::set_active_power_demand"] + + caller = ["B"] # The caller is a Block + caller_type = ["D"] + block_location = [0] # U CBlock + + set_size = np.array(set_size_demand, dtype=np.uint32) + set_elements = np.array(set_elements_demand, dtype=np.uint32) + + NumberDataMappings = set_size.shape[0] // 2 + SetSize_dim = set_size.shape[0] + SetElements_dim = set_elements.shape[0] + + if block is None: + block = Block( + id=Attribute("id", "0"), + filename=Attribute("filename", "EC_CO_Test_TUB.nc4[0]"), + ) + + stochastic_block = Block( + block_type="StochasticBlock", + NumberDataMappings=NumberDataMappings, + SetSize_dim=SetSize_dim, + SetElements_dim=SetElements_dim, + FunctionName=Variable( + "FunctionName", + "str", + ("NumberDataMappings",), + np.repeat( + np.array(function_name_demand, dtype="object"), + NumberDataMappings, ), + ), + Caller=Variable( + "Caller", + "c", + ("NumberDataMappings",), + np.array(caller, dtype="object"), + ), + DataType=Variable( + "DataType", + "c", + ("NumberDataMappings",), + np.array(caller_type, dtype="object"), + ), + SetSize=Variable( + "SetSize", + "u4", + ("SetSize_dim",), + set_size, + ), + SetElements=Variable( + "SetElements", + "u4", + ("SetElements_dim",), + set_elements, + ), + AbstractPath=Block( + PathDim=Dimension("PathDim", len(block_location)), + TotalLength=Dimension("TotalLength", 0), PathGroupIndices=Variable( "PathGroupIndices", "str", ("TotalLength",), - np.array( - [ - "0", - "x_thermal", - "1", - "x_intermittent", - "2", - "x_battery", - "2", - "x_converter", - "3", - "x_intermittent", - ], - dtype="object", - ), + np.array([], dtype="object"), ), - PathNodeTypes=Variable( - "PathNodeTypes", - "c", + PathElementIndices=Variable( + "PathElementIndices", + "u4", ("TotalLength",), - np.tile(["B", "V"], TotalLength // 2), + [], # ignored missing values (masked array) ), PathRangeIndices=Variable( "PathRangeIndices", "u4", ("TotalLength",), - path_range_indices, # important to have missing values! only ones does not work + [], # ignored missing values ), PathStart=Variable( "PathStart", "u4", ("PathDim",), - np.array(range(0, 10, 2), dtype=np.uint32), # ignored missing values + np.array(block_location, dtype=np.uint32), ), + PathNodeTypes=Variable("PathNodeTypes", "c", ("TotalLength",), []), ), - StochasticBlock=Block( - block_type="StochasticBlock", - NumberDataMappings=NumberDataMappings, - SetSize_dim=SizeDim_perScenario * NumberDataMappings, - SetElements_dim=SizeElements_perScenario * NumberDataMappings, - FunctionName=Variable( - "FunctionName", - "str", - ("NumberDataMappings",), - np.repeat( - np.array(["UCBlock::set_active_power_demand"], dtype="object"), - NumberDataMappings, - ), - ), - Caller=Variable( - "Caller", - "c", - ("NumberDataMappings",), - np.repeat(["B"], NumberDataMappings), - ), - DataType=Variable( - "DataType", - "c", - ("NumberDataMappings",), - np.repeat(["D"], NumberDataMappings), - ), - SetSize=Variable( - "SetSize", - "u4", - ("SetSize_dim",), - set_size, - ), - SetElements=Variable( - "SetElements", - "u4", - ("SetElements_dim",), - set_elements, - ), - AbstractPath=Block( - PathDim=Dimension("PathDim", PathDim2), - TotalLength=Dimension("TotalLength", 0), # Unlimited - PathGroupIndices=Variable( - "PathGroupIndices", - "str", - ("TotalLength",), - np.array([], dtype="object"), - ), - PathElementIndices=Variable( - "PathElementIndices", - "u4", - ("TotalLength",), - [], # ignored missing values (masked array) - ), - PathRangeIndices=Variable( - "PathRangeIndices", - "u4", - ("TotalLength",), - [], # ignored missing values - ), - PathStart=Variable( - "PathStart", - "u4", - ("PathDim",), - np.repeat([0], PathDim2), # ignored missing values - ), - PathNodeTypes=Variable("PathNodeTypes", "c", ("TotalLength",), []), - ), - Block=Block( - id=Attribute("id", "0"), - filename=Attribute("filename", "EC_CO_Test_TUB.nc4[0]"), - ), + Block=block, + ) + + return stochastic_block + + + def convert_to_twostagestochasticblock(self, master, index_id, name_id): + """ + Adds a TwoStageStochasticBlock to the SMSNetwork, which is used for stochastic problems. + + Parameters + ---------- + master : SMSNetwork + The root SMSNetwork object + index_id : int + ID for block naming + name_id : str + Name for the TwoStageStochasticBlock + + Returns + ------- + SMSNetwork + The updated SMSNetwork with the TwoStageStochasticBlock added. + """ + + dds = self.build_tssb_scenario_set() + abstract_path = self.build_tssb_abstract_path() + stochastic_block = self.build_tssb_stochastic_block() + master.add( + "TwoStageStochasticBlock", + "Block_0", + id="0", + NumberScenarios=Dimension( + "NumberScenarios", dds.dimensions["NumberScenarios"].value ), + DiscreteScenarioSet=dds, + StaticAbstractPath=abstract_path, + StochasticBlock=stochastic_block, ) # -----------------