Skip to content

Commit

Permalink
Fix missing fixed costs and fix forecaster (#295)
Browse files Browse the repository at this point in the history
-fix missing fixed cost in marginal cost calculations -> now power plant
and storage units consider both variable and fixed cost
- before all calculations were missing fixed costs completely since all
input files provide only fixed costs
-fix forecaster keeping old values even if new values are given -> now
values are overwritten
-fix learning_strategy missing cost calculations
-fix tests to account for missing fixed cost in marginal cost
  • Loading branch information
nick-harder authored Feb 14, 2024
1 parent 7380ef5 commit aa6cd9c
Show file tree
Hide file tree
Showing 32 changed files with 156 additions and 165 deletions.
14 changes: 5 additions & 9 deletions assume/common/forecasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,8 @@ def set_forecast(self, data: pd.DataFrame | pd.Series | None, prefix=""):
for column in data.columns:
self.forecasts[column] = data[column].item()
else:
# if some columns already exist, just add the new columns
new_columns = set(data.columns) - set(self.forecasts.columns)
self.forecasts = pd.concat(
[self.forecasts, data[list(new_columns)]], axis=1
)
# Add new columns to the existing DataFrame, overwriting any existing columns with the same names
self.forecasts = self.forecasts.assign(**data)
else:
self.forecasts[prefix + data.name] = data

Expand Down Expand Up @@ -365,12 +362,11 @@ def calculate_marginal_cost(self, pp_series: pd.Series) -> pd.Series:

fuel_cost = fuel_price / pp_series["efficiency"]
emissions_cost = co2_price * emission_factor / pp_series["efficiency"]
fixed_cost = pp_series["fixed_cost"] if "fixed_cost" in pp_series else 0.0
variable_cost = (
pp_series["variable_cost"] if "variable_cost" in pp_series else 0.0
additional_cost = (
pp_series["additional_cost"] if "additional_cost" in pp_series else 0.0
)

marginal_cost = fuel_cost + emissions_cost + fixed_cost + variable_cost
marginal_cost = fuel_cost + emissions_cost + additional_cost

return marginal_cost

Expand Down
2 changes: 1 addition & 1 deletion assume/scenario/loader_amiris.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ def add_agent_to_world(
# AMIRIS does not have min_power
"min_power": 0,
"max_power": power,
"fixed_cost": markup,
"additional_cost": markup,
"bidding_strategies": strategies,
"technology": translate_fuel_type[prototype["FuelType"]],
"fuel_type": translate_fuel_type[prototype["FuelType"]],
Expand Down
6 changes: 3 additions & 3 deletions assume/strategies/flexable.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,8 @@ def calculate_bids(
min_power[start] = unit.calculate_ramp(
op_time, previous_power, min_power[start], current_power
)
bid_quantity = min_power[start] - previous_power
if bid_quantity >= 0:
bid_quantity = previous_power - min_power[start]
if bid_quantity <= 0:
continue

# bid_quantity < 0
Expand All @@ -365,7 +365,7 @@ def calculate_bids(
if specific_revenue < 0:
capacity_price = (
abs(specific_revenue)
* (unit.min_power + bid_quantity)
* (bid_quantity - unit.min_power)
/ bid_quantity
)
else:
Expand Down
2 changes: 1 addition & 1 deletion assume/strategies/flexable_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ def calculate_reward(
end = order["end_time"]
end_excl = end - unit.index.freq
index = pd.date_range(start, end_excl, freq=unit.index.freq)
costs = pd.Series(float(unit.fixed_cost), index=index)
costs = pd.Series(float(unit.additional_cost), index=index)
for start in index:
if unit.outputs[product_type][start] != 0:
costs[start] += abs(
Expand Down
18 changes: 11 additions & 7 deletions assume/strategies/learning_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ def calculate_reward(
profit = 0
reward = 0
opportunity_cost = 0
costs = 0

# iterate over all orders in the orderbook, to calculate order specific profit
for order in orderbook:
Expand All @@ -399,38 +400,40 @@ def calculate_reward(
duration = (end - start) / timedelta(hours=1)

# calculate profit as income - running_cost from this event
price_difference = order["accepted_price"] - marginal_cost
order_profit = price_difference * order["accepted_volume"] * duration
order_profit = order["accepted_price"] * order["accepted_volume"] * duration
order_cost = marginal_cost * order["accepted_volume"] * duration

# calculate opportunity cost
# as the loss of income we have because we are not running at full power
order_opportunity_cost = (
price_difference
(order["accepted_price"] - marginal_cost)
* (
unit.max_power - unit.outputs[product_type].loc[start:end_excl]
).sum()
* duration
)

# if our opportunity costs are negative, we did not miss an opportunity to earn money and we set them to 0
order_opportunity_cost = max(order_opportunity_cost, 0)

# collect profit and opportunity cost for all orders
opportunity_cost += order_opportunity_cost
profit += order_profit
costs += order_cost
opportunity_cost += order_opportunity_cost

# consideration of start-up costs, which are evenly divided between the
# upward and downward regulation events
if (
unit.outputs[product_type].loc[start] != 0
and unit.outputs[product_type].loc[start - unit.index.freq] == 0
):
profit = profit - unit.hot_start_cost / 2
costs += unit.hot_start_cost / 2
elif (
unit.outputs[product_type].loc[start] == 0
and unit.outputs[product_type].loc[start - unit.index.freq] != 0
):
profit = profit - unit.hot_start_cost / 2
costs += unit.hot_start_cost / 2

profit = profit - costs

# ---------------------------
# 4.1 Calculate Reward
Expand All @@ -446,6 +449,7 @@ def calculate_reward(
unit.outputs["profit"].loc[start:end_excl] += profit
unit.outputs["reward"].loc[start:end_excl] = reward
unit.outputs["regret"].loc[start:end_excl] = opportunity_cost
unit.outputs["total_costs"].loc[start:end_excl] = costs

def load_actor_params(self, load_path):
"""
Expand Down
78 changes: 37 additions & 41 deletions assume/units/powerplant.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import logging
from datetime import datetime, timedelta
from functools import lru_cache
from typing import Tuple, Union

import pandas as pd

Expand All @@ -17,7 +18,7 @@

class PowerPlant(SupportsMinMax):
"""
A class for a powerplant unit.
A class for a power plant unit.
Args:
id (str): The ID of the storage unit.
Expand All @@ -27,27 +28,25 @@ class PowerPlant(SupportsMinMax):
index (pandas.DatetimeIndex): The index of the unit.
max_power (float): The maximum power output capacity of the power plant in MW.
min_power (float, optional): The minimum power output capacity of the power plant in MW. Defaults to 0.0 MW.
efficiency (float, optional): The efficiency of the poewr plant in converting fuel to electricity (Defaults to 1.0). Defaults to 1.0.
fixed_cost (float, optional): The fixed operating cost of the power plant, independent of the power output (Defaults to 0.0 monetary units). Defaults to 0.0.
variable_cost (float | pd.Series, optional): The variable operating cost of the power plant, dependent on the power output (Defaults to 0.0 monetary units). Defaults to 0.0.
partial_load_eff (bool, optional): Does the efficiency varies at part loads? (Defaults to False). Defaults to False.
fuel_type (str, optional): The type of fuel used by the power plant for power generation (Defaults to "others"). Defaults to "others".
emission_factor (float, optional): The emission factor associated with the power plants fuel type -> CO2 emissions per unit of energy produced (Defaults to 0.0.). Defaults to 0.0.
ramp_up (float | None, optional): The ramp-up rate of the power plant, indicating how quickly it can increase power output (Defaults to -1). Defaults to None.
ramp_down (float | None, optional): The ramp-down rate of the power plant, indicating how quickly it can decrease power output. (Defaults to -1). Defaults to None.
hot_start_cost (float, optional): The cost of a hot start, where the power plant is restarted after a recent shutdown.(Defaults to 0 monetary units.). Defaults to 0.
warm_start_cost (float, optional): The cost of a warm start, where the power plant is restarted after a moderate downtime.(Defaults to 0 monetary units.). Defaults to 0.
cold_start_cost (float, optional): The cost of a cold start, where the power plant is restarted after a prolonged downtime.(Defaults to 0 monetary units.). Defaults to 0.
min_operating_time (float, optional): The minimum duration that the power plant must operate once started, in hours.(Defaults to 0 hours.). Defaults to 0.
min_down_time (float, optional): The minimum downtime required after a shutdown before the power plant can be restarted, in hours.(Defaults to 0 hours.). Defaults to 0.
downtime_hot_start (int, optional): The downtime required after a hot start before the power plant can be restarted, in hours.(Defaults to 8 hours.). Defaults to 8.
downtime_warm_start (int, optional): The downtime required after a warm start before the power plant can be restarted, in hours.( Defaults to 48 hours.). Defaults to 48.
heat_extraction (bool, optional): A boolean indicating whether the power plant can extract heat for external purposes.(Defaults to False.). Defaults to False.
max_heat_extraction (float, optional): The maximum amount of heat that the power plant can extract for external use, in some suitable unit.(Defaults to 0.). Defaults to 0.
location (tuple[float, float], optional): The geographical coordinates (latitude and longitude) of the power plant's location.(Defaults to (0.0, 0.0).). Defaults to (0.0, 0.0).
node (str, optional): The identifier of the electrical bus or network node to which the power plant is connected.(Defaults to "bus0".). Defaults to "bus0".
efficiency (float, optional): The efficiency of the power plant in converting fuel to electricity. Defaults to 1.0.
additional_cost (Union[float, pd.Series], optional): Additional costs associated with power generation, in EUR/MWh. Defaults to 0.
partial_load_eff (bool, optional): Does the efficiency vary at part loads? Defaults to False.
fuel_type (str, optional): The type of fuel used by the power plant for power generation. Defaults to "others".
emission_factor (float, optional): The emission factor associated with the power plant's fuel type (CO2 emissions per unit of energy produced). Defaults to 0.0.
ramp_up (Union[float, None], optional): The ramp-up rate of the power plant, indicating how quickly it can increase power output. Defaults to None.
ramp_down (Union[float, None], optional): The ramp-down rate of the power plant, indicating how quickly it can decrease power output. Defaults to None.
hot_start_cost (float, optional): The cost of a hot start, where the power plant is restarted after a recent shutdown. Defaults to 0.
warm_start_cost (float, optional): The cost of a warm start, where the power plant is restarted after a moderate downtime. Defaults to 0.
cold_start_cost (float, optional): The cost of a cold start, where the power plant is restarted after a prolonged downtime. Defaults to 0.
min_operating_time (float, optional): The minimum duration that the power plant must operate once started, in hours. Defaults to 0.
min_down_time (float, optional): The minimum downtime required after a shutdown before the power plant can be restarted, in hours. Defaults to 0.
downtime_hot_start (int, optional): The downtime required after a hot start before the power plant can be restarted, in hours. Defaults to 8.
downtime_warm_start (int, optional): The downtime required after a warm start before the power plant can be restarted, in hours. Defaults to 48.
heat_extraction (bool, optional): A boolean indicating whether the power plant can extract heat for external purposes. Defaults to False.
max_heat_extraction (float, optional): The maximum amount of heat that the power plant can extract for external use, in some suitable unit. Defaults to 0.
location (Tuple[float, float], optional): The geographical coordinates (latitude and longitude) of the power plant's location. Defaults to (0.0, 0.0).
node (str, optional): The identifier of the electrical bus or network node to which the power plant is connected. Defaults to "bus0".
**kwargs (dict, optional): Additional keyword arguments to be passed to the base class. Defaults to {}.
"""

def __init__(
Expand All @@ -60,13 +59,12 @@ def __init__(
max_power: float,
min_power: float = 0.0,
efficiency: float = 1.0,
fixed_cost: float = 0.0,
variable_cost: float | pd.Series = 0.0,
additional_cost: Union[float, pd.Series] = 0.0,
partial_load_eff: bool = False,
fuel_type: str = "others",
emission_factor: float = 0.0,
ramp_up: float | None = None,
ramp_down: float | None = None,
ramp_up: Union[float, None] = None,
ramp_down: Union[float, None] = None,
hot_start_cost: float = 0,
warm_start_cost: float = 0,
cold_start_cost: float = 0,
Expand All @@ -76,7 +74,7 @@ def __init__(
downtime_warm_start: int = 48, # hours
heat_extraction: bool = False,
max_heat_extraction: float = 0,
location: tuple[float, float] = (0.0, 0.0),
location: Tuple[float, float] = (0.0, 0.0),
node: str = "bus0",
**kwargs,
):
Expand All @@ -94,13 +92,20 @@ def __init__(
self.max_power = max_power
self.min_power = min_power
self.efficiency = efficiency
self.additional_cost = additional_cost
self.partial_load_eff = partial_load_eff
self.fuel_type = fuel_type
self.emission_factor = emission_factor
self.heat_extraction = heat_extraction
self.max_heat_extraction = max_heat_extraction
self.hot_start_cost = hot_start_cost * max_power
self.warm_start_cost = warm_start_cost * max_power
self.cold_start_cost = cold_start_cost * max_power

# check ramping enabled
self.ramp_down = max_power if ramp_down == 0 or ramp_down is None else ramp_down
self.ramp_up = max_power if ramp_up == 0 or ramp_up is None else ramp_up

self.min_operating_time = min_operating_time if min_operating_time > 0 else 1
self.min_down_time = min_down_time if min_down_time > 0 else 1
self.downtime_hot_start = downtime_hot_start / (
Expand All @@ -110,15 +115,6 @@ def __init__(
self.index.freq / timedelta(hours=1)
)

self.fixed_cost = fixed_cost
self.variable_cost = variable_cost
self.hot_start_cost = hot_start_cost * max_power
self.warm_start_cost = warm_start_cost * max_power
self.cold_start_cost = cold_start_cost * max_power

self.heat_extraction = heat_extraction
self.max_heat_extraction = max_heat_extraction

self.init_marginal_cost()

def init_marginal_cost(self):
Expand Down Expand Up @@ -237,7 +233,7 @@ def calc_simple_marginal_cost(
marginal_cost = (
fuel_price / self.efficiency
+ self.forecaster.get_price("co2") * self.emission_factor / self.efficiency
+ self.variable_cost
+ self.additional_cost
)

return marginal_cost
Expand Down Expand Up @@ -296,16 +292,16 @@ def calc_marginal_cost_with_partial_eff(
efficiency = self.efficiency - eta_loss
co2_price = self.forecaster.get_price("co2").at[timestep]

variable_cost = (
self.variable_cost
if isinstance(self.variable_cost, float)
else self.variable_cost[timestep]
additional_cost = (
self.additional_cost.at[timestep]
if isinstance(self.additional_cost, pd.Series)
else self.additional_cost
)

marginal_cost = (
fuel_price / efficiency
+ co2_price * self.emission_factor / efficiency
+ variable_cost
+ additional_cost
)

return marginal_cost
Expand Down
Loading

0 comments on commit aa6cd9c

Please sign in to comment.