Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explicit demand response peak load #484

Open
wants to merge 63 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
32b689c
Add residential componnets and convert DST to classes
nick-harder Oct 17, 2024
01acad7
-add release notes
nick-harder Oct 17, 2024
9cd2e93
-create empty dict for dst componnets to avoid import error
nick-harder Oct 18, 2024
e61c116
-adjust structure
nick-harder Oct 18, 2024
0511490
-fix error in min_down_time and up_time constraints
nick-harder Oct 18, 2024
7e23e78
-missing docstring
nick-harder Oct 18, 2024
3df0bb3
switch back to glpk for now in the tests
nick-harder Oct 18, 2024
10fe4d6
Add check for natural gas price profile in DRIPlant class
nick-harder Oct 18, 2024
c165563
-fix tests
nick-harder Oct 18, 2024
d4e427a
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Oct 27, 2024
2a4b240
input files for experiment
Manish-Khanra Oct 28, 2024
8c1d606
gg
Manish-Khanra Oct 29, 2024
74e0401
grid congestion forcast included
Manish-Khanra Oct 30, 2024
a10a5bf
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Oct 30, 2024
d7b4802
line co2 factor fixed
Manish-Khanra Oct 30, 2024
7375c9a
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Oct 31, 2024
3ec01dc
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Oct 31, 2024
6f8dc86
test
Manish-Khanra Nov 6, 2024
1d03896
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Nov 6, 2024
fa7043c
test
Manish-Khanra Nov 7, 2024
8b0c84e
split load_shift into positive and negative
nick-harder Nov 8, 2024
59bac27
test
Manish-Khanra Nov 10, 2024
6c289d4
flex_power_requirement fixed
Manish-Khanra Nov 11, 2024
7e1c509
Grid congestion based flexibility finalised
Manish-Khanra Nov 11, 2024
11154d6
peak load shift
Manish-Khanra Nov 12, 2024
7a23fd8
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Nov 12, 2024
b183e83
...
Manish-Khanra Nov 13, 2024
da9ee4d
fix optimal values initialization
nick-harder Nov 13, 2024
ccb349b
def peak_load_shifting_flexibility implemented
Manish-Khanra Nov 14, 2024
05de6d7
Refactores poeal_load_cap
Manish-Khanra Nov 14, 2024
7edf4cd
renewable utilisation included
Manish-Khanra Nov 16, 2024
a4618ab
tests inlcuded
Manish-Khanra Nov 16, 2024
b6edc0c
Removed input
Manish-Khanra Nov 16, 2024
df54e4e
removed plot
Manish-Khanra Nov 16, 2024
8ad6a53
removed example
Manish-Khanra Nov 16, 2024
747280e
node added
Manish-Khanra Nov 16, 2024
3b8a2d3
solver fixed
Manish-Khanra Nov 16, 2024
a7d9d27
solver
Manish-Khanra Nov 17, 2024
54da44f
nodes added
Manish-Khanra Nov 17, 2024
53d0915
added lines and buses
Manish-Khanra Nov 17, 2024
5237327
lines and nodes added
Manish-Khanra Nov 17, 2024
30cf785
Changes to highs
Manish-Khanra Nov 17, 2024
96aace5
highs
Manish-Khanra Nov 17, 2024
580a2c8
solver fixed
Manish-Khanra Nov 17, 2024
19f0f51
removies plot function
Manish-Khanra Nov 17, 2024
222370b
fix solver options in steel plant when not using gurobi
maurerle Nov 18, 2024
9266b0f
test fixed
Manish-Khanra Nov 18, 2024
84217f2
iloc fixed
Manish-Khanra Nov 19, 2024
d4c3f4d
Big M for load shifting implemented
Manish-Khanra Nov 21, 2024
d423abb
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Nov 21, 2024
8a2631a
deleted wrong licence
Manish-Khanra Nov 21, 2024
75d1849
Merge branch 'main' into explicit_demand_response_peak_load
maurerle Nov 25, 2024
159b7f7
fix tests
maurerle Nov 25, 2024
16f3e29
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Nov 26, 2024
dead787
inputs added
Manish-Khanra Nov 26, 2024
d448285
commented out plot
Manish-Khanra Nov 27, 2024
c3f0e72
Merge branch 'main' of https://github.com/assume-framework/assume int…
Manish-Khanra Nov 27, 2024
5ebd0b4
-remove changes in example_01a
nick-harder Dec 3, 2024
71563ce
-remove unrequired changes
nick-harder Dec 3, 2024
dedbd4e
Merge branch 'main' into explicit_demand_response_peak_load
nick-harder Dec 3, 2024
56d8527
-fix hydrogen plant
nick-harder Dec 3, 2024
dc34ee9
-fix tests
nick-harder Dec 3, 2024
1d6627f
Merge branch 'main' into explicit_demand_response_peak_load
nick-harder Dec 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 210 additions & 28 deletions assume/common/forecasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,21 +89,23 @@ class CsvForecaster(Forecaster):
"""
This class represents a forecaster that provides timeseries for forecasts derived from existing files.

It initializes with the provided index. It includes methods to retrieve forecasts for specific columns,
availability of units, and prices of fuel types, returning the corresponding timeseries as pandas Series.
It initializes with the provided index and configuration data, including power plants, demand units,
and market configurations. The forecaster also supports optional inputs like DSM (demand-side management) units,
buses, and lines for more advanced forecasting scenarios.

Attributes:
index (pandas.Series): The index of the forecasts.
powerplants_units (dict[str, pandas.Series]): The power plants.
Methods are included to retrieve forecasts for specific columns, availability of units,
and prices of fuel types, returning the corresponding timeseries as pandas Series.

Args:
index (pandas.Series): The index of the forecasts.
powerplants_units (dict[str, pandas.Series]): The power plants.

Example:
>>> forecaster = CsvForecaster(index=pd.Series([1, 2, 3]))
>>> forecast = forecaster['temperature']
>>> print(forecast)
index (pd.Series): The index of the forecasts.
powerplants_units (pd.DataFrame): A DataFrame containing information about power plants.
demand_units (pd.DataFrame): A DataFrame with demand unit data.
market_configs (dict[str, dict]): Configuration details for the markets.
buses (pd.DataFrame | None, optional): A DataFrame of buses information. Defaults to None.
lines (pd.DataFrame | None, optional): A DataFrame of line information. Defaults to None.
save_path (str, optional): Path where the forecasts should be saved. Defaults to an empty string.
*args (object): Additional positional arguments.
**kwargs (object): Additional keyword arguments.

"""

