Skip to content

Commit

Permalink
Merge branch 'main' into data_request_mechanism
Browse files Browse the repository at this point in the history
  • Loading branch information
maurerle committed Nov 13, 2023
2 parents 2aeb5b0 + 61d58a8 commit 05acc78
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 55 deletions.
75 changes: 33 additions & 42 deletions assume/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ def calculate_bids(

return bids

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
"""
return 0

def set_dispatch_plan(
self,
marketconfig: MarketConfig,
Expand All @@ -125,28 +138,33 @@ def set_dispatch_plan(
end = order["end_time"]
end_excl = end - self.index.freq
if isinstance(order["accepted_volume"], dict):
self.outputs[product_type].loc[start:end_excl] += [
order["accepted_volume"][key]
for key in order["accepted_volume"].keys()
]
added_volume = list(order["accepted_volume"].values())
else:
self.outputs[product_type].loc[start:end_excl] += order[
"accepted_volume"
]

added_volume = order["accepted_volume"]
self.outputs[product_type].loc[start:end_excl] += added_volume
self.calculate_cashflow(product_type, orderbook)

self.outputs[product_type + "_marginal_costs"].loc[start:end_excl] += (
self.calculate_marginal_cost(start, self.outputs[product_type].loc[start])
* self.outputs[product_type].loc[start:end_excl]
)

self.bidding_strategies[product_type].calculate_reward(
unit=self,
marketconfig=marketconfig,
orderbook=orderbook,
)

def calculate_generation_cost(
self,
start: datetime,
end: datetime,
product_type: str,
):
if start not in self.index:
return
product_type_mc = product_type + "_marginal_costs"
for t in self.outputs[product_type_mc][start:end].index:
mc = self.calculate_marginal_cost(
start, self.outputs[product_type].loc[start]
)
self.outputs[product_type_mc][t] = mc * self.outputs[product_type][start]

def execute_current_dispatch(
self,
start: pd.Timestamp,
Expand Down Expand Up @@ -182,7 +200,7 @@ def get_output_before(self, dt: datetime, product_type: str = "energy") -> float
if dt - self.index.freq < self.index[0]:
return 0
else:
return self.outputs["energy"].at[dt - self.index.freq]
return self.outputs[product_type].at[dt - self.index.freq]

def as_dict(self) -> dict:
"""
Expand Down Expand Up @@ -291,19 +309,6 @@ def calculate_min_max_power(
"""
pass

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

def calculate_ramp(
self, previous_power: float, power: float, current_power: float = 0
) -> float:
Expand Down Expand Up @@ -532,19 +537,6 @@ def calculate_min_max_discharge(
"""
pass

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

def get_soc_before(self, dt: datetime) -> float:
"""
return SoC before the given datetime.
Expand Down Expand Up @@ -720,8 +712,7 @@ def calculate_reward(
:param orderbook: The orderbook.
:type orderbook: Orderbook
"""

pass
pass


class LearningStrategy(BaseStrategy):
Expand Down
12 changes: 5 additions & 7 deletions assume/common/units_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -293,15 +293,13 @@ def write_actual_dispatch(self, product_type: str):
end = now
current_dispatch.name = "power"
data = pd.DataFrame(current_dispatch)
data["soc"] = unit.outputs["soc"][start:end]
unit.calculate_generation_cost(start, now, "energy")
valid_outputs = ["soc", "cashflow", "marginal_costs", "total_costs"]

for key in unit.outputs.keys():
if "cashflow" in key:
data[key] = unit.outputs[key][start:end]
if "marginal_costs" in key:
data[key] = unit.outputs[key][start:end]
if "total_costs" in key:
data[key] = unit.outputs[key][start:end]
for output in valid_outputs:
if output in key:
data[key] = unit.outputs[key][start:end]

data["unit"] = unit_id
unit_dispatch_dfs.append(data)
Expand Down
2 changes: 1 addition & 1 deletion assume/markets/base_market.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def validate_registration(
Used to check if a participant is eligible to bid on this market
"""

# simple check that 1 MW can be bid at least
# simple check that 1 MW can be bid at least by powerplants
def requirement(unit: dict):
return unit.get("unit_type") != "power_plant" or abs(unit["max_power"]) >= 1

Expand Down
180 changes: 180 additions & 0 deletions tests/test_baseunit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# SPDX-FileCopyrightText: ASSUME Developers
#
# SPDX-License-Identifier: AGPL-3.0-or-later

from datetime import datetime, timedelta

import pandas as pd
import pytest

from assume.common.base import BaseStrategy, BaseUnit
from assume.common.forecasts import NaiveForecast
from assume.common.market_objects import MarketConfig, Orderbook, Product


class BasicStrategy(BaseStrategy):
def calculate_bids(
self,
unit: BaseUnit,
market_config: MarketConfig,
product_tuples: list[Product],
**kwargs,
) -> Orderbook:
bids = []
for product in product_tuples:
bids.append(
{
"start_time": product[0],
"end_time": product[1],
"only_hours": product[2],
"price": 10,
"volume": 20,
}
)
return bids


@pytest.fixture
def base_unit() -> BaseUnit:
# Create a PowerPlant instance with some example parameters
index = pd.date_range("2022-01-01", periods=4, freq="H")
ff = NaiveForecast(
index, availability=1, fuel_price=[10, 11, 12, 13], co2_price=[10, 20, 30, 30]
)
return BaseUnit(
id="test_pp",
unit_operator="test_operator",
technology="base",
bidding_strategies={"energy": BasicStrategy()},
index=index,
)


def test_init_function(base_unit, mock_market_config):
assert base_unit.id == "test_pp"
assert base_unit.unit_operator == "test_operator"
assert base_unit.technology == "base"
assert base_unit.as_dict() == {
"id": "test_pp",
"technology": "base",
"unit_operator": "test_operator",
"node": "",
"unit_type": "base_unit",
}


def test_output_before(base_unit, mock_market_config):
index = base_unit.index
assert base_unit.get_output_before(index[0], "energy") == 0
assert base_unit.get_output_before(index[0], "fictional_product_type") == 0

base_unit.outputs["energy"][index[0]] = 100
assert base_unit.get_output_before(index[1], "energy") == 100
assert base_unit.get_output_before(index[0], "energy") == 0

base_unit.outputs["fictional_product_type"][index[0]] = 200
assert base_unit.get_output_before(index[1], "fictional_product_type") == 200
assert base_unit.get_output_before(index[0], "fictional_product_type") == 0


def test_calculate_bids(base_unit, mock_market_config):
index = base_unit.index
start = index[0]
end = index[1]
product_tuples = [(start, end, None)]
bids = base_unit.calculate_bids(mock_market_config, product_tuples)
assert bids == [
{
"start_time": start,
"end_time": end,
"only_hours": None,
"price": 10,
"volume": 20,
}
]

orderbook = [
{
"start_time": start,
"end_time": end,
"only_hours": None,
"price": 10,
"volume": 10,
"accepted_price": 11,
"accepted_volume": 10,
}
]
# mock calculate_marginal_cost
base_unit.calculate_marginal_cost = lambda *x: 10
base_unit.set_dispatch_plan(mock_market_config, orderbook)
base_unit.calculate_generation_cost(index[0], index[1], "energy")

# we apply the dispatch plan of 10 MW
assert base_unit.outputs["energy"][start] == 10
assert base_unit.outputs["energy_marginal_costs"][start] == 10 * 10
# we received more, as accepted_price is higher
assert base_unit.outputs["energy_cashflow"][start] == 10 * 11

# we somehow sold an additional 10 MW
base_unit.set_dispatch_plan(mock_market_config, orderbook)
base_unit.calculate_generation_cost(index[0], index[1], "energy")

# the final output should be 10+10
assert base_unit.outputs["energy"][start] == 20
# the marginal cost for this volume should be twice as much too
assert base_unit.outputs["energy_marginal_costs"][start] == 200
assert base_unit.outputs["energy_cashflow"][start] == 20 * 11


def test_calculate_multi_bids(base_unit, mock_market_config):
index = base_unit.index

# when we set
orderbook = [
{
"start_time": index[0],
"end_time": index[1],
"only_hours": None,
"price": 10,
"volume": 10,
"accepted_price": 11,
"accepted_volume": 10,
},
{
"start_time": index[1],
"end_time": index[2],
"only_hours": None,
"price": 10,
"volume": 10,
"accepted_price": 11,
"accepted_volume": 10,
},
]
# mock calculate_marginal_cost
base_unit.calculate_marginal_cost = lambda *x: 10
base_unit.set_dispatch_plan(mock_market_config, orderbook)
base_unit.calculate_generation_cost(index[0], index[1], "energy")

assert base_unit.outputs["energy"][index[0]] == 10
assert base_unit.outputs["energy_marginal_costs"][index[0]] == 100
assert base_unit.outputs["energy_cashflow"][index[0]] == 110
assert base_unit.outputs["energy"][index[1]] == 10
assert base_unit.outputs["energy_marginal_costs"][index[1]] == 100
assert base_unit.outputs["energy_cashflow"][index[1]] == 110

base_unit.set_dispatch_plan(mock_market_config, orderbook)
base_unit.calculate_generation_cost(index[0], index[1], "energy")

# should be correctly applied for the sum, even if different hours are applied
assert base_unit.outputs["energy"][index[0]] == 20
assert base_unit.outputs["energy_marginal_costs"][index[0]] == 200
assert base_unit.outputs["energy_cashflow"][index[0]] == 220
assert base_unit.outputs["energy"][index[1]] == 20
assert base_unit.outputs["energy_marginal_costs"][index[1]] == 200
assert base_unit.outputs["energy_cashflow"][index[1]] == 220

# in base_unit - this should not do anything but get return the energy dispatch
assert (
base_unit.execute_current_dispatch(index[0], index[2])
== base_unit.outputs["energy"][index[0] : index[2]]
).all()
8 changes: 4 additions & 4 deletions tests/test_flexable_storage_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
)
from assume.units import Storage

start = datetime(2023, 7, 1)
end = datetime(2023, 7, 2)


@pytest.fixture
def storage() -> Storage:
Expand Down Expand Up @@ -50,7 +47,8 @@ def storage() -> Storage:

def test_flexable_eom_storage(mock_market_config, storage):
index = pd.date_range("2023-07-01", periods=4, freq="H")
end = datetime(2023, 7, 1, 1, 0, 0)
start = datetime(2023, 7, 1)
end = datetime(2023, 7, 1, 1)
strategy = flexableEOMStorage()
mc = mock_market_config
product_tuples = [(start, end, None)]
Expand Down Expand Up @@ -129,6 +127,7 @@ def test_flexable_eom_storage(mock_market_config, storage):

def test_flexable_pos_crm_storage(mock_market_config, storage):
index = pd.date_range("2023-07-01", periods=4, freq="H")
start = datetime(2023, 7, 1)
end = datetime(2023, 7, 1, 4, 0, 0)
strategy = flexablePosCRMStorage()
mc = mock_market_config
Expand Down Expand Up @@ -169,6 +168,7 @@ def test_flexable_pos_crm_storage(mock_market_config, storage):

def test_flexable_neg_crm_storage(mock_market_config, storage):
index = pd.date_range("2023-07-01", periods=4, freq="H")
start = datetime(2023, 7, 1)
end = datetime(2023, 7, 1, 4, 0, 0)
strategy = flexableNegCRMStorage()
mc = mock_market_config
Expand Down
2 changes: 1 addition & 1 deletion tests/test_flexable_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def power_plant() -> PowerPlant:


def test_flexable_eom(mock_market_config, power_plant):
end = datetime(2023, 7, 1, 1, 0, 0)
end = datetime(2023, 7, 1, 1)
strategy = flexableEOM()
mc = mock_market_config
product_tuples = [(start, end, None)]
Expand Down

0 comments on commit 05acc78

Please sign in to comment.