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

Correct cost calculations and storage operation #206

Merged
merged 7 commits into from
Sep 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
86 changes: 83 additions & 3 deletions assume/common/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,17 @@
cashflow * hours
)

def get_starting_costs(self, op_time: int):
"""
op_time is hours running from get_operation_time
returns the costs if start_up is planned
:param op_time: operation time
:type op_time: int
:return: start_costs
:rtype: float
"""
return 0

Check warning on line 237 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L237

Added line #L237 was not covered by tests


class SupportsMinMax(BaseUnit):
"""
Expand Down Expand Up @@ -368,6 +379,75 @@
runn += 1
return (-1) ** is_off * runn

def get_average_operation_times(self, start: datetime):
"""
calculates the average uninterupted operation time
:param start: the current time
:type start: datetime
:return: avg_op_time
:rtype: float
:return: avg_down_time
:rtype: float
"""
op_series = []

before = start - self.index.freq
arr = self.outputs["energy"][self.index[0] : before][::-1] > 0

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

op_series = []
status = arr.iloc[0]
runn = 0
for val in arr:
if val == status:
runn += 1

Check warning on line 406 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L401-L406

Added lines #L401 - L406 were not covered by tests
else:
op_series.append(-((-1) ** status) * runn)
runn = 0
status = val
op_series.append(-((-1) ** status) * runn)

Check warning on line 411 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L408-L411

Added lines #L408 - L411 were not covered by tests

op_times = [operation for operation in op_series if operation > 0]
if op_times == []:
avg_op_time = self.min_operating_time

Check warning on line 415 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L413-L415

Added lines #L413 - L415 were not covered by tests
else:
avg_op_time = sum(op_times) / len(op_times)

Check warning on line 417 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L417

Added line #L417 was not covered by tests

down_times = [operation for operation in op_series if operation < 0]
if down_times == []:
avg_down_time = self.min_down_time

Check warning on line 421 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L419-L421

Added lines #L419 - L421 were not covered by tests
else:
avg_down_time = abs(sum(down_times) / len(down_times))

Check warning on line 423 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L423

Added line #L423 was not covered by tests

return max(1, avg_op_time), max(1, avg_down_time)

Check warning on line 425 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L425

Added line #L425 was not covered by tests

def get_starting_costs(self, op_time: int):
"""
op_time is hours running from get_operation_time
returns the costs if start_up is planned
:param op_time: operation time
:type op_time: int
:return: start_costs
:rtype: float
"""
if op_time > 0:
# unit is running
return 0

Check warning on line 438 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L438

Added line #L438 was not covered by tests

if self.downtime_hot_start is not None and self.hot_start_cost is not None:
if -op_time <= self.downtime_hot_start:
return self.hot_start_cost
if self.downtime_warm_start is not None and self.warm_start_cost is not None:
if -op_time <= self.downtime_warm_start:
return self.warm_start_cost
if self.cold_start_cost is not None:
return self.cold_start_cost

Check warning on line 447 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L443-L447

Added lines #L443 - L447 were not covered by tests

return 0

Check warning on line 449 in assume/common/base.py

View check run for this annotation

Codecov / codecov/patch

assume/common/base.py#L449

Added line #L449 was not covered by tests


class SupportsMinMaxCharge(BaseUnit):
"""
Expand Down Expand Up @@ -473,7 +553,7 @@
:return: the SoC before the given datetime
:rtype: float
"""
if dt - self.index.freq < self.index[0]:
if dt - self.index.freq <= self.index[0]:
return self.initial_soc
else:
return self.outputs["soc"].at[dt - self.index.freq]
Expand All @@ -493,7 +573,7 @@

def calculate_ramp_discharge(
self,
previous_soc: float,
soc: float,
previous_power: float,
power_discharge: float,
current_power: float = 0,
Expand Down Expand Up @@ -539,7 +619,7 @@

def calculate_ramp_charge(
self,
previous_soc: float,
soc: float,
previous_power: float,
power_charge: float,
current_power: float = 0,
Expand Down
2 changes: 1 addition & 1 deletion assume/common/forecasts.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ def __getitem__(self, column: str) -> pd.Series:
value = self.fuel_price
elif "demand" in column:
value = self.demand
elif column == "price_forecast":
elif column == "price_EOM":
value = self.price_forecast
else:
value = 0
Expand Down
9 changes: 5 additions & 4 deletions assume/common/units_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,12 +220,13 @@ def write_actual_dispatch(self):
current_dispatch.name = "power"
data = pd.DataFrame(current_dispatch)
data["soc"] = unit.outputs["soc"][start:end]
# TODO make that right for all products

for key in unit.outputs.keys():
if "energy_cashflow" in key:
if "cashflow" in key:
data[key] = unit.outputs[key][start:end]

if "energy_marginal_costs" in key:
if "marginal_costs" in key:
data[key] = unit.outputs[key][start:end]
if "total_costs" in key:
data[key] = unit.outputs[key][start:end]

data["unit"] = unit_id
Expand Down
4 changes: 2 additions & 2 deletions assume/markets/clearing_algorithms/complex_clearing.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,8 @@ def extract_results(
meta.append(
{
"supply_volume": supply_volume,
"demand_volume": demand_volume,
"demand_volume_energy": demand_volume * duration_hours,
"demand_volume": -demand_volume,
"demand_volume_energy": -demand_volume * duration_hours,
"supply_volume_energy": supply_volume * duration_hours,
"price": clear_price,
"max_price": clear_price,
Expand Down
2 changes: 1 addition & 1 deletion assume/strategies/dmas_powerplant.py
Original file line number Diff line number Diff line change
Expand Up @@ -538,7 +538,7 @@ def calculate_bids(
hour_count = len(product_tuples)
hour_count2 = hour_count * 2

base_price = unit.forecaster["price_forecast"][
base_price = unit.forecaster["price_EOM"][
start : start + timedelta(hours=hour_count2 - 1)
]
e_price = unit.forecaster.get_price("co2")[
Expand Down
8 changes: 4 additions & 4 deletions assume/strategies/dmas_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,12 +188,12 @@ def optimize_result(self, unit: SupportsMinMaxCharge, committed_power: np.array)
)
abs_difference = [self.model.plus[t] + self.model.minus[t] for t in time_range]
costs = [
abs_difference[t] * np.abs(unit.forecaster["price_forecast"][t] * 2)
abs_difference[t] * np.abs(unit.forecaster["price_EOM"][t] * 2)
for t in time_range
]

profit = [
-self.power[t] * unit.forecaster["price_forecast"][t] - costs[t]
-self.power[t] * unit.forecaster["price_EOM"][t] - costs[t]
for t in time_range
]
self.model.obj = Objective(
Expand Down Expand Up @@ -223,7 +223,7 @@ def optimize(
opt_results = {key: np.zeros(hour_count) for key in PRICE_FUNCS.keys()}
time_range = range(hour_count)

base_price = unit.forecaster["price_forecast"][
base_price = unit.forecaster["price_EOM"][
start : start + timedelta(hours=hour_count)
]

Expand Down Expand Up @@ -286,7 +286,7 @@ def calculate_bids(
opt_results = self.optimize(unit, start, hour_count)
total_orders = {}
block_id = 0
power_prices = unit.forecaster["price_forecast"][
power_prices = unit.forecaster["price_EOM"][
start : start + timedelta(hours=hour_count)
]
for key, power in opt_results.items():
Expand Down
Loading