Skip to content

Commit

Permalink
Merge branch 'main' into markets
Browse files Browse the repository at this point in the history
  • Loading branch information
nick-harder authored Jan 30, 2024
2 parents 90d496e + 8290bf7 commit f45d190
Show file tree
Hide file tree
Showing 86 changed files with 38,975 additions and 1,701 deletions.
5 changes: 5 additions & 0 deletions assume/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from importlib.metadata import version

from assume.common import MarketConfig, MarketProduct
from assume.scenario.loader_csv import (
load_custom_units,
load_scenario_folder,
run_learning,
)
from assume.world import World

__version__ = version("assume-framework")
Expand Down
59 changes: 43 additions & 16 deletions assume/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def calculate_generation_cost(
"""
if start not in self.index:
return
start = self.index[0]
product_type_mc = product_type + "_marginal_costs"
for t in self.outputs[product_type_mc][start:end].index:
mc = self.calculate_marginal_cost(
Expand Down Expand Up @@ -271,6 +271,19 @@ def get_starting_costs(self, op_time: int) -> float:
"""
return 0

def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float:
"""
Calculates the marginal cost for the given power
:param start: the start time of the dispatch
:type start: pd.Timestamp
:param power: the power output of the unit
:type power: float
:return: the marginal cost for the given power
:rtype: float
"""
pass


class SupportsMinMax(BaseUnit):
"""
Expand All @@ -297,8 +310,8 @@ class SupportsMinMax(BaseUnit):
ramp_up: float
efficiency: float
emission_factor: float
min_operating_time: int
min_down_time: int
min_operating_time: int = 0
min_down_time: int = 0

def calculate_min_max_power(
self, start: pd.Timestamp, end: pd.Timestamp, product_type: str = "energy"
Expand All @@ -317,19 +330,32 @@ def calculate_min_max_power(
pass

def calculate_ramp(
self, previous_power: float, power: float, current_power: float = 0
self,
op_time: int,
previous_power: float,
power: float,
current_power: float = 0,
) -> float:
"""
Calculates the ramp for the given power
Args:
op_time (int): the operation time
previous_power (float): the previous power output of the unit
power (float): the power output of the unit
current_power (float): the current power output of the unit
Returns:
float: the ramp for the given power
"""
# op_time_befor = self.get_operation_time(start)
# was off before, but should be on now and min_down_time is not reached
if power > 0 and op_time < 0 and op_time > -self.min_down_time:
power = 0
# was on before, but should be off now and min_operating_time is not reached
elif power == 0 and op_time > 0 and op_time < self.min_operating_time:
power = self.min_power

if power == 0:
# if less than min_power is required, we run min_power
# we could also split at self.min_power/2
Expand Down Expand Up @@ -375,9 +401,9 @@ def get_operation_time(self, start: datetime) -> int:
int: the operation time
"""
before = start - self.index.freq
# before = start

max_time = max(self.min_operating_time, self.min_down_time)
begin = before - self.index.freq * max_time
begin = start - self.index.freq * max_time
end = before
arr = self.outputs["energy"][begin:end][::-1] > 0
if len(arr) < 1:
Expand All @@ -400,6 +426,9 @@ def get_average_operation_times(self, start: datetime) -> tuple[float, float]:
Returns:
tuple[float, float]: avg_op_time, avg_down_time
Note:
down_time in general is indicated with negative values
"""
op_series = []

Expand All @@ -408,7 +437,7 @@ def get_average_operation_times(self, start: datetime) -> tuple[float, float]:

if len(arr) < 1:
# before start of index
return self.min_operating_time, self.min_down_time
return max(self.min_operating_time, 1), min(-self.min_down_time, -1)

op_series = []
status = arr.iloc[0]
Expand All @@ -418,7 +447,7 @@ def get_average_operation_times(self, start: datetime) -> tuple[float, float]:
runn += 1
else:
op_series.append(-((-1) ** status) * runn)
runn = 0
runn = 1
status = val
op_series.append(-((-1) ** status) * runn)

Expand All @@ -432,9 +461,11 @@ def get_average_operation_times(self, start: datetime) -> tuple[float, float]:
if down_times == []:
avg_down_time = self.min_down_time
else:
avg_down_time = abs(sum(down_times) / len(down_times))
avg_down_time = sum(down_times) / len(down_times)

return max(1, avg_op_time), max(1, avg_down_time)
return max(1, avg_op_time, self.min_operating_time), min(
-1, avg_down_time, -self.min_down_time
)

def get_starting_costs(self, op_time: int) -> float:
"""
Expand Down Expand Up @@ -591,8 +622,7 @@ def calculate_ramp_discharge(
)
else:
# Assuming the storage is not restricted by ramping charging down
if previous_power < 0:
previous_power = 0
previous_power = max(previous_power, 0)

power_discharge = min(
power_discharge,
Expand Down Expand Up @@ -635,8 +665,7 @@ def calculate_ramp_charge(
previous_power - self.ramp_down_discharge - current_power, 0
)
else:
if previous_power > 0:
previous_power = 0
previous_power = min(previous_power, 0)

power_charge = max(
power_charge,
Expand Down Expand Up @@ -727,7 +756,6 @@ class LearningConfig(TypedDict):
observation_dimension (int): The observation dimension.
action_dimension (int): The action dimension.
continue_learning (bool): Whether to continue learning.
load_model_path (str): The path to the model to load.
max_bid_price (float): The maximum bid price.
learning_mode (bool): Whether to use learning mode.
algorithm (str): The algorithm to use.
Expand All @@ -748,7 +776,6 @@ class LearningConfig(TypedDict):
observation_dimension: int
action_dimension: int
continue_learning: bool
load_model_path: str
max_bid_price: float
learning_mode: bool
algorithm: str
Expand Down
17 changes: 10 additions & 7 deletions assume/common/forecasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ def __getitem__(self, column: str) -> pd.Series:
This method returns the forecast for a given column as a pandas Series based on the provided index.
"""

return pd.Series(0, self.index)
return pd.Series(0.0, self.index)

def get_availability(self, unit: str) -> pd.Series:
"""
Expand Down Expand Up @@ -123,7 +122,8 @@ def __getitem__(self, column: str) -> pd.Series:
if column not in self.forecasts.columns:
if "availability" in column:
return pd.Series(1, self.index)
return pd.Series(0, self.index)
return pd.Series(0.0, self.index)

return self.forecasts[column]

def set_forecast(self, data: pd.DataFrame | pd.Series | None, prefix=""):
Expand Down Expand Up @@ -168,7 +168,7 @@ def calc_forecast_if_needed(self):
Calculates the forecasts if they are not already calculated.
This method calculates additional forecasts if they do not already exist, including
"price_forecast" and "residual_load_forecast".
"price_EOM" and "residual_load_forecast".
"""

cols = []
Expand Down Expand Up @@ -313,16 +313,19 @@ def calculate_marginal_cost(self, pp_series: pd.Series):
if fp_column in self.forecasts.columns:
fuel_price = self.forecasts[fp_column]
else:
fuel_price = pd.Series(0, index=self.index)
fuel_price = pd.Series(0.0, index=self.index)

emission_factor = pp_series["emission_factor"]
co2_price = self.forecasts["fuel_price_co2"]

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
)

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

return marginal_cost

Expand Down Expand Up @@ -388,7 +391,7 @@ def __getitem__(self, column: str) -> pd.Series:
"""

if column not in self.forecasts.columns:
return pd.Series(0, self.index)
return pd.Series(0.0, self.index)
noise = np.random.normal(0, self.sigma, len(self.index))
return self.forecasts[column] * noise

Expand Down
64 changes: 55 additions & 9 deletions assume/common/units_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
Orderbook,
RegistrationMessage,
)
from assume.common.utils import aggregate_step_amount
from assume.common.utils import aggregate_step_amount, get_products_index
from assume.strategies import BaseStrategy, LearningStrategy
from assume.units import BaseUnit

Expand Down Expand Up @@ -446,27 +446,60 @@ async def formulate_bids(
order["volume"] = round(order["volume"] / market.volume_tick)
if market.price_tick:
order["price"] = round(order["price"] / market.price_tick)

order["bid_id"] = f"{unit_id}_{i+1}"
if "bid_id" not in order.keys() or order["bid_id"] is None:
order["bid_id"] = f"{unit_id}_{i+1}"
order["unit_id"] = unit_id
orderbook.append(order)

return orderbook

def write_learning_to_output(
self, start: datetime, marketconfig: MarketConfig
self, products_index: pd.DatetimeIndex, marketconfig: MarketConfig
) -> None:
"""
Sends the current rl_strategy update to the output agent.
Args:
start (datetime): The start time.
products_index (pd.DatetimeIndex): The index of all products.
marketconfig (MarketConfig): The market configuration.
"""
try:
from assume.strategies.learning_advanced_orders import (
RLAdvancedOrderStrategy,
)
except ImportError as e:
self.logger.info(
"Import of Learning Strategies failed. Check that you have all required packages installed (torch): %s",
e,
)
return

output_agent_list = []
start = products_index[0]
for unit_id, unit in self.units.items():
# rl only for energy market for now!
if isinstance(
unit.bidding_strategies.get(marketconfig.product_type),
(RLAdvancedOrderStrategy),
):
# TODO: check whether to split the reward, profit and regret to different lines
output_dict = {
"datetime": start,
"profit": unit.outputs["profit"].loc[products_index].sum(),
"reward": unit.outputs["reward"].loc[products_index].sum() / 24,
"regret": unit.outputs["regret"].loc[products_index].sum(),
"unit": unit_id,
}
noise_tuple = unit.outputs["rl_exploration_noise"].loc[start]
action_tuple = unit.outputs["rl_actions"].loc[start]
action_dim = len(action_tuple)
for i in range(action_dim):
output_dict[f"exploration_noise_{i}"] = noise_tuple[i]
output_dict[f"actions_{i}"] = action_tuple[i]

output_agent_list.append(output_dict)

elif isinstance(
unit.bidding_strategies.get(marketconfig.product_type),
LearningStrategy,
):
Expand Down Expand Up @@ -502,7 +535,7 @@ def write_learning_to_output(

def write_to_learning(
self,
start: datetime,
products_index: pd.DatetimeIndex,
marketconfig: MarketConfig,
obs_dim: int,
act_dim: int,
Expand All @@ -522,9 +555,13 @@ def write_to_learning(
"""
all_observations = []
all_rewards = []
start = products_index[0]
try:
import torch as th

from assume.strategies.learning_advanced_orders import (
RLAdvancedOrderStrategy,
)
except ImportError:
logger.error("tried writing learning_params, but torch is not installed")
return
Expand All @@ -536,6 +573,15 @@ def write_to_learning(
for unit in self.units.values():
# rl only for energy market for now!
if isinstance(
unit.bidding_strategies.get(marketconfig.product_type),
(RLAdvancedOrderStrategy),
):
all_observations[i, :] = unit.outputs["rl_observations"][start]
all_actions[i, :] = unit.outputs["rl_actions"][start]
all_rewards.append(sum(unit.outputs["reward"][products_index]))
i += 1

elif isinstance(
unit.bidding_strategies.get(marketconfig.product_type),
LearningStrategy,
):
Expand Down Expand Up @@ -576,6 +622,7 @@ def write_learning_params(
"""

learning_strategies = []
products_index = get_products_index(orderbook)

for unit in self.units.values():
bidding_strategy = unit.bidding_strategies.get(marketconfig.product_type)
Expand All @@ -588,16 +635,15 @@ def write_learning_params(

# should write learning results if at least one bidding_strategy is a learning strategy
if learning_strategies and orderbook:
start = orderbook[0]["start_time"]
# write learning output
self.write_learning_to_output(start, marketconfig)
self.write_learning_to_output(products_index, marketconfig)

# we are using the first learning_strategy to check learning_mode
# as this should be the same value for all strategies
if learning_strategies[0].learning_mode:
# in learning mode we are sending data to learning
self.write_to_learning(
start=start,
products_index=products_index,
marketconfig=marketconfig,
obs_dim=obs_dim,
act_dim=act_dim,
Expand Down
Loading

0 comments on commit f45d190

Please sign in to comment.