Expand All @@ -112,16 +114,21 @@ def __init__(
index: pd.Series,
powerplants_units: pd.DataFrame,
demand_units: pd.DataFrame,
market_configs: dict = {},
market_configs: dict[str, dict],
buses: pd.DataFrame | None = None,
lines: pd.DataFrame | None = None,
save_path: str = "",
*args,
**kwargs,
*args: object,
**kwargs: object,
):
super().__init__(index, *args, **kwargs)
self.logger = logging.getLogger(__name__)
self.powerplants_units = powerplants_units
self.demand_units = demand_units
self.market_configs = market_configs
self.buses = buses
self.lines = lines

self.forecasts = pd.DataFrame(index=index)
self.save_path = save_path

Expand Down Expand Up @@ -191,24 +198,51 @@ def calc_forecast_if_needed(self):
"""
Calculates the forecasts if they are not already calculated.

This method calculates price forecast and residual load forecast for available markets, if
these don't already exist.
This method calculates price forecast and residual load forecast for available markets,
and other necessary forecasts if they don't already exist.
"""
self.add_missing_availability_columns()
self.calculate_market_forecasts()

# the following forecasts are only calculated if buses and lines are available
# and self.demand_units have a node column
if self.buses and self.lines:
# check if the demand_units have a node column and
# if the nodes are available in the buses
if (
"node" in self.demand_units.columns
and self.demand_units["node"].isin(self.buses.index).all()
):
self.add_node_congestion_signals()
self.add_utilisation_forecasts()
else:
self.logger.warning(
"Node-specific congestion signals and renewable utilisation forecasts could not be calculated. "
"Either 'node' column is missing in demand_units or nodes are not available in buses."
)

cols = []
for pp in self.powerplants_units.index:
col = f"availability_{pp}"
if col not in self.forecasts.columns:
s = pd.Series(1, index=self.forecasts.index)
s.name = col
cols.append(s)
cols.append(self.forecasts)
self.forecasts = pd.concat(cols, axis=1).copy()
def add_missing_availability_columns(self):
"""Add missing availability columns to the forecasts."""
missing_cols = [
f"availability_{pp}"
for pp in self.powerplants_units.index
if f"availability_{pp}" not in self.forecasts.columns
]

