From 738c0994b633df7350b935cdccd7dc0b32809ffd Mon Sep 17 00:00:00 2001 From: isaac_gumbrell Date: Fri, 20 Mar 2026 14:20:20 +0800 Subject: [PATCH 1/6] feat: Add aggregate_line_capacities function and integrate into complex clearing - Implemented aggregate_line_capacities function to compute forward and reverse capacities for lines based on directional columns or aggregated by node pairs. - Updated complex_clearing.py to utilize precomputed directional capacities in market clearing constraints. - Enhanced market clearing optimization to accept directional capacities as an argument. - Added example scenario to showcase and test directional constraint functionality. --- assume/common/utils.py | 124 ++++++++++++++ .../clearing_algorithms/complex_clearing.py | 72 +++++++-- examples/inputs/example_01j/buses.csv | 4 + examples/inputs/example_01j/buses.csv.license | 3 + examples/inputs/example_01j/config.yaml | 152 ++++++++++++++++++ examples/inputs/example_01j/demand_df.csv | 97 +++++++++++ .../inputs/example_01j/demand_df.csv.license | 3 + examples/inputs/example_01j/demand_units.csv | 4 + .../example_01j/demand_units.csv.license | 3 + .../inputs/example_01j/fuel_prices_df.csv | 2 + .../example_01j/fuel_prices_df.csv.license | 3 + examples/inputs/example_01j/lines.csv | 4 + examples/inputs/example_01j/lines.csv.license | 3 + .../inputs/example_01j/powerplant_units.csv | 51 ++++++ .../example_01j/powerplant_units.csv.license | 3 + 15 files changed, 514 insertions(+), 14 deletions(-) create mode 100644 examples/inputs/example_01j/buses.csv create mode 100644 examples/inputs/example_01j/buses.csv.license create mode 100644 examples/inputs/example_01j/config.yaml create mode 100644 examples/inputs/example_01j/demand_df.csv create mode 100644 examples/inputs/example_01j/demand_df.csv.license create mode 100644 examples/inputs/example_01j/demand_units.csv create mode 100644 examples/inputs/example_01j/demand_units.csv.license create mode 100644 examples/inputs/example_01j/fuel_prices_df.csv create mode 100644 examples/inputs/example_01j/fuel_prices_df.csv.license create mode 100644 examples/inputs/example_01j/lines.csv create mode 100644 examples/inputs/example_01j/lines.csv.license create mode 100644 examples/inputs/example_01j/powerplant_units.csv create mode 100644 examples/inputs/example_01j/powerplant_units.csv.license diff --git a/assume/common/utils.py b/assume/common/utils.py index 8c6cbe8ea..f64db0da4 100644 --- a/assume/common/utils.py +++ b/assume/common/utils.py @@ -550,6 +550,130 @@ def create_incidence_matrix(lines, buses, zones_id=None): return incidence_matrix +def aggregate_line_capacities( + lines: pd.DataFrame, + incidence_matrix: pd.DataFrame, + zones_id: str = None, + node_mapping: dict = None, +) -> pd.DataFrame: + """ + Compute forward and reverse capacities for each line (or aggregated edge) used + by the transport-model clearing (`complex_clearing`). + + The function returns a DataFrame indexed by the columns of `incidence_matrix` + with columns `cap_forward` and `cap_reverse` (absolute MW). If a column in + `incidence_matrix` matches a line index in `lines`, the capacities are taken + from that physical line. Otherwise the function attempts a zone-pair style + aggregation: physical lines are mapped to node pairs using `node_mapping` + (or by treating buses as nodes), and capacities are summed per zone-pair. + + Directional columns in `lines` take precedence: + - `s_nom_forward` used for forward (bus0 -> bus1) + - `s_nom_reverse` used for reverse (bus1 -> bus0) + If missing, fallback to `s_nom * s_max_pu` for that direction. + + Args: + lines: DataFrame of lines (indexed by line id). + incidence_matrix: Incidence matrix whose columns identify edges/lines. + zones_id: Optional zones identifier (unused here, kept for API compatibility). + node_mapping: Optional mapping from bus id -> node/zone id. + + Returns: + pd.DataFrame: indexed by `incidence_matrix.columns` with columns + ['cap_forward', 'cap_reverse']. + """ + + # prepare defaults for each physical line + per_line_caps = {} + for line_idx, line in lines.iterrows(): + s_max_pu = ( + lines.at[line_idx, "s_max_pu"] + if "s_max_pu" in lines.columns + and not pd.isna(lines.at[line_idx, "s_max_pu"]) + else 1.0 + ) + default_capacity = lines.at[line_idx, "s_nom"] * s_max_pu + + if "s_nom_forward" in lines.columns and not pd.isna( + lines.at[line_idx, "s_nom_forward"] + ): + cap_f = lines.at[line_idx, "s_nom_forward"] + else: + cap_f = default_capacity + + if "s_nom_reverse" in lines.columns and not pd.isna( + lines.at[line_idx, "s_nom_reverse"] + ): + cap_r = lines.at[line_idx, "s_nom_reverse"] + else: + cap_r = default_capacity + + per_line_caps[line_idx] = { + "cap_forward": float(cap_f), + "cap_reverse": float(cap_r), + } + + # If all incidence columns directly match physical lines, return per-line caps + cols = list(incidence_matrix.columns) + if all(col in per_line_caps for col in cols): + df = pd.DataFrame.from_dict(per_line_caps, orient="index") + # Ensure ordering matches incidence_matrix.columns + return df.reindex(cols) + + # Otherwise, attempt to aggregate by node-pair keys (zone-pair aggregation) + # Build mapping from physical line -> node pair key + if node_mapping is None: + # identity mapping: bus id -> bus id + node_mapping = {} + for _, row in lines.iterrows(): + node_mapping[row["bus0"]] = row["bus0"] + node_mapping[row["bus1"]] = row["bus1"] + + agg_caps = {col: {"cap_forward": 0.0, "cap_reverse": 0.0} for col in cols} + + for line_idx, line in lines.iterrows(): + bus0 = line["bus0"] + bus1 = line["bus1"] + node0 = node_mapping.get(bus0, bus0) + node1 = node_mapping.get(bus1, bus1) + + # Determine forward/reverse capacities for this physical line + caps = per_line_caps[line_idx] + + # Try matching a column that corresponds to the node0->node1 direction + key_f = f"{node0}_{node1}" + key_r = f"{node1}_{node0}" + + if key_f in agg_caps: + agg_caps[key_f]["cap_forward"] += caps["cap_forward"] + agg_caps[key_f]["cap_reverse"] += caps["cap_reverse"] + elif key_r in agg_caps: + # If the aggregated column uses reversed ordering, still add capacities + agg_caps[key_r]["cap_forward"] += caps["cap_forward"] + agg_caps[key_r]["cap_reverse"] += caps["cap_reverse"] + else: + # final fallback: if no matching aggregated key, try to add to any column + # that contains either node name (best-effort) + matched = False + for col in cols: + if str(node0) in str(col) and str(node1) in str(col): + agg_caps[col]["cap_forward"] += caps["cap_forward"] + agg_caps[col]["cap_reverse"] += caps["cap_reverse"] + matched = True + break + if not matched: + # give up and skip mapping this physical line + logger.debug( + f"aggregate_line_capacities: could not map line {line_idx} to incidence column" + ) + + df = pd.DataFrame.from_dict(agg_caps, orient="index") + # ensure numeric types + df["cap_forward"] = df["cap_forward"].astype(float) + df["cap_reverse"] = df["cap_reverse"].astype(float) + return df + + def normalize_availability(powerplants_df, availability_df): # Create a copy of the availability dataframe to avoid modifying the original normalized_df = availability_df.copy() diff --git a/assume/markets/clearing_algorithms/complex_clearing.py b/assume/markets/clearing_algorithms/complex_clearing.py index 7e81e6a76..6fcbbda59 100644 --- a/assume/markets/clearing_algorithms/complex_clearing.py +++ b/assume/markets/clearing_algorithms/complex_clearing.py @@ -12,7 +12,11 @@ from pyomo.opt import OptSolver, SolverFactory, TerminationCondition from assume.common.market_objects import MarketConfig, MarketProduct, Orderbook -from assume.common.utils import create_incidence_matrix, get_supported_solver_pyomo +from assume.common.utils import ( + aggregate_line_capacities, + create_incidence_matrix, + get_supported_solver_pyomo, +) from assume.markets.base_market import MarketRole # Set the log level to WARNING @@ -32,6 +36,7 @@ def market_clearing_opt_constraints( with_linked_bids: bool, incidence_matrix: pd.DataFrame, lines: pd.DataFrame, + directional_capacities: pd.DataFrame = None, ): """ Adds the constraints to the model. @@ -184,17 +189,27 @@ def energy_balance_rule(model, node, t): model.transmission_constr = pyo.ConstraintList() for t in model.T: for line in model.lines: - # s_max_pu might also be time variant. but for now we assume it is static - s_max_pu = ( - lines.at[line, "s_max_pu"] - if "s_max_pu" in lines.columns - and not pd.isna(lines.at[line, "s_max_pu"]) - else 1.0 - ) - capacity = lines.at[line, "s_nom"] * s_max_pu - # Limit the flow on each line - model.transmission_constr.add(model.flows[t, line] <= capacity) - model.transmission_constr.add(model.flows[t, line] >= -capacity) + # If precomputed directional capacities are provided, use them + if ( + directional_capacities is not None + and line in directional_capacities.index + ): + cap_forward = directional_capacities.at[line, "cap_forward"] + cap_reverse = directional_capacities.at[line, "cap_reverse"] + model.transmission_constr.add(model.flows[t, line] <= cap_forward) + model.transmission_constr.add(model.flows[t, line] >= -cap_reverse) + else: + # s_max_pu might also be time variant. but for now we assume it is static + s_max_pu = ( + lines.at[line, "s_max_pu"] + if "s_max_pu" in lines.columns + and not pd.isna(lines.at[line, "s_max_pu"]) + else 1.0 + ) + capacity = lines.at[line, "s_nom"] * s_max_pu + # Limit the flow on each line (symmetric fallback) + model.transmission_constr.add(model.flows[t, line] <= capacity) + model.transmission_constr.add(model.flows[t, line] >= -capacity) def market_clearing_opt_objective(model: pyo.ConcreteModel, orders: Orderbook): @@ -220,6 +235,7 @@ def market_clearing_opt( with_linked_bids: bool, incidence_matrix: pd.DataFrame = None, lines: pd.DataFrame = None, + directional_capacities: pd.DataFrame = None, solver: OptSolver = None, solver_options: dict = {}, func_constraints=market_clearing_opt_constraints, @@ -267,7 +283,14 @@ def market_clearing_opt( model = pyo.ConcreteModel() func_constraints( - model, orders, market_products, mode, with_linked_bids, incidence_matrix, lines + model, + orders, + market_products, + mode, + with_linked_bids, + incidence_matrix, + lines, + directional_capacities, ) func_objective(model, orders) @@ -382,7 +405,27 @@ def __init__(self, marketconfig: MarketConfig): # Nodal Case self.incidence_matrix = create_incidence_matrix(self.lines, buses) self.nodes = buses.index.values - + # Pre-compute directional capacities for use in the clearing constraints + try: + self.directional_capacities = aggregate_line_capacities( + self.lines, + self.incidence_matrix, + zones_id=self.zones_id, + node_mapping=self.node_to_zone, + ) + except Exception: + self.directional_capacities = None + + # Informational log if input contains directional columns + if self.lines is not None: + has_directional = ( + "s_nom_forward" in self.lines.columns + or "s_nom_reverse" in self.lines.columns + ) + if has_directional: + logger.info( + "Directional NTC columns detected in lines data. Asymmetric transfer limits will be applied." + ) self.log_flows = self.marketconfig.param_dict.get("log_flows", False) self.pricing_mechanism = self.marketconfig.param_dict.get( "pricing_mechanism", "pay_as_clear" @@ -518,6 +561,7 @@ def clear( with_linked_bids=with_linked_bids, incidence_matrix=self.incidence_matrix, lines=self.lines, + directional_capacities=getattr(self, "directional_capacities", None), solver=self.solver, solver_options=self.solver_options, ) diff --git a/examples/inputs/example_01j/buses.csv b/examples/inputs/example_01j/buses.csv new file mode 100644 index 000000000..b20b73ba6 --- /dev/null +++ b/examples/inputs/example_01j/buses.csv @@ -0,0 +1,4 @@ +name,v_nom,zone_id,x,y +north_1,380.0,north_1,10.0,54.0 +north_2,380.0,north_2,9.5,53.5 +south,380.0,south,11.6,48.1 diff --git a/examples/inputs/example_01j/buses.csv.license b/examples/inputs/example_01j/buses.csv.license new file mode 100644 index 000000000..a6ae06366 --- /dev/null +++ b/examples/inputs/example_01j/buses.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/examples/inputs/example_01j/config.yaml b/examples/inputs/example_01j/config.yaml new file mode 100644 index 000000000..40e6718f8 --- /dev/null +++ b/examples/inputs/example_01j/config.yaml @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: ASSUME Developers +# +# SPDX-License-Identifier: AGPL-3.0-or-later + +base: + start_date: 2019-01-01 00:00 + end_date: 2019-01-04 00:00 + time_step: 1h + save_frequency_hours: 24 + + markets_config: + EOM: + start_date: 2019-01-01 00:00 + operator: EOM_operator + product_type: energy + products: + - duration: 1h + count: 24 + first_delivery: 24h + opening_frequency: 24h + opening_duration: 20h + volume_unit: MWh + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: complex_clearing + additional_fields: + - node + - max_power + param_dict: + network_path: . + solver: highs + zones_identifier: zone_id + pricing_mechanism: pay_as_clear + log_flows: true + + redispatch: + start_date: 2019-01-01 21:00 + operator: network_operator + product_type: energy + products: + - duration: 1h + count: 24 + first_delivery: 3h + opening_frequency: 24h + opening_duration: 2h + volume_unit: MWh + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: redispatch + additional_fields: + - node + - min_power + - max_power + param_dict: + network_path: . + solver: highs + payment_mechanism: pay_as_bid + backup_marginal_cost: 10000 + +zonal_case: + start_date: 2019-01-01 00:00 + end_date: 2019-01-06 00:00 + time_step: 1h + save_frequency_hours: 24 + + markets_config: + DAM: + operator: EOM_operator + product_type: energy + products: + - duration: 1h + count: 24 + first_delivery: 24h + opening_frequency: 24h + opening_duration: 24h + volume_unit: MWh + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: complex_clearing + param_dict: + network_path: . + solver: highs + zones_identifier: zone_id + pricing_mechanism: pay_as_clear + additional_fields: + - bid_type + - node + +nodal_case: + start_date: 2019-01-01 00:00 + end_date: 2019-01-04 00:00 + time_step: 1h + save_frequency_hours: 24 + + markets_config: + nodal: + start_date: 2019-01-01 00:00 + operator: EOM_operator + product_type: energy + products: + - duration: 1h + count: 24 + first_delivery: 1h + opening_frequency: 24h + opening_duration: 1h + volume_unit: MWh + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: nodal_clearing + additional_fields: + - node + - max_power + param_dict: + network_path: . + solver: highs + zones_identifier: zone_id + pricing_mechanism: pay_as_clear + log_flows: true + + redispatch: + start_date: 2019-01-01 21:00 + operator: network_operator + product_type: energy + products: + - duration: 1h + count: 24 + first_delivery: 3h + opening_frequency: 24h + opening_duration: 2h + volume_unit: MWh + maximum_bid_volume: 100000 + maximum_bid_price: 3000 + minimum_bid_price: -500 + price_unit: EUR/MWh + market_mechanism: redispatch + additional_fields: + - node + - min_power + - max_power + param_dict: + network_path: . + solver: highs + payment_mechanism: pay_as_bid + backup_marginal_cost: 10000 diff --git a/examples/inputs/example_01j/demand_df.csv b/examples/inputs/example_01j/demand_df.csv new file mode 100644 index 000000000..642e494a1 --- /dev/null +++ b/examples/inputs/example_01j/demand_df.csv @@ -0,0 +1,97 @@ +datetime,demand_north_1,demand_north_2,demand_east,demand_west,demand_south +2019-01-01 00:00:00,2400.0,2400.0,480.0,11600.0,17400.0 +2019-01-01 01:00:00,2800.0,2800.0,560.0,11200.0,16800.0 +2019-01-01 02:00:00,3200.0,3200.0,640.0,10800.0,16200.0 +2019-01-01 03:00:00,3600.0,3600.0,720.0,10400.0,15600.0 +2019-01-01 04:00:00,4000.0,4000.0,800.0,10000.0,15000.0 +2019-01-01 05:00:00,4400.0,4400.0,880.0,9600.0,14400.0 +2019-01-01 06:00:00,4800.0,4800.0,960.0,9200.0,13800.0 +2019-01-01 07:00:00,5200.0,5200.0,1040.0,8800.0,13200.0 +2019-01-01 08:00:00,5600.0,5600.0,1120.0,8400.0,12600.0 +2019-01-01 09:00:00,6000.0,6000.0,1200.0,8000.0,12000.0 +2019-01-01 10:00:00,6400.0,6400.0,1280.0,7600.0,11400.0 +2019-01-01 11:00:00,6800.0,6800.0,1360.0,7200.0,10800.0 +2019-01-01 12:00:00,7200.0,7200.0,1440.0,6800.0,10200.0 +2019-01-01 13:00:00,7600.0,7600.0,1520.0,6400.0,9600.0 +2019-01-01 14:00:00,8000.0,8000.0,1600.0,6000.0,9000.0 +2019-01-01 15:00:00,8400.0,8400.0,1680.0,5600.0,8400.0 +2019-01-01 16:00:00,8800.0,8800.0,1760.0,5200.0,7800.0 +2019-01-01 17:00:00,9200.0,9200.0,1840.0,4800.0,7200.0 +2019-01-01 18:00:00,9600.0,9600.0,1920.0,4400.0,6600.0 +2019-01-01 19:00:00,10000.0,10000.0,2000.0,4000.0,6000.0 +2019-01-01 20:00:00,10400.0,10400.0,2080.0,3600.0,5400.0 +2019-01-01 21:00:00,10800.0,10800.0,2160.0,3200.0,4800.0 +2019-01-01 22:00:00,11200.0,11200.0,2240.0,2800.0,4200.0 +2019-01-01 23:00:00,11600.0,11600.0,2320.0,2400.0,3600.0 +2019-01-02 00:00:00,2400.0,2400.0,480.0,11600.0,17400.0 +2019-01-02 01:00:00,2800.0,2800.0,560.0,11200.0,16800.0 +2019-01-02 02:00:00,3200.0,3200.0,640.0,10800.0,16200.0 +2019-01-02 03:00:00,3600.0,3600.0,720.0,10400.0,15600.0 +2019-01-02 04:00:00,4000.0,4000.0,800.0,10000.0,15000.0 +2019-01-02 05:00:00,4400.0,4400.0,880.0,9600.0,14400.0 +2019-01-02 06:00:00,4800.0,4800.0,960.0,9200.0,13800.0 +2019-01-02 07:00:00,5200.0,5200.0,1040.0,8800.0,13200.0 +2019-01-02 08:00:00,5600.0,5600.0,1120.0,8400.0,12600.0 +2019-01-02 09:00:00,6000.0,6000.0,1200.0,8000.0,12000.0 +2019-01-02 10:00:00,6400.0,6400.0,1280.0,7600.0,11400.0 +2019-01-02 11:00:00,6800.0,6800.0,1360.0,7200.0,10800.0 +2019-01-02 12:00:00,7200.0,7200.0,1440.0,6800.0,10200.0 +2019-01-02 13:00:00,7600.0,7600.0,1520.0,6400.0,9600.0 +2019-01-02 14:00:00,8000.0,8000.0,1600.0,6000.0,9000.0 +2019-01-02 15:00:00,8400.0,8400.0,1680.0,5600.0,8400.0 +2019-01-02 16:00:00,8800.0,8800.0,1760.0,5200.0,7800.0 +2019-01-02 17:00:00,9200.0,9200.0,1840.0,4800.0,7200.0 +2019-01-02 18:00:00,9600.0,9600.0,1920.0,4400.0,6600.0 +2019-01-02 19:00:00,10000.0,10000.0,2000.0,4000.0,6000.0 +2019-01-02 20:00:00,10400.0,10400.0,2080.0,3600.0,5400.0 +2019-01-02 21:00:00,10800.0,10800.0,2160.0,3200.0,4800.0 +2019-01-02 22:00:00,11200.0,11200.0,2240.0,2800.0,4200.0 +2019-01-02 23:00:00,11600.0,11600.0,2320.0,2400.0,3600.0 +2019-01-03 00:00:00,2400.0,2400.0,480.0,11600.0,17400.0 +2019-01-03 01:00:00,2800.0,2800.0,560.0,11200.0,16800.0 +2019-01-03 02:00:00,3200.0,3200.0,640.0,10800.0,16200.0 +2019-01-03 03:00:00,3600.0,3600.0,720.0,10400.0,15600.0 +2019-01-03 04:00:00,4000.0,4000.0,800.0,10000.0,15000.0 +2019-01-03 05:00:00,4400.0,4400.0,880.0,9600.0,14400.0 +2019-01-03 06:00:00,4800.0,4800.0,960.0,9200.0,13800.0 +2019-01-03 07:00:00,5200.0,5200.0,1040.0,8800.0,13200.0 +2019-01-03 08:00:00,5600.0,5600.0,1120.0,8400.0,12600.0 +2019-01-03 09:00:00,6000.0,6000.0,1200.0,8000.0,12000.0 +2019-01-03 10:00:00,6400.0,6400.0,1280.0,7600.0,11400.0 +2019-01-03 11:00:00,6800.0,6800.0,1360.0,7200.0,10800.0 +2019-01-03 12:00:00,7200.0,7200.0,1440.0,6800.0,10200.0 +2019-01-03 13:00:00,7600.0,7600.0,1520.0,6400.0,9600.0 +2019-01-03 14:00:00,8000.0,8000.0,1600.0,6000.0,9000.0 +2019-01-03 15:00:00,8400.0,8400.0,1680.0,5600.0,8400.0 +2019-01-03 16:00:00,8800.0,8800.0,1760.0,5200.0,7800.0 +2019-01-03 17:00:00,9200.0,9200.0,1840.0,4800.0,7200.0 +2019-01-03 18:00:00,9600.0,9600.0,1920.0,4400.0,6600.0 +2019-01-03 19:00:00,10000.0,10000.0,2000.0,4000.0,6000.0 +2019-01-03 20:00:00,10400.0,10400.0,2080.0,3600.0,5400.0 +2019-01-03 21:00:00,10800.0,10800.0,2160.0,3200.0,4800.0 +2019-01-03 22:00:00,11200.0,11200.0,2240.0,2800.0,4200.0 +2019-01-03 23:00:00,11600.0,11600.0,2320.0,2400.0,3600.0 +2019-01-04 00:00:00,2400.0,2400.0,480.0,11600.0,17400.0 +2019-01-04 01:00:00,2800.0,2800.0,560.0,11200.0,16800.0 +2019-01-04 02:00:00,3200.0,3200.0,640.0,10800.0,16200.0 +2019-01-04 03:00:00,3600.0,3600.0,720.0,10400.0,15600.0 +2019-01-04 04:00:00,4000.0,4000.0,800.0,10000.0,15000.0 +2019-01-04 05:00:00,4400.0,4400.0,880.0,9600.0,14400.0 +2019-01-04 06:00:00,4800.0,4800.0,960.0,9200.0,13800.0 +2019-01-04 07:00:00,5200.0,5200.0,1040.0,8800.0,13200.0 +2019-01-04 08:00:00,5600.0,5600.0,1120.0,8400.0,12600.0 +2019-01-04 09:00:00,6000.0,6000.0,1200.0,8000.0,12000.0 +2019-01-04 10:00:00,6400.0,6400.0,1280.0,7600.0,11400.0 +2019-01-04 11:00:00,6800.0,6800.0,1360.0,7200.0,10800.0 +2019-01-04 12:00:00,7200.0,7200.0,1440.0,6800.0,10200.0 +2019-01-04 13:00:00,7600.0,7600.0,1520.0,6400.0,9600.0 +2019-01-04 14:00:00,8000.0,8000.0,1600.0,6000.0,9000.0 +2019-01-04 15:00:00,8400.0,8400.0,1680.0,5600.0,8400.0 +2019-01-04 16:00:00,8800.0,8800.0,1760.0,5200.0,7800.0 +2019-01-04 17:00:00,9200.0,9200.0,1840.0,4800.0,7200.0 +2019-01-04 18:00:00,9600.0,9600.0,1920.0,4400.0,6600.0 +2019-01-04 19:00:00,10000.0,10000.0,2000.0,4000.0,6000.0 +2019-01-04 20:00:00,10400.0,10400.0,2080.0,3600.0,5400.0 +2019-01-04 21:00:00,10800.0,10800.0,2160.0,3200.0,4800.0 +2019-01-04 22:00:00,11200.0,11200.0,2240.0,2800.0,4200.0 +2019-01-04 23:00:00,11600.0,11600.0,2320.0,2400.0,3600.0 diff --git a/examples/inputs/example_01j/demand_df.csv.license b/examples/inputs/example_01j/demand_df.csv.license new file mode 100644 index 000000000..a6ae06366 --- /dev/null +++ b/examples/inputs/example_01j/demand_df.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/examples/inputs/example_01j/demand_units.csv b/examples/inputs/example_01j/demand_units.csv new file mode 100644 index 000000000..bf427b668 --- /dev/null +++ b/examples/inputs/example_01j/demand_units.csv @@ -0,0 +1,4 @@ +name,technology,bidding_EOM,bidding_redispatch,bidding_nodal,bidding_DAM,max_power,min_power,unit_operator,node +demand_north_1,inflex_demand,demand_energy_naive,demand_energy_naive_redispatch,demand_energy_naive,demand_energy_naive,200000,0,eom_de,north_1 +demand_north_2,inflex_demand,demand_energy_naive,demand_energy_naive_redispatch,demand_energy_naive,demand_energy_naive,100000,0,eom_de,north_2 +demand_south,inflex_demand,demand_energy_naive,demand_energy_naive_redispatch,demand_energy_naive,demand_energy_naive,200000,0,eom_de,south diff --git a/examples/inputs/example_01j/demand_units.csv.license b/examples/inputs/example_01j/demand_units.csv.license new file mode 100644 index 000000000..a6ae06366 --- /dev/null +++ b/examples/inputs/example_01j/demand_units.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/examples/inputs/example_01j/fuel_prices_df.csv b/examples/inputs/example_01j/fuel_prices_df.csv new file mode 100644 index 000000000..7a273ea7c --- /dev/null +++ b/examples/inputs/example_01j/fuel_prices_df.csv @@ -0,0 +1,2 @@ +fuel,uranium,lignite,hard coal,natural gas,oil,biomass,co2 +price,0,2,10,25,40,20,25 diff --git a/examples/inputs/example_01j/fuel_prices_df.csv.license b/examples/inputs/example_01j/fuel_prices_df.csv.license new file mode 100644 index 000000000..a6ae06366 --- /dev/null +++ b/examples/inputs/example_01j/fuel_prices_df.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/examples/inputs/example_01j/lines.csv b/examples/inputs/example_01j/lines.csv new file mode 100644 index 000000000..c34a0525e --- /dev/null +++ b/examples/inputs/example_01j/lines.csv @@ -0,0 +1,4 @@ +line,bus0,bus1,s_nom,s_max_pu,s_nom_forward,s_nom_reverse,x,r +Line_N_S_1,north_1,south,5000.0,1,5000.0,3000.0,0.01,0.001 +Line_N_S_2,north_2,south,5000.0,1,4500.0,3500.0,0.01,0.001 +Line_N_N,north_1,north_2,5000.0,1,4000.0,3000.0,0.01,0.001 diff --git a/examples/inputs/example_01j/lines.csv.license b/examples/inputs/example_01j/lines.csv.license new file mode 100644 index 000000000..a6ae06366 --- /dev/null +++ b/examples/inputs/example_01j/lines.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/examples/inputs/example_01j/powerplant_units.csv b/examples/inputs/example_01j/powerplant_units.csv new file mode 100644 index 000000000..b3c5a7289 --- /dev/null +++ b/examples/inputs/example_01j/powerplant_units.csv @@ -0,0 +1,51 @@ +name,technology,bidding_EOM,bidding_redispatch,bidding_nodal,bidding_DAM,fuel_type,emission_factor,max_power,min_power,efficiency,additional_cost,node,unit_operator +Unit 1,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,5,north_1,Operator 1 +Unit 2,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,6,north_1,Operator 1 +Unit 3,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,7,north_1,Operator 1 +Unit 4,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,8,north_1,Operator 1 +Unit 5,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,9,north_1,Operator 1 +Unit 6,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,10,north_1,Operator 1 +Unit 7,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,11,north_1,Operator 1 +Unit 8,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,12,north_1,Operator 1 +Unit 9,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,13,north_1,Operator 1 +Unit 10,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,14,north_1,Operator 1 +Unit 11,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,15,north_2,Operator 2 +Unit 12,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,16,north_2,Operator 2 +Unit 13,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,17,north_2,Operator 2 +Unit 14,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,18,north_2,Operator 2 +Unit 15,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,19,north_2,Operator 2 +Unit 16,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,20,north_2,Operator 2 +Unit 17,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,21,north_2,Operator 2 +Unit 18,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,22,north_2,Operator 2 +Unit 19,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,23,north_2,Operator 2 +Unit 20,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,24,north_2,Operator 2 +Unit 21,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,25,south,Operator 3 +Unit 22,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,26,south,Operator 3 +Unit 23,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,27,south,Operator 3 +Unit 24,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,28,south,Operator 3 +Unit 25,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,29,south,Operator 3 +Unit 26,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,30,south,Operator 4 +Unit 27,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,31,south,Operator 4 +Unit 28,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,32,south,Operator 4 +Unit 29,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,33,south,Operator 4 +Unit 30,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,34,south,Operator 4 +Unit 31,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,35,south,Operator 4 +Unit 32,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,36,south,Operator 4 +Unit 33,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,37,south,Operator 4 +Unit 34,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,38,south,Operator 4 +Unit 35,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,39,south,Operator 4 +Unit 36,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,40,south,Operator 4 +Unit 37,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,41,north_1,Operator 5 +Unit 38,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,42,north_1,Operator 5 +Unit 39,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,43,north_1,Operator 5 +Unit 40,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,44,north_1,Operator 5 +Unit 41,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,45,north_1,Operator 5 +Unit 42,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,46,north_1,Operator 5 +Unit 43,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,47,north_1,Operator 5 +Unit 44,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,48,north_1,Operator 5 +Unit 45,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,49,north_1,Operator 5 +Unit 46,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,50,north_1,Operator 5 +Unit 47,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,51,north_1,Operator 5 +Unit 48,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,52,north_1,Operator 5 +Unit 49,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,53,north_1,Operator 5 +Unit 50,nuclear,powerplant_energy_naive,powerplant_energy_naive_redispatch,powerplant_energy_naive,powerplant_energy_heuristic_block,uranium,0.0,1000.0,0,0.3,54,north_1,Operator 5 diff --git a/examples/inputs/example_01j/powerplant_units.csv.license b/examples/inputs/example_01j/powerplant_units.csv.license new file mode 100644 index 000000000..a6ae06366 --- /dev/null +++ b/examples/inputs/example_01j/powerplant_units.csv.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: ASSUME Developers + +SPDX-License-Identifier: AGPL-3.0-or-later From fd836b9d82f5c950db585ac937a0ac09ca5a9327 Mon Sep 17 00:00:00 2001 From: "Gumbrell, Isaac" Date: Mon, 25 May 2026 11:47:33 +0800 Subject: [PATCH 2/6] docs(clearing): document directional capacities --- docs/source/example_simulations.rst | 3 +++ docs/source/market_mechanism.rst | 8 +++++++- docs/source/release_notes.rst | 1 + 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/source/example_simulations.rst b/docs/source/example_simulations.rst index fac0c55d4..59ece55f4 100644 --- a/docs/source/example_simulations.rst +++ b/docs/source/example_simulations.rst @@ -50,6 +50,9 @@ Overview of Example Simulations * - small_with_zonal_clearing‡ - example_01d - Implements zonal market clearing. + * - small_with_directional_zonal_clearing‡ + - example_01j + - Demonstrates complex clearing with directional transfer capacities between zones. * - market_study_eom - example_01f - Showcases comparison of single market to multi market. Case 1 in [3]_ diff --git a/docs/source/market_mechanism.rst b/docs/source/market_mechanism.rst index 9e599210f..2d276c25f 100644 --- a/docs/source/market_mechanism.rst +++ b/docs/source/market_mechanism.rst @@ -79,7 +79,13 @@ is given by: :math:`\mathbf{a}_{c, p} \: u_c \leq u_{p} \quad \forall \: c, p \i with the incidence matrix :math:`\mathbf{a}_{c, p}` defining the links between bids as 1, if c is linked as child to p, 0 else. -Flows in the network are limited by the Net Transfer Capacity ('s_nom' * 's_max_pu') of each line l: :math:`\quad -NTC_{l} \leq F_{l, t} \leq NTC_{l} \quad \forall \: l \in \mathcal{L}, t \in \mathcal{T}`, +Flows in the network are limited by the Net Transfer Capacity of each line :math:`l`. By default ASSUME derives a symmetric limit from ``s_nom * s_max_pu`` and applies it in both directions: + +.. math:: + + -NTC_{l} \leq F_{l, t} \leq NTC_{l} \quad \forall \: l \in \mathcal{L}, t \in \mathcal{T} + +If ``lines.csv`` additionally provides ``s_nom_forward`` and/or ``s_nom_reverse``, the complex clearing uses these directional limits instead. This allows transport constraints to differ by flow direction, which is useful for zonal representations with asymmetric commercial transfer capacities. Because with this algorithm, paradoxically accepted bids (PABs) can occur, the objective is solved in an iterative manner: diff --git a/docs/source/release_notes.rst b/docs/source/release_notes.rst index 50ced9f0f..f74ad49e4 100644 --- a/docs/source/release_notes.rst +++ b/docs/source/release_notes.rst @@ -15,6 +15,7 @@ Upcoming Release **New Features:** - **Generic Forecasting Interface**: This interface enables to specify different forecast algorithms for preprocess, initialization and update during runtime. They can be specified in the config.yaml or unit csv files. For more information about currently implemented algorithms and how to specify them please read the documentation on Unit forecasts. + - **Directional transfer capacities in complex clearing**: ``complex_clearing`` can now use asymmetric line limits from ``s_nom_forward`` and ``s_nom_reverse`` instead of assuming the same transfer capacity in both directions. **Improvements:** - **In complex clearing, the solver instance is now created once during initialization of the clearing role and reused for each market clearing**. This improves performance for e.g. year-long simulations. From a6ea4189d730134b5b63d4aa9951688902b01629 Mon Sep 17 00:00:00 2001 From: gugrimm <155527339+gugrimm@users.noreply.github.com> Date: Wed, 27 May 2026 17:41:47 +0200 Subject: [PATCH 3/6] add test_aggregate_line_capacities to test_utils --- tests/test_utils.py | 72 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/tests/test_utils.py b/tests/test_utils.py index f76fc50b7..c8241787d 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -17,6 +17,7 @@ from assume.common.fast_pandas import FastIndex, FastSeries from assume.common.market_objects import MarketConfig, MarketProduct from assume.common.utils import ( + aggregate_line_capacities, aggregate_step_amount, convert_to_rrule_freq, datetime2timestamp, @@ -847,6 +848,74 @@ def test_load_index_file(): assert df is None +def test_aggregate_line_capacities_direct_match(): + lines = pd.DataFrame( + { + "bus0": ["B1", "B2"], + "bus1": ["B2", "B3"], + "s_nom": [100.0, 200.0], + "s_max_pu": [1.0, 0.8], + "s_nom_forward": [None, 180.0], + "s_nom_reverse": [None, 190.0] + }, + index=["L1", "L2"] + ) + + incidence_matrix = pd.DataFrame(index=["B1", "B2", "B3"], columns=["L1", "L2"]) + + result = aggregate_line_capacities(lines, incidence_matrix) + + assert "cap_forward" in result.columns + assert "cap_reverse" in result.columns + assert list(result.index) == ["L1", "L2"] + + assert result.at["L1", "cap_forward"] == 100.0 + assert result.at["L1", "cap_reverse"] == 100.0 + + assert result.at["L2", "cap_forward"] == 180.0 + assert result.at["L2", "cap_reverse"] == 190.0 + + +def test_aggregate_line_capacities_aggregation(): + lines = pd.DataFrame( + { + "bus0": ["B1", "B2", "B3"], + "bus1": ["B2", "B4", "B4"], + "s_nom": [100.0, 50.0, 50.0], + }, + index=["L1", "L2", "L3"] + ) + + node_mapping = {"B1": "Z1", "B2": "Z1", "B3": "Z2", "B4": "Z2"} + + incidence_matrix = pd.DataFrame(index=["Z1", "Z2"], columns=["Z1_Z1", "Z1_Z2", "Z2_Z2"]) + + result = aggregate_line_capacities(lines, incidence_matrix, node_mapping=node_mapping) + + assert list(result.index) == ["Z1_Z1", "Z1_Z2", "Z2_Z2"] + assert result.at["Z1_Z1", "cap_forward"] == 100.0 + assert result.at["Z1_Z2", "cap_forward"] == 50.0 + assert result.at["Z2_Z2", "cap_forward"] == 50.0 + + +def test_aggregate_line_capacities_fallback_and_reverse(): + lines = pd.DataFrame( + { + "bus0": ["B1", "B2"], + "bus1": ["B2", "B3"], + "s_nom": [30.0, 40.0], + }, + index=["L1", "L2"] + ) + + incidence_matrix = pd.DataFrame(columns=["B2_B1", "Link_B2_B3_fallback"]) + + result = aggregate_line_capacities(lines, incidence_matrix) + + assert result.at["B2_B1", "cap_forward"] == 30.0 + assert result.at["B2_B1", "cap_reverse"] == 30.0 + + if __name__ == "__main__": test_convert_rrule() test_available_products() @@ -855,3 +924,6 @@ def test_load_index_file(): test_initializer() test_sep_block_orders() test_aggregate_step_amount() + test_aggregate_line_capacities_direct_match() + test_aggregate_line_capacities_aggregation() + test_aggregate_line_capacities_fallback_and_reverse() From 6f117e2deaf8575cbe74544bc6ca63ae30542af0 Mon Sep 17 00:00:00 2001 From: gugrimm <155527339+gugrimm@users.noreply.github.com> Date: Wed, 27 May 2026 17:54:34 +0200 Subject: [PATCH 4/6] include s_max_pu in directional test --- tests/test_utils.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_utils.py b/tests/test_utils.py index c8241787d..052e0f085 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -848,7 +848,7 @@ def test_load_index_file(): assert df is None -def test_aggregate_line_capacities_direct_match(): +def test_aggregate_line_capacities_with_s_max_pu(): lines = pd.DataFrame( { "bus0": ["B1", "B2"], @@ -872,11 +872,11 @@ def test_aggregate_line_capacities_direct_match(): assert result.at["L1", "cap_forward"] == 100.0 assert result.at["L1", "cap_reverse"] == 100.0 - assert result.at["L2", "cap_forward"] == 180.0 - assert result.at["L2", "cap_reverse"] == 190.0 + assert result.at["L2", "cap_forward"] == 180.0*0.8 + assert result.at["L2", "cap_reverse"] == 190.0*0.8 -def test_aggregate_line_capacities_aggregation(): +def test_aggregate_line_capacities_zonal_aggregation(): lines = pd.DataFrame( { "bus0": ["B1", "B2", "B3"], @@ -924,6 +924,6 @@ def test_aggregate_line_capacities_fallback_and_reverse(): test_initializer() test_sep_block_orders() test_aggregate_step_amount() - test_aggregate_line_capacities_direct_match() - test_aggregate_line_capacities_aggregation() + test_aggregate_line_capacities_with_s_max_pu() + test_aggregate_line_capacities_zonal_aggregation() test_aggregate_line_capacities_fallback_and_reverse() From 40aeaec4e9c255da8c5208597a6e10d96e2849c8 Mon Sep 17 00:00:00 2001 From: gugrimm <155527339+gugrimm@users.noreply.github.com> Date: Wed, 27 May 2026 17:59:23 +0200 Subject: [PATCH 5/6] add test for directional capacities in complex clearing --- tests/test_complex_market_mechanisms.py | 95 +++++++++++++++++++++++++ 1 file changed, 95 insertions(+) diff --git a/tests/test_complex_market_mechanisms.py b/tests/test_complex_market_mechanisms.py index c213fa07b..c1883c667 100644 --- a/tests/test_complex_market_mechanisms.py +++ b/tests/test_complex_market_mechanisms.py @@ -157,6 +157,101 @@ def test_market_coupling(): market_config.param_dict = {} +def test_market_coupling_directional_capacities(): + market_config = simple_dayahead_auction_config + h = 2 + market_config.market_products = [ + MarketProduct(timedelta(hours=1), h, timedelta(hours=1)) + ] + market_config.additional_fields = [ + "bid_type", + "node_id", + ] + # Create a dictionary with the data + nodes = { + "name": ["node1", "node2"], + "v_nom": [380.0, 380.0], + } + + # Convert the dictionary to a Pandas DataFrame with 'name' as the index + nodes = pd.DataFrame(nodes).set_index("name") + + # Create a dictionary with lines having explicit directional capacities + # Flow from node2 to node1 will be limited to 300 (s_nom_reverse) + # Flow from node1 to node2 is allowed up to 1000 (s_nom_forward) + lines = { + "name": ["line_1"], + "bus0": ["node1"], + "bus1": ["node2"], + "s_nom": [500.0], + "s_nom_forward": [1000.0], + "s_nom_reverse": [300.0], + } + + # Convert the dictionary to a Pandas DataFrame + lines = pd.DataFrame(lines) + + grid_data = {"buses": nodes, "lines": lines} + market_config.param_dict["grid_data"] = grid_data + market_config.param_dict["log_flows"] = True + + next_opening = market_config.opening_hours.after(datetime(2005, 6, 1)) + products = get_available_products(market_config.market_products, next_opening) + assert len(products) == h + + """ + Create Orderbook with constant order volumes and prices: + - dem1: volume = -1000, price = 3000, node1 + - dem2: volume = -200, price = 3000, node2 + - gen1: volume = 1000, price = 100, node1 + - gen2: volume = 1000, price = 50, node2 + """ + orderbook = [] + orderbook = extend_orderbook( + products, volume=-1000, price=3000, orderbook=orderbook, node="node1" + ) + orderbook = extend_orderbook( + products, volume=-200, price=3000, orderbook=orderbook, node="node2" + ) + orderbook = extend_orderbook(products, 1000, 100, orderbook, node="node1") + orderbook = extend_orderbook(products, 1000, 50, orderbook, node="node2") + + mr = ComplexClearingRole(market_config) + accepted_orders, rejected_orders, meta, flows = mr.clear(orderbook, products) + + assert meta[0]["node"] == "node1" + # Local supply at node1 is 700 because it imports max 300 from node2 + assert math.isclose(meta[0]["supply_volume"], 700, abs_tol=eps) + assert math.isclose(meta[0]["demand_volume"], 1000, abs_tol=eps) + assert math.isclose(meta[0]["price"], 100, abs_tol=eps) + + assert meta[2]["node"] == "node2" + # Local supply at node2 is 500 (200 local demand + 300 export) + assert math.isclose(meta[2]["supply_volume"], 500, abs_tol=eps) + assert math.isclose(meta[2]["demand_volume"], 200, abs_tol=eps) + assert math.isclose(meta[2]["price"], 50, abs_tol=eps) + + assert rejected_orders == [] + assert accepted_orders[0]["agent_addr"] == "dem1" + assert math.isclose(accepted_orders[0]["accepted_volume"], -1000, abs_tol=eps) + assert accepted_orders[1]["agent_addr"] == "dem3" + assert math.isclose(accepted_orders[1]["accepted_volume"], -200, abs_tol=eps) + + assert accepted_orders[2]["agent_addr"] == "gen5" + assert math.isclose(accepted_orders[2]["accepted_volume"], 700, abs_tol=eps) + assert math.isclose(accepted_orders[2]["accepted_price"], 100, abs_tol=eps) + + assert accepted_orders[3]["agent_addr"] == "gen7" + assert math.isclose(accepted_orders[3]["accepted_volume"], 500, abs_tol=eps) + assert math.isclose(accepted_orders[3]["accepted_price"], 50, abs_tol=eps) + + # Check that recorded flows reflect the directional capacities (flow is node1 -> node2 so it's negative) + # The flow limit from node2 to node1 is -300 + assert math.isclose(flows[(products[0][0], 0)], -300, abs_tol=eps) + + market_config.param_dict = {} + + def test_market_coupling_with_island(): market_config = simple_dayahead_auction_config h = 2 From c8e48617c817c4710593ac7d1ee81c265b74f0f5 Mon Sep 17 00:00:00 2001 From: gugrimm <155527339+gugrimm@users.noreply.github.com> Date: Wed, 27 May 2026 18:03:45 +0200 Subject: [PATCH 6/6] pre-commit --- tests/test_complex_market_mechanisms.py | 2 +- tests/test_utils.py | 44 ++++++++++++++----------- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/tests/test_complex_market_mechanisms.py b/tests/test_complex_market_mechanisms.py index c1883c667..b24499bae 100644 --- a/tests/test_complex_market_mechanisms.py +++ b/tests/test_complex_market_mechanisms.py @@ -244,7 +244,7 @@ def test_market_coupling_directional_capacities(): assert accepted_orders[3]["agent_addr"] == "gen7" assert math.isclose(accepted_orders[3]["accepted_volume"], 500, abs_tol=eps) assert math.isclose(accepted_orders[3]["accepted_price"], 50, abs_tol=eps) - + # Check that recorded flows reflect the directional capacities (flow is node1 -> node2 so it's negative) # The flow limit from node2 to node1 is -300 assert math.isclose(flows[(products[0][0], 0)], -300, abs_tol=eps) diff --git a/tests/test_utils.py b/tests/test_utils.py index 052e0f085..fa000639f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -856,24 +856,24 @@ def test_aggregate_line_capacities_with_s_max_pu(): "s_nom": [100.0, 200.0], "s_max_pu": [1.0, 0.8], "s_nom_forward": [None, 180.0], - "s_nom_reverse": [None, 190.0] + "s_nom_reverse": [None, 190.0], }, - index=["L1", "L2"] + index=["L1", "L2"], ) - + incidence_matrix = pd.DataFrame(index=["B1", "B2", "B3"], columns=["L1", "L2"]) - + result = aggregate_line_capacities(lines, incidence_matrix) - + assert "cap_forward" in result.columns assert "cap_reverse" in result.columns assert list(result.index) == ["L1", "L2"] - + assert result.at["L1", "cap_forward"] == 100.0 assert result.at["L1", "cap_reverse"] == 100.0 - - assert result.at["L2", "cap_forward"] == 180.0*0.8 - assert result.at["L2", "cap_reverse"] == 190.0*0.8 + + assert result.at["L2", "cap_forward"] == 180.0 * 0.8 + assert result.at["L2", "cap_reverse"] == 190.0 * 0.8 def test_aggregate_line_capacities_zonal_aggregation(): @@ -883,15 +883,19 @@ def test_aggregate_line_capacities_zonal_aggregation(): "bus1": ["B2", "B4", "B4"], "s_nom": [100.0, 50.0, 50.0], }, - index=["L1", "L2", "L3"] + index=["L1", "L2", "L3"], ) - + node_mapping = {"B1": "Z1", "B2": "Z1", "B3": "Z2", "B4": "Z2"} - - incidence_matrix = pd.DataFrame(index=["Z1", "Z2"], columns=["Z1_Z1", "Z1_Z2", "Z2_Z2"]) - - result = aggregate_line_capacities(lines, incidence_matrix, node_mapping=node_mapping) - + + incidence_matrix = pd.DataFrame( + index=["Z1", "Z2"], columns=["Z1_Z1", "Z1_Z2", "Z2_Z2"] + ) + + result = aggregate_line_capacities( + lines, incidence_matrix, node_mapping=node_mapping + ) + assert list(result.index) == ["Z1_Z1", "Z1_Z2", "Z2_Z2"] assert result.at["Z1_Z1", "cap_forward"] == 100.0 assert result.at["Z1_Z2", "cap_forward"] == 50.0 @@ -905,13 +909,13 @@ def test_aggregate_line_capacities_fallback_and_reverse(): "bus1": ["B2", "B3"], "s_nom": [30.0, 40.0], }, - index=["L1", "L2"] + index=["L1", "L2"], ) - + incidence_matrix = pd.DataFrame(columns=["B2_B1", "Link_B2_B3_fallback"]) - + result = aggregate_line_capacities(lines, incidence_matrix) - + assert result.at["B2_B1", "cap_forward"] == 30.0 assert result.at["B2_B1", "cap_reverse"] == 30.0