if missing_cols:
# Create a DataFrame with the missing columns initialized to 1
missing_data = pd.DataFrame(
1, index=self.forecasts.index, columns=missing_cols
)
# Append the missing columns to the forecasts
self.forecasts = pd.concat([self.forecasts, missing_data], axis=1).copy()

def calculate_market_forecasts(self):
"""Calculate market-specific price and residual load forecasts."""
for market_id, config in self.market_configs.items():
if config["product_type"] != "energy":
self.logger.warning(
f"Price forecast could be calculated for {market_id}. It can only be calculated for energy only markets for now"
f"Price forecast could not be calculated for {market_id}. It can only be calculated for energy-only markets for now."
)
continue

Expand All @@ -222,6 +256,29 @@ def calc_forecast_if_needed(self):
self.calculate_residual_load_forecast(market_id=market_id)
)

def add_node_congestion_signals(self):
"""Add node-specific congestion signals to the forecasts."""
node_congestion_signal_df = self.calculate_node_specific_congestion_forecast()
for col in node_congestion_signal_df.columns:
if col not in self.forecasts.columns:
self.forecasts[col] = node_congestion_signal_df[col]

def add_utilisation_forecasts(self):
"""Add renewable utilisation forecasts if missing."""
utilisation_columns = [
f"{node}_renewable_utilisation"
for node in self.demand_units["node"].unique()
]
utilisation_columns.append("all_nodes_renewable_utilisation")

if not all(col in self.forecasts.columns for col in utilisation_columns):
renewable_utilisation_forecast = (
self.calculate_renewable_utilisation_forecast()
)
for col in renewable_utilisation_forecast.columns:
if col not in self.forecasts.columns:
self.forecasts[col] = renewable_utilisation_forecast[col]

def get_registered_market_participants(self, market_id):
"""
Retrieves information about market participants to make accurate price forecasts.
Expand Down Expand Up @@ -299,7 +356,7 @@ def calculate_market_price_forecast(self, market_id):

"""

# calculate infeed of renewables and residual demand_df
# calculate infeed of renewables and residual demand
# check if max_power is a series or a float

# select only those power plant units, which have a bidding strategy for the specific market_id
Expand Down Expand Up @@ -380,6 +437,131 @@ def calculate_marginal_cost(self, pp_series: pd.Series) -> pd.Series:

return marginal_cost

def calculate_node_specific_congestion_forecast(self) -> pd.DataFrame:
"""
Calculates a collective node-specific congestion signal by aggregating the congestion severity of all
transmission lines connected to each node, taking into account powerplant load based on availability factors.

Returns:
pd.DataFrame: A DataFrame with columns for each node, where each column represents the collective
congestion signal time series for that node.
"""
# Step 1: Calculate powerplant load using availability factors
availability_factor_df = pd.DataFrame(
index=self.index, columns=self.powerplants_units.index, data=0.0
)

# Calculate load for each powerplant based on availability factor and max power
for pp, max_power in self.powerplants_units["max_power"].items():
availability_factor_df[pp] = (
self.forecasts[f"availability_{pp}"] * max_power
)

# Step 2: Calculate net load for each node (demand - generation)
net_load_by_node = {}

for node in self.demand_units["node"].unique():
# Calculate total demand for this node
node_demand_units = self.demand_units[
self.demand_units["node"] == node
].index
node_demand = self.forecasts[node_demand_units].sum(axis=1)

# Calculate total generation for this node by summing powerplant loads
node_generation_units = self.powerplants_units[
self.powerplants_units["node"] == node
].index
node_generation = availability_factor_df[node_generation_units].sum(axis=1)

# Calculate net load (demand - generation)
net_load_by_node[node] = node_demand - node_generation

# Step 3: Calculate line-specific congestion severity
line_congestion_severity = pd.DataFrame(index=self.index)

for line_id, line_data in self.lines.iterrows():
node1, node2 = line_data["bus0"], line_data["bus1"]
line_capacity = line_data["s_nom"]

# Calculate net load for the line as the sum of net loads from both connected nodes
line_net_load = net_load_by_node[node1] + net_load_by_node[node2]
congestion_severity = line_net_load / line_capacity

# Store the line-specific congestion severity in DataFrame
line_congestion_severity[f"{line_id}_congestion_severity"] = (
congestion_severity
)

# Step 4: Calculate node-specific congestion signal by aggregating connected lines
node_congestion_signal = pd.DataFrame(index=self.index)

for node in self.demand_units["node"].unique():
# Find all lines connected to this node
connected_lines = self.lines[
(self.lines["bus0"] == node) | (self.lines["bus1"] == node)
].index

# Collect all relevant line congestion severities
relevant_lines = [
f"{line_id}_congestion_severity" for line_id in connected_lines
]

# Ensure only existing columns are used to avoid KeyError
relevant_lines = [
line
for line in relevant_lines
if line in line_congestion_severity.columns
]

# Aggregate congestion severities for this node (use max or mean)
if relevant_lines:
node_congestion_signal[f"{node}_congestion_severity"] = (
line_congestion_severity[relevant_lines].max(axis=1)
)

return node_congestion_signal

def calculate_renewable_utilisation_forecast(self) -> pd.DataFrame:
"""
Calculates the renewable utilisation forecast by summing the available renewable generation
for each node and an overall 'all_nodes' summary.

Returns:
pd.DataFrame: A DataFrame with columns for each node, where each column represents the renewable
utilisation signal time series for that node and a column for total utilisation across all nodes.
"""
# Initialize a DataFrame to store renewable utilisation for each node
renewable_utilisation = pd.DataFrame(index=self.index)

# Identify renewable power plants by filtering `powerplants_units` DataFrame
renewable_plants = self.powerplants_units[
self.powerplants_units["fuel_type"] == "renewable"
]

# Calculate utilisation based on availability and max power for each renewable plant
for node in self.demand_units["node"].unique():
node_renewable_sum = pd.Series(0, index=self.index)

# Filter renewable plants in this specific node
node_renewable_plants = renewable_plants[renewable_plants["node"] == node]

for pp in node_renewable_plants.index:
max_power = node_renewable_plants.loc[pp, "max_power"]
availability_col = f"availability_{pp}"

# Calculate renewable power based on availability and max capacity
if availability_col in self.forecasts.columns:
node_renewable_sum += self.forecasts[availability_col] * max_power

# Store the node-specific renewable utilisation
renewable_utilisation[f"{node}_renewable_utilisation"] = node_renewable_sum

# Calculate the total renewable utilisation across all nodes
all_nodes_sum = renewable_utilisation.sum(axis=1)
renewable_utilisation["all_nodes_renewable_utilisation"] = all_nodes_sum

return renewable_utilisation

def save_forecasts(self, path=None):
"""
Saves the forecasts to a csv file located at the specified path.
Expand Down
9 changes: 9 additions & 0 deletions assume/scenario/loader_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ def load_dsm_units(
"unit_type",
"node",
"flexibility_measure",
"congestion_threshold",
"peak_load_cap",
]
# Filter the common columns to only include those that exist in the DataFrame
common_columns = [col for col in common_columns if col in dsm_units.columns]
Expand Down Expand Up @@ -497,11 +499,16 @@ def load_config_and_create_forecaster(
path=path, config=config, file_name="temperature", index=index
)

buses = load_file(path=path, config=config, file_name="buses")
lines = load_file(path=path, config=config, file_name="lines")

forecaster = CsvForecaster(
index=index,
powerplants_units=powerplant_units,
demand_units=demand_units,
market_configs=config["markets_config"],
buses=buses,
lines=lines,
)

forecaster.set_forecast(forecasts_df)
Expand All @@ -526,6 +533,8 @@ def load_config_and_create_forecaster(
"storage_units": storage_units,
"demand_units": demand_units,
"dsm_units": dsm_units,
"buses": buses,
"lines": lines,
"forecaster": forecaster,
}

Expand Down
6 changes: 3 additions & 3 deletions assume/strategies/naive_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ def calculate_bids(
**kwargs,
) -> Orderbook:
# calculate the optimal operation of the unit
unit.calculate_optimal_operation_if_needed()
unit.determine_optimal_operation_with_flex()

bids = []
for product in product_tuples:
Expand All @@ -167,7 +167,7 @@ def calculate_bids(
start = product[0]

volume = unit.opt_power_requirement.at[start]
marginal_price = unit.calculate_marginal_cost(start, volume)
marginal_price = 3000
bids.append(
{
"start_time": start,
Expand Down Expand Up @@ -195,7 +195,7 @@ def calculate_bids(
**kwargs,
) -> Orderbook:
# calculate the optimal operation of the unit according to the objective function
unit.calculate_optimal_operation_if_needed()
unit.determine_optimal_operation_with_flex()

bids = []
for product in product_tuples:
Expand Down
Loading
Loading