diff --git a/assume/common/base.py b/assume/common/base.py index 5627eb66..a5c4f378 100644 --- a/assume/common/base.py +++ b/assume/common/base.py @@ -225,6 +225,17 @@ def calculate_cashflow(self, product_type: str, orderbook: Orderbook): 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 + class SupportsMinMax(BaseUnit): """ @@ -368,6 +379,75 @@ def get_operation_time(self, start: datetime): 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 + else: + op_series.append(-((-1) ** status) * runn) + runn = 0 + status = val + op_series.append(-((-1) ** status) * runn) + + op_times = [operation for operation in op_series if operation > 0] + if op_times == []: + avg_op_time = self.min_operating_time + else: + avg_op_time = sum(op_times) / len(op_times) + + down_times = [operation for operation in op_series if operation < 0] + if down_times == []: + avg_down_time = self.min_down_time + else: + avg_down_time = abs(sum(down_times) / len(down_times)) + + return max(1, avg_op_time), max(1, avg_down_time) + + 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 + + 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 + + return 0 + class SupportsMinMaxCharge(BaseUnit): """ @@ -473,7 +553,7 @@ def get_soc_before(self, dt: datetime) -> float: :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] @@ -493,7 +573,7 @@ def get_clean_spread(self, prices: pd.DataFrame): def calculate_ramp_discharge( self, - previous_soc: float, + soc: float, previous_power: float, power_discharge: float, current_power: float = 0, @@ -539,7 +619,7 @@ def calculate_ramp_discharge( def calculate_ramp_charge( self, - previous_soc: float, + soc: float, previous_power: float, power_charge: float, current_power: float = 0, diff --git a/assume/common/forecasts.py b/assume/common/forecasts.py index 496c8199..7865defd 100644 --- a/assume/common/forecasts.py +++ b/assume/common/forecasts.py @@ -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 diff --git a/assume/common/units_operator.py b/assume/common/units_operator.py index b2dafa96..27e05030 100644 --- a/assume/common/units_operator.py +++ b/assume/common/units_operator.py @@ -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 diff --git a/assume/markets/clearing_algorithms/complex_clearing.py b/assume/markets/clearing_algorithms/complex_clearing.py index 808e8cba..9bad8a87 100644 --- a/assume/markets/clearing_algorithms/complex_clearing.py +++ b/assume/markets/clearing_algorithms/complex_clearing.py @@ -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, diff --git a/assume/strategies/dmas_powerplant.py b/assume/strategies/dmas_powerplant.py index a8fbb749..5fdc699d 100644 --- a/assume/strategies/dmas_powerplant.py +++ b/assume/strategies/dmas_powerplant.py @@ -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")[ diff --git a/assume/strategies/dmas_storage.py b/assume/strategies/dmas_storage.py index 4825b372..c0475b70 100644 --- a/assume/strategies/dmas_storage.py +++ b/assume/strategies/dmas_storage.py @@ -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( @@ -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) ] @@ -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(): diff --git a/assume/strategies/flexable.py b/assume/strategies/flexable.py index 472770d7..ddd3f593 100644 --- a/assume/strategies/flexable.py +++ b/assume/strategies/flexable.py @@ -48,6 +48,9 @@ def calculate_bids( min_power, max_power = unit.calculate_min_max_power(start, end) bids = [] + op_time = unit.get_operation_time(start) + avg_op_time, avg_down_time = unit.get_average_operation_times(start) + for product in product_tuples: bid_quantity_inflex, bid_price_inflex = 0, 0 bid_quantity_flex, bid_price_flex = 0, 0 @@ -83,17 +86,23 @@ def calculate_bids( # ============================================================================= # Calculating possible price # ============================================================================= - if unit.get_operation_time(start) > 0: + if op_time > 0: bid_price_inflex = calculate_EOM_price_if_on( unit, start, - marginal_cost_inflex, + marginal_cost_flex, bid_quantity_inflex, self.foresight, + avg_down_time, ) else: bid_price_inflex = calculate_EOM_price_if_off( - unit, start, marginal_cost_flex, bid_quantity_inflex + unit, + start, + marginal_cost_flex, + bid_quantity_inflex, + op_time, + avg_op_time, ) if unit.outputs["heat"][start] > 0: @@ -104,10 +113,7 @@ def calculate_bids( power_loss_ratio = 0.0 # Flex-bid price formulation - if ( - unit.get_operation_time(start) <= -unit.min_down_time - or unit.get_operation_time(start) > 0 - ): + if op_time <= -unit.min_down_time or op_time > 0: bid_quantity_flex = max_power[start] - bid_quantity_inflex bid_price_flex = (1 - power_loss_ratio) * marginal_cost_flex @@ -131,9 +137,27 @@ def calculate_bids( ) # calculate previous power with planned dispatch (bid_quantity) previous_power = bid_quantity_inflex + bid_quantity_flex + current_power + if previous_power > 0: + op_time = max(op_time, 0) + 1 + else: + op_time = min(op_time, 0) - 1 return bids + def calculate_reward( + self, + unit, + marketconfig: MarketConfig, + orderbook: Orderbook, + ): + # TODO: Calculate profits over all markets + + calculate_reward_EOM( + unit=unit, + marketconfig=marketconfig, + orderbook=orderbook, + ) + class flexableEOMBlock(BaseStrategy): """ @@ -178,6 +202,8 @@ def calculate_bids( bids = [] bid_quantity_block = {} bid_price_block = [] + op_time = unit.get_operation_time(start) + avg_op_time, avg_down_time = unit.get_average_operation_times(start) for product in product_tuples: bid_quantity_flex, bid_price_flex = 0, 0 @@ -188,11 +214,11 @@ def calculate_bids( current_power = unit.outputs["energy"].at[start] - # adjust for ramp down speed + # adjust for ramp speed max_power[start] = unit.calculate_ramp( previous_power, max_power[start], current_power ) - # adjust for ramp up speed + # adjust for ramp speed min_power[start] = unit.calculate_ramp( previous_power, min_power[start], current_power ) @@ -214,17 +240,23 @@ def calculate_bids( # ============================================================================= # Calculating possible price # ============================================================================= - if unit.get_operation_time(start) > 0: + if op_time > 0: bid_price_inflex = calculate_EOM_price_if_on( unit, start, - marginal_cost_inflex, + marginal_cost_flex, bid_quantity_inflex, self.foresight, + avg_down_time, ) else: bid_price_inflex = calculate_EOM_price_if_off( - unit, start, marginal_cost_flex, bid_quantity_inflex + unit, + start, + marginal_cost_inflex, + bid_quantity_inflex, + op_time, + avg_op_time, ) if unit.outputs["heat"][start] > 0: @@ -240,9 +272,7 @@ def calculate_bids( or unit.get_operation_time(start) > 0 ): bid_quantity_flex = max_power[start] - bid_quantity_inflex - bid_price_flex = ( - 1 - power_loss_ratio - ) * marginal_cost_flex + bid_price_inflex # hihger price than inflex + bid_price_flex = (1 - power_loss_ratio) * marginal_cost_flex bid_quantity_block[product[0]] = bid_quantity_inflex bid_price_block.append(bid_price_inflex) @@ -259,6 +289,10 @@ def calculate_bids( ) # calculate previous power with planned dispatch (bid_quantity) previous_power = bid_quantity_inflex + bid_quantity_flex + current_power + if previous_power > 0: + op_time = max(op_time, 0) + 1 + else: + op_time = min(op_time, 0) - 1 bids.append( { @@ -275,6 +309,20 @@ def calculate_bids( return bids + def calculate_reward( + self, + unit, + marketconfig: MarketConfig, + orderbook: Orderbook, + ): + # TODO: Calculate profits over all markets + + calculate_reward_EOM( + unit=unit, + marketconfig=marketconfig, + orderbook=orderbook, + ) + class flexablePosCRM(BaseStrategy): """ @@ -477,6 +525,8 @@ def calculate_EOM_price_if_off( start, marginal_cost_inflex, bid_quantity_inflex, + op_time, + avg_op_time=1, ): """ The powerplant is currently off and calculates a startup markup as an extra @@ -492,17 +542,14 @@ def calculate_EOM_price_if_off( :return: The bid price of the unit :rtype: float """ - av_operating_time = max((unit.outputs["energy"][:start] > 0).mean(), 1) - # 1 prevents division by 0 - op_time = unit.get_operation_time(start) starting_cost = unit.get_starting_costs(op_time) # if we split starting_cost across av_operating_time # we are never adding the other parts of the cost to the following hours if bid_quantity_inflex == 0: - markup = starting_cost / av_operating_time + markup = starting_cost / avg_op_time else: - markup = starting_cost / av_operating_time / bid_quantity_inflex + markup = starting_cost / avg_op_time / bid_quantity_inflex bid_price_inflex = min(marginal_cost_inflex + markup, 3000.0) @@ -512,12 +559,13 @@ def calculate_EOM_price_if_off( def calculate_EOM_price_if_on( unit: SupportsMinMax, start, - marginal_cost_inflex, + marginal_cost_flex, bid_quantity_inflex, foresight, + avg_down_time=1, ): """ - Check the description provided by Thomas in last version, the average downtime is not available + Check the description provided by Thomas in last version, the average downtime is available here The powerplant is currently on :param unit: A unit that the unit operator manages @@ -535,11 +583,11 @@ def calculate_EOM_price_if_on( return 0 t = start - op_time = unit.get_operation_time(start) + # TODO is it correct to bill for cold, hot and warm starts in one start? - starting_cost = unit.get_starting_costs(op_time) + starting_cost = unit.get_starting_costs(-max(avg_down_time, 1)) - price_reduction_restart = starting_cost / unit.min_down_time / bid_quantity_inflex + price_reduction_restart = starting_cost / avg_down_time / bid_quantity_inflex if unit.outputs["heat"][t] > 0: heat_gen_cost = ( @@ -551,18 +599,15 @@ def calculate_EOM_price_if_on( possible_revenue = get_specific_revenue( unit=unit, - marginal_cost=marginal_cost_inflex, + marginal_cost=marginal_cost_flex, t=start, foresight=foresight, ) - if ( - possible_revenue >= 0 - and unit.forecaster["price_forecast"][t] < marginal_cost_inflex - ): - marginal_cost_inflex = 0 + if possible_revenue >= 0 and unit.forecaster["price_EOM"][t] < marginal_cost_flex: + marginal_cost_flex = 0 bid_price_inflex = max( - -price_reduction_restart - heat_gen_cost + marginal_cost_inflex, + -price_reduction_restart - heat_gen_cost + marginal_cost_flex, -499.00, ) @@ -608,11 +653,47 @@ def get_specific_revenue( """ price_forecast = [] - if t + foresight > unit.forecaster["price_forecast"].index[-1]: - price_forecast = unit.forecaster["price_forecast"][t:] + if t + foresight > unit.forecaster["price_EOM"].index[-1]: + price_forecast = unit.forecaster["price_EOM"][t:] else: - price_forecast = unit.forecaster["price_forecast"][t : t + foresight] + price_forecast = unit.forecaster["price_EOM"][t : t + foresight] possible_revenue = (price_forecast - marginal_cost).sum() return possible_revenue + + +def calculate_reward_EOM( + unit, + marketconfig: MarketConfig, + orderbook: Orderbook, +): + """ + Calculate reward (costs and profit) + :param unit: Unit to calculate reward for + :type unit: SupportsMinMax + :param marketconfig: Market configuration + :type marketconfig: MarketConfig + :param orderbook: Orderbook + :type orderbook: Orderbook + """ + # TODO: Calculate profits over all markets + product_type = marketconfig.product_type + + for order in orderbook: + start = order["start_time"] + end = order["end_time"] + end_excl = end - unit.index.freq + index = pd.date_range(start, end_excl, freq=unit.index.freq) + costs = pd.Series(unit.fixed_cost, index=index) + for start in index: + if unit.outputs[product_type][start] != 0: + op_time = unit.get_operation_time(start - unit.index.freq) + costs[start] += unit.get_starting_costs(op_time) + costs[start] += unit.outputs[product_type][ + start + ] * unit.calculate_marginal_cost( + start, unit.outputs[product_type][start] + ) + + unit.outputs["total_costs"][index] = costs diff --git a/assume/strategies/flexable_storage.py b/assume/strategies/flexable_storage.py index ba8e45f1..b18c6f73 100644 --- a/assume/strategies/flexable_storage.py +++ b/assume/strategies/flexable_storage.py @@ -1,3 +1,5 @@ +from datetime import timedelta + import numpy as np import pandas as pd @@ -44,14 +46,7 @@ def calculate_bids( previous_power = unit.get_output_before(start) - # get the dispatched power from last day - dispatch = np.where( - unit.outputs["energy"] > 0, - (unit.outputs["energy"] / unit.efficiency_discharge), - (unit.outputs["energy"] * unit.efficiency_charge), - ) - soc = unit.get_soc_before(start) - theoretic_SOC = soc - ((dispatch / unit.max_volume)).sum() + theoretic_SOC = unit.outputs["soc"][start] min_power_charge, max_power_charge = unit.calculate_min_max_charge( start, end_all @@ -75,7 +70,7 @@ def calculate_bids( previous_power, max_power_discharge[start], current_power_discharge, - min_power_discharge.iloc[0], + min_power_discharge[start], ) min_power_discharge[start] = unit.calculate_ramp_discharge( theoretic_SOC, @@ -99,7 +94,7 @@ def calculate_bids( min_power_charge[start], ) - price_forecast = unit.forecaster["price_forecast"] + price_forecast = unit.forecaster["price_EOM"] average_price = calculate_price_average( unit=unit, @@ -113,6 +108,7 @@ def calculate_bids( elif price_forecast[start] <= average_price * unit.efficiency_charge: bid_quantity = max_power_charge[start] else: + previous_power = current_power continue bids.append( @@ -124,17 +120,64 @@ def calculate_bids( "volume": bid_quantity, } ) - previous_power = bid_quantity + current_power - if previous_power > 0: - theoretic_SOC -= ( - bid_quantity + current_power - ) / unit.efficiency_discharge - elif previous_power < 0: - theoretic_SOC -= (bid_quantity + current_power) * unit.efficiency_charge + time_delta = (end - start) / timedelta(hours=1) + if bid_quantity + current_power > 0: + delta_soc = -( + (bid_quantity + current_power) + * time_delta + / unit.efficiency_discharge + / unit.max_volume + ) + elif bid_quantity + current_power < 0: + delta_soc = -( + (bid_quantity + current_power) + * time_delta + * unit.efficiency_charge + / unit.max_volume + ) + else: + delta_soc = 0 + + theoretic_SOC += delta_soc + previous_power = bid_quantity + current_power return bids + def calculate_reward( + self, + unit, + marketconfig: MarketConfig, + orderbook: Orderbook, + ): + """ + Calculate reward (costs) + :param unit: Unit to calculate reward for + :type unit: SupportsMinMax + :param marketconfig: Market configuration + :type marketconfig: MarketConfig + :param orderbook: Orderbook + :type orderbook: Orderbook + """ + # TODO: Calculate profits over all markets + product_type = marketconfig.product_type + + for order in orderbook: + start = order["start_time"] + end = order["end_time"] + end_excl = end - unit.index.freq + index = pd.date_range(start, end_excl, freq=unit.index.freq) + costs = pd.Series(unit.fixed_cost, index=index) + for start in index: + if unit.outputs[product_type][start] != 0: + costs[start] += unit.outputs[product_type][ + start + ] * unit.calculate_marginal_cost( + start, unit.outputs[product_type][start] + ) + + unit.outputs["total_costs"][index] = costs + class flexablePosCRMStorage(BaseStrategy): def __init__(self, *args, **kwargs): @@ -175,13 +218,13 @@ def calculate_bids( start, end ) bids = [] + theoretic_SOC = unit.outputs["soc"][start] for product in product_tuples: start = product[0] current_power = unit.outputs["energy"].at[start] - soc = unit.get_soc_before(start) bid_quantity = unit.calculate_ramp_discharge( - soc, + theoretic_SOC, previous_power, max_power_discharge[start], current_power, @@ -199,7 +242,7 @@ def calculate_bids( marginal_cost=marginal_cost, current_time=start, foresight=self.foresight, - price_forecast=unit.forecaster["price_forecast"], + price_forecast=unit.forecaster["price_EOM"], ) if specific_revenue >= 0: @@ -208,8 +251,8 @@ def calculate_bids( capacity_price = ( abs(specific_revenue) * unit.min_power_discharge / bid_quantity ) - soc = unit.get_soc_before(start) - energy_price = capacity_price / (soc * unit.max_volume) + + energy_price = capacity_price / (theoretic_SOC * unit.max_volume) if market_config.product_type == "capacity_pos": bids.append( @@ -221,6 +264,7 @@ def calculate_bids( "volume": bid_quantity, } ) + previous_power = current_power elif market_config.product_type == "energy_pos": bids.append( { @@ -231,11 +275,20 @@ def calculate_bids( "volume": bid_quantity, } ) + time_delta = (end - start) / timedelta(hours=1) + delta_soc = -( + (bid_quantity + current_power) + * time_delta + / unit.efficiency_discharge + / unit.max_volume + ) + theoretic_SOC += delta_soc + previous_power = bid_quantity + current_power else: + previous_power = current_power raise ValueError( f"Product {market_config.product_type} is not supported by this strategy." ) - previous_power = bid_quantity + current_power return bids @@ -272,7 +325,7 @@ def calculate_bids( end = product_tuples[-1][1] previous_power = unit.get_output_before(start) - soc = unit.get_soc_before(start) + theoretic_SOC = unit.outputs["soc"][start] min_power_charge, max_power_charge = unit.calculate_min_max_charge(start, end) @@ -281,7 +334,7 @@ def calculate_bids( start = product[0] current_power = unit.outputs["energy"].at[start] bid_quantity = unit.calculate_ramp_charge( - soc, + theoretic_SOC, previous_power, max_power_charge[start], current_power, @@ -289,6 +342,7 @@ def calculate_bids( # if bid_quantity >= min_bid_volume --> not checked here if bid_quantity == 0: + previous_power = current_power continue if market_config.product_type == "capacity_neg": @@ -301,6 +355,7 @@ def calculate_bids( "volume": bid_quantity, } ) + previous_power = current_power elif market_config.product_type == "energy_neg": bids.append( { @@ -311,11 +366,20 @@ def calculate_bids( "volume": bid_quantity, } ) + time_delta = (end - start) / timedelta(hours=1) + delta_soc = ( + (bid_quantity + current_power) + * time_delta + * unit.efficiency_charge + / unit.max_volume + ) + theoretic_SOC += delta_soc + previous_power = bid_quantity + current_power else: + previous_power = current_power raise ValueError( f"Product {market_config.product_type} is not supported by this strategy." ) - previous_power = current_power + bid_quantity return bids @@ -375,13 +439,13 @@ def get_specific_revenue(unit, marginal_cost, current_time, foresight, price_for ) possible_revenue = 0 - soc = unit.get_soc_before(current_time) + soc = unit.outputs["soc"][t] theoretic_SOC = soc previous_power = unit.get_output_before(t) for i, market_price in enumerate(price_forecast): theoretic_power_discharge = unit.calculate_ramp_discharge( - soc, + theoretic_SOC, previous_power=previous_power, power_discharge=max_power_discharge.iloc[i], ) diff --git a/assume/strategies/learning_strategies.py b/assume/strategies/learning_strategies.py index f32a0939..d1f52187 100644 --- a/assume/strategies/learning_strategies.py +++ b/assume/strategies/learning_strategies.py @@ -271,15 +271,14 @@ def create_observation( / scaling_factor_res_load ) - if end_excl + forecast_len > unit.forecaster["price_forecast"].index[-1]: + if end_excl + forecast_len > unit.forecaster["price_EOM"].index[-1]: scaled_price_forecast = ( - unit.forecaster["price_forecast"].loc[start:].values - / scaling_factor_price + unit.forecaster["price_EOM"].loc[start:].values / scaling_factor_price ) scaled_price_forecast = np.concatenate( [ scaled_price_forecast, - unit.forecaster["price_forecast"].iloc[ + unit.forecaster["price_EOM"].iloc[ : self.foresight - len(scaled_price_forecast) ], ] @@ -287,9 +286,7 @@ def create_observation( else: scaled_price_forecast = ( - unit.forecaster["price_forecast"] - .loc[start : end_excl + forecast_len] - .values + unit.forecaster["price_EOM"].loc[start : end_excl + forecast_len].values / scaling_factor_price ) diff --git a/assume/strategies/storage_strategies.py b/assume/strategies/storage_strategies.py deleted file mode 100644 index 1d3be600..00000000 --- a/assume/strategies/storage_strategies.py +++ /dev/null @@ -1,286 +0,0 @@ -from datetime import datetime - -import numpy as np -import pandas as pd - -from assume.common.base import BaseStrategy, SupportsMinMaxCharge -from assume.common.market_objects import MarketConfig, Orderbook, Product - - -class complexEOMStorage(BaseStrategy): - """ - complexEOMStorage is a strategy for storage units that are able to charge and discharge (Energy Only Market) - """ - - def __init__(self): - """ - :param foresight: [description], defaults to pd.Timedelta("12h") - :type foresight: [type], optional - """ - super().__init__() - - self.foresight = pd.Timedelta("12h") - - def calculate_bids( - self, - unit: SupportsMinMaxCharge, - market_config: MarketConfig, - product_tuples: list[Product], - **kwargs, - ) -> Orderbook: - """ - Takes information from a unit that the unit operator manages and - defines how it is dispatched to the market - - :param unit: the unit to be dispatched - :type unit: SupportsMinMax - :param market_config: the market configuration - :type market_config: MarketConfig - :param product_tuples: list of all products the unit can offer - :type product_tuples: list[Product] - :return: the bids consisting of the start time, end time, only hours, price and volume. - :rtype: Orderbook - - Strategy analogue to flexABLE - """ - product = product_tuples[0] - start = product[0] - end = product[1] - - min_power_charge, max_power_charge = unit.calculate_min_max_charge(start, end) - min_power_discharge, max_power_discharge = unit.calculate_min_max_discharge( - start, end - ) - - # ============================================================================= - # Storage Unit is either charging, discharging, or off - # ============================================================================= - bid_quantity_mr_discharge = min_power_discharge - bid_quantity_flex_discharge = max_power_discharge - min_power_discharge - bid_quantity_mr_charge = min_power_charge - bid_quantity_flex_charge = max_power_charge - min_power_charge - - cost_mr_discharge = unit.calculate_marginal_cost(start, min_power_discharge) - cost_flex_discharge = unit.calculate_marginal_cost(start, max_power_discharge) - cost_mr_charge = unit.calculate_marginal_cost(start, min_power_charge) - cost_flex_charge = unit.calculate_marginal_cost(start, max_power_charge) - - average_price = self.calculate_price_average(unit) - - previous_power = unit.get_output_before(start) - - price_forecast = unit.forecaster["price_EOM"][t : t + self.foresight] - - if price_forecast[start] >= average_price / unit.efficiency_discharge: - # place bid to discharge - if previous_power > 0: - # was discharging before - bid_price_mr = self.calculate_EOM_price_continue_discharging( - start, unit, cost_mr_discharge, bid_quantity_mr_discharge - ) - bid_quantity_mr = bid_quantity_mr_discharge - bid_price_flex = cost_flex_discharge - bid_quantity_flex = bid_quantity_flex_discharge - - elif previous_power < 0: - # was charging before - if unit.min_down_time > 0: - bid_quantity_mr = 0 - bid_price_mr = 0 - - else: - bid_price_mr = self.calculate_EOM_price_if_off( - unit, - cost_flex_discharge, - bid_quantity_mr_discharge, - ) - bid_quantity_mr = bid_quantity_mr_discharge - bid_price_flex = cost_flex_discharge - bid_quantity_flex = bid_quantity_flex_discharge - else: - bid_price_mr = 0 - bid_quantity_mr = 0 - bid_price_flex = 0 - bid_quantity_flex = 0 - - elif price_forecast[start] <= average_price * unit.efficiency_charge: - # place bid to charge - if previous_power > 0: - # was discharging before - if unit.min_down_time > 0: - bid_quantity_mr = 0 - bid_price_mr = 0 - else: - bid_price_mr = self.calculate_EOM_price_if_off( - unit, cost_mr_charge, bid_quantity_mr_charge - ) - bid_quantity_mr = bid_quantity_mr_charge - bid_price_flex = cost_flex_charge - bid_quantity_flex = bid_quantity_flex_charge - - elif previous_power < 0: - # was charging before - bid_price_mr = bid_quantity_mr_charge - bid_quantity_mr = cost_mr_charge - bid_price_flex = cost_flex_charge - bid_quantity_flex = bid_quantity_flex_charge - else: - bid_price_mr = 0 - bid_quantity_mr = 0 - bid_price_flex = 0 - bid_quantity_flex = 0 - - else: - bid_price_mr = 0 - bid_quantity_mr = 0 - bid_price_flex = 0 - bid_quantity_flex = 0 - - bids = [ - { - "start_time": product[0], - "end_time": product[1], - "only_hours": product[2], - "price": bid_price_mr, - "volume": bid_quantity_mr, - }, - { - "start_time": product[0], - "end_time": product[1], - "only_hours": product[2], - "price": bid_price_flex, - "volume": bid_quantity_flex, - }, - ] - return bids - - def calculate_price_average(self, unit: SupportsMinMaxCharge, t: datetime): - """ - Calculates the average price for the next 12 hours - Returns the average price - - :param unit: the unit to be dispatched - :type unit: SupportsMinMax - :param t: the current time - :type t: datetime - :return: the average price - :rtype: float - """ - average_price = np.mean( - unit.forecaster["price_EOM"][t - self.foresight : t + self.foresight] - ) - - return average_price - - def calculate_EOM_price_if_off(self, unit, marginal_cost_mr, bid_quantity_mr): - """ - Calculates the bid price if the unit is off - Returns the bid price - - :param unit: the unit to be dispatched - :type unit: SupportsMinMax - :param marginal_cost_mr: the marginal cost of the unit - :type marginal_cost_mr: float - :param bid_quantity_mr: the bid quantity of the unit - :type bid_quantity_mr: float - :return: the bid price - :rtype: float - """ - av_operating_time = max((unit.outputs["energy"][:start] > 0).mean(), 1) - # 1 prevents division by 0 - - op_time = unit.get_operation_time(start) - starting_cost = unit.get_starting_costs(op_time) - markup = starting_cost / av_operating_time / bid_quantity_mr - - bid_price_mr = min(marginal_cost_mr + markup, 3000.0) - - return bid_price_mr - - def calculate_EOM_price_continue_discharging( - self, start, unit, marginal_cost_flex, bid_quantity_mr - ): - """ - Calculates the bid price if the unit is discharging - Returns the bid price - - :param start: the start time of the product - :type start: datetime - :param unit: the unit to be dispatched - :type unit: SupportsMinMax - :param marginal_cost_flex: the marginal cost of the unit - :type marginal_cost_flex: float - :param bid_quantity_mr: the bid quantity of the unit - :type bid_quantity_mr: float - :return: the bid price - :rtype: float - """ - if bid_quantity_mr == 0: - return 0 - - t = start - op_time = unit.get_operation_time(start) - starting_cost = unit.get_starting_costs(op_time) - - price_reduction_restart = starting_cost / unit.min_down_time / bid_quantity_mr - - possible_revenue = self.get_possible_revenues( - marginal_cost=marginal_cost_flex, - unit=unit, - t=start, - ) - if ( - possible_revenue >= 0 - and unit.forecaster["price_EOM"][t] < marginal_cost_flex - ): - marginal_cost_flex = 0 - - bid_price_mr = max( - -price_reduction_restart + marginal_cost_flex, - -2999.00, - ) - - return bid_price_mr - - def get_starting_costs(self, time, unit): - """ - get the starting costs of the unit - Returns the starting costs - - :param time: the time the unit is off - :type time: float - :param unit: the unit to be dispatched - :type unit: SupportsMinMax - :return: the starting costs - :rtype: float - """ - if time < unit.downtime_hot_start: - return unit.hot_start_cost - - elif time < unit.downtime_warm_start: - return unit.warm_start_cost - - else: - return unit.cold_start_cost - - def get_possible_revenues(self, marginal_cost, unit, t): - """ - get the possible revenues of the unit - Returns the possible revenues - - :param marginal_cost: the marginal cost of the unit - :type marginal_cost: float - :param unit: the unit to be dispatched - :type unit: SupportsMinMax - :param t: the current time - :type t: datetime - :return: the possible revenues - :rtype: float - """ - price_forecast = unit.forecaster["price_EOM"][t : t + self.foresight] - - possible_revenue = sum( - marketPrice - marginal_cost for marketPrice in price_forecast - ) - - return possible_revenue diff --git a/assume/units/demand.py b/assume/units/demand.py index 20e301dd..1cc83989 100644 --- a/assume/units/demand.py +++ b/assume/units/demand.py @@ -109,7 +109,8 @@ def calculate_min_max_power( :return: the bid volume as both the minimum and maximum power output of the unit :rtype: tuple[pd.Series, pd.Series] """ - bid_volume = (self.volume - self.outputs[product_type]).loc[start:end] + end_excl = end - self.index.freq + bid_volume = (self.volume - self.outputs[product_type]).loc[start:end_excl] return bid_volume, bid_volume def calculate_marginal_cost(self, start: pd.Timestamp, power: float) -> float: diff --git a/assume/units/powerplant.py b/assume/units/powerplant.py index df18ebb2..11f35126 100644 --- a/assume/units/powerplant.py +++ b/assume/units/powerplant.py @@ -77,6 +77,7 @@ def __init__( min_power: float = 0.0, efficiency: float = 1.0, fixed_cost: float = 0.0, + variable_cost: float | pd.Series = 0.0, partial_load_eff: bool = False, fuel_type: str = "others", emission_factor: float = 0.0, @@ -120,9 +121,12 @@ def __init__( self.downtime_hot_start = downtime_hot_start / ( self.index.freq / timedelta(hours=1) ) - self.downtime_warm_start = downtime_warm_start + self.downtime_warm_start = downtime_warm_start / ( + 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 @@ -147,6 +151,7 @@ def execute_current_dispatch( ): """ Executes the current dispatch of the unit based on the provided timestamps. + The dispatch is only executed, if it is in the constraints given by the unit. Returns the volume of the unit within the given time range. :param start: the start time of the dispatch @@ -158,14 +163,24 @@ def execute_current_dispatch( """ end_excl = end - self.index.freq - # TODO ramp down and turn off only for relevant timesteps - if self.outputs["energy"][start:end_excl].mean() < self.min_power: - self.outputs["energy"].loc[start:end_excl] = 0 - - # TODO check if resulting power is < max_power - # if self.outputs["energy"][start:end_excl].max() > self.max_power: - # max_pow = self.outputs["energy"][start:end_excl].max() - # logger.error(f"{max_pow} greater than {self.max_power} - bidding twice?") + + max_power = ( + self.forecaster.get_availability(self.id)[start:end_excl] * self.max_power + ) + + for t in self.outputs["energy"][start:end_excl].index: + current_power = self.outputs["energy"][t] + previous_power = self.get_output_before(t) + + max_power_t = self.calculate_ramp(previous_power, max_power[t]) + min_power_t = self.calculate_ramp(previous_power, self.min_power) + + if current_power > max_power_t: + self.outputs["energy"][t] = max_power_t + + elif current_power < min_power_t and current_power > 0: + self.outputs["energy"][t] = 0 + return self.outputs["energy"].loc[start:end_excl] def calc_simple_marginal_cost( @@ -182,7 +197,7 @@ def calc_simple_marginal_cost( marginal_cost = ( fuel_price / self.efficiency + self.forecaster.get_price("co2") * self.emission_factor / self.efficiency - + self.fixed_cost + + self.variable_cost ) return marginal_cost @@ -241,10 +256,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] + ) + marginal_cost = ( fuel_price / efficiency + co2_price * self.emission_factor / efficiency - + self.fixed_cost + + variable_cost ) return marginal_cost @@ -332,17 +353,3 @@ def as_dict(self) -> dict: ) return unit_dict - - def get_starting_costs(self, op_time): - """ - op_time is hours running - """ - if op_time > 0: - # unit is running - return 0 - if -op_time < self.downtime_hot_start: - return self.hot_start_cost - elif -op_time < self.downtime_warm_start: - return self.warm_start_cost - else: - return self.cold_start_cost diff --git a/assume/units/storage.py b/assume/units/storage.py index 3d4c306c..5375cdf5 100644 --- a/assume/units/storage.py +++ b/assume/units/storage.py @@ -133,6 +133,7 @@ def __init__( self.max_power_discharge = max_power_discharge self.min_power_discharge = min_power_discharge self.initial_soc = initial_soc + self.outputs["soc"] = pd.Series(self.initial_soc, index=self.index, dtype=float) self.soc_tick = soc_tick @@ -179,7 +180,7 @@ def __init__( # The downtime before hot start of the storage unit. self.downtime_hot_start = downtime_hot_start # The downtime before warm start of the storage unit. - self.warm_start_cost = downtime_warm_start + self.downtime_warm_start = downtime_warm_start self.fixed_cost = fixed_cost @@ -200,45 +201,32 @@ def execute_current_dispatch(self, start: pd.Timestamp, end: pd.Timestamp): :rtype: pd.Series """ end_excl = end - self.index.freq + time_delta = self.index.freq / timedelta(hours=1) for t in self.outputs["energy"][start:end_excl].index: delta_soc = 0 - soc = self.get_soc_before(t) + soc = self.outputs["soc"][t] if self.outputs["energy"][t] > self.max_power_discharge: self.outputs["energy"][t] = self.max_power_discharge - logger.error( - f"The energy dispatched is greater the maximum power to discharge, dispatched amount is adjusted." - ) elif self.outputs["energy"][t] < self.max_power_charge: self.outputs["energy"][t] = self.max_power_charge - logger.error( - f"The energy dispatched is greater than the maximum power to charge, dispatched amount is adjusted." - ) elif ( self.outputs["energy"][t] < self.min_power_discharge and self.outputs["energy"][t] > self.min_power_charge and self.outputs["energy"][t] != 0 ): self.outputs["energy"][t] = 0 - logger.error( - f"The energy dispatched is between min_power_charge and min_power_discharge, no energy is dispatched" - ) # discharging if self.outputs["energy"][t] > 0: max_soc_discharge = self.calculate_soc_max_discharge(soc) if self.outputs["energy"][t] > max_soc_discharge: - if abs(self.outputs["energy"][t] - max_soc_discharge) > EPS: - logger.error( - f"The energy dispatched exceeds the minimum SOC significantly, the dispatched amount is adjusted." - ) self.outputs["energy"][t] = max_soc_discharge delta_soc = ( -self.outputs["energy"][t] - * self.index.freq - / timedelta(hours=1) + * time_delta / self.efficiency_discharge / self.max_volume ) @@ -248,21 +236,16 @@ def execute_current_dispatch(self, start: pd.Timestamp, end: pd.Timestamp): max_soc_charge = self.calculate_soc_max_charge(soc) if self.outputs["energy"][t] < max_soc_charge: - if abs(self.outputs["energy"][t] - max_soc_charge) > EPS: - logger.error( - f"The energy dispatched exceeds the maximum SOC, the dispatched amount is adjusted." - ) self.outputs["energy"][t] = max_soc_charge delta_soc = ( -self.outputs["energy"][t] - * self.index.freq - / timedelta(hours=1) + * time_delta * self.efficiency_charge / self.max_volume ) - self.outputs["soc"][t] = soc + delta_soc + self.outputs["soc"][t + self.index.freq :] = soc + delta_soc return self.outputs["energy"].loc[start:end_excl] @@ -288,32 +271,10 @@ def calculate_marginal_cost( ) efficiency = self.efficiency_charge - marginal_cost = variable_cost / efficiency + self.fixed_cost + marginal_cost = variable_cost / efficiency return marginal_cost - def as_dict(self) -> dict: - """ - Return the storage unit's attributes as a dictionary, including specific attributes. - - :return: the storage unit's attributes as a dictionary - :rtype: dict - """ - unit_dict = super().as_dict() - unit_dict.update( - { - "max_power_charge": self.max_power_charge, - "max_power_discharge": self.max_power_discharge, - "min_power_charge": self.min_power_charge, - "min_power_discharge": self.min_power_discharge, - "efficiency_charge": self.efficiency_discharge, - "efficiency_discharge": self.efficiency_charge, - "unit_type": "storage", - } - ) - - return unit_dict - def calculate_soc_max_discharge(self, soc) -> float: duration = self.index.freq / timedelta(hours=1) power = max( @@ -383,7 +344,7 @@ def calculate_min_max_charge( ) # restrict charging according to max_volume - max_soc_charge = self.calculate_soc_max_charge(self.get_soc_before(start)) + max_soc_charge = self.calculate_soc_max_charge(self.outputs["soc"][start]) max_power_charge = max_power_charge.clip(lower=max_soc_charge) return min_power_charge, max_power_charge @@ -430,21 +391,21 @@ def calculate_min_max_discharge( ) # restrict according to min_volume - max_soc_discharge = self.calculate_soc_max_discharge(self.get_soc_before(start)) + max_soc_discharge = self.calculate_soc_max_discharge(self.outputs["soc"][start]) max_power_discharge = max_power_discharge.clip(upper=max_soc_discharge) return min_power_discharge, max_power_discharge def calculate_ramp_discharge( self, - previous_soc: float, + soc: float, previous_power: float, power_discharge: float, current_power: float = 0, min_power_discharge: float = 0, ) -> float: power_discharge = super().calculate_ramp_discharge( - previous_soc, + soc, previous_power, power_discharge, current_power, @@ -452,9 +413,8 @@ def calculate_ramp_discharge( ) # restrict according to min_SOC - max_soc_discharge = self.calculate_soc_max_discharge(previous_soc) - if power_discharge > max_soc_discharge: - power_discharge = max_soc_discharge + max_soc_discharge = self.calculate_soc_max_discharge(soc) + power_discharge = min(power_discharge, max_soc_discharge) if power_discharge < min_power_discharge: power_discharge = 0 @@ -462,24 +422,23 @@ def calculate_ramp_discharge( def calculate_ramp_charge( self, - previous_soc: float, + soc: float, previous_power: float, power_charge: float, current_power: float = 0, min_power_charge: float = 0, ) -> float: power_charge = super().calculate_ramp_charge( - previous_soc, + soc, previous_power, power_charge, current_power, ) # restrict charging according to max_SOC - max_soc_charge = self.calculate_soc_max_charge(previous_soc) + max_soc_charge = self.calculate_soc_max_charge(soc) - if power_charge < max_soc_charge: - power_charge = max_soc_charge + power_charge = max(power_charge, max_soc_charge) if power_charge > min_power_charge: power_charge = 0 @@ -498,3 +457,25 @@ def get_starting_costs(self, op_time): return self.warm_start_cost else: return self.cold_start_cost + + def as_dict(self) -> dict: + """ + Return the storage unit's attributes as a dictionary, including specific attributes. + + :return: the storage unit's attributes as a dictionary + :rtype: dict + """ + unit_dict = super().as_dict() + unit_dict.update( + { + "max_power_charge": self.max_power_charge, + "max_power_discharge": self.max_power_discharge, + "min_power_charge": self.min_power_charge, + "min_power_discharge": self.min_power_discharge, + "efficiency_charge": self.efficiency_discharge, + "efficiency_discharge": self.efficiency_charge, + "unit_type": "storage", + } + ) + + return unit_dict diff --git a/examples/inputs/example_01c/price_forecasts.csv b/examples/inputs/example_01c/price_forecasts.csv deleted file mode 100644 index 917ecc2a..00000000 --- a/examples/inputs/example_01c/price_forecasts.csv +++ /dev/null @@ -1,486 +0,0 @@ -,EOM,CRM_pos,CRM_neg -2019-01-01 00:00:00,3.333333333333333,, -2019-01-01 01:00:00,13.666666666666666,, -2019-01-01 02:00:00,24.0,, -2019-01-01 03:00:00,33.875,, -2019-01-01 04:00:00,24.0,, -2019-01-01 05:00:00,13.666666666666666,, -2019-01-01 06:00:00,20.0,, -2019-01-01 07:00:00,24.0,, -2019-01-01 08:00:00,43.75,, -2019-01-01 09:00:00,11.666666666666666,, -2019-01-01 10:00:00,3.333333333333333,, -2019-01-01 11:00:00,24.0,, -2019-01-01 12:00:00,33.875,, -2019-01-01 13:00:00,24.0,, -2019-01-01 14:00:00,22.0,, -2019-01-01 15:00:00,3.333333333333333,, -2019-01-01 16:00:00,3.333333333333333,, -2019-01-01 17:00:00,20.0,, -2019-01-01 18:00:00,22.0,, -2019-01-01 19:00:00,20.0,, -2019-01-01 20:00:00,24.0,, -2019-01-01 21:00:00,24.0,, -2019-01-01 22:00:00,24.0,, -2019-01-01 23:00:00,33.875,, -2019-01-02 00:00:00,24.0,, -2019-01-02 01:00:00,24.0,, -2019-01-02 02:00:00,43.75,, -2019-01-02 03:00:00,33.875,, -2019-01-02 04:00:00,43.75,, -2019-01-02 05:00:00,43.75,, -2019-01-02 06:00:00,43.75,, -2019-01-02 07:00:00,43.75,, -2019-01-02 08:00:00,43.75,, -2019-01-02 09:00:00,24.0,, -2019-01-02 10:00:00,11.666666666666666,, -2019-01-02 11:00:00,24.0,, -2019-01-02 12:00:00,24.0,, -2019-01-02 13:00:00,43.75,, -2019-01-02 14:00:00,24.0,, -2019-01-02 15:00:00,24.0,, -2019-01-02 16:00:00,33.875,, -2019-01-02 17:00:00,43.75,, -2019-01-02 18:00:00,24.0,, -2019-01-02 19:00:00,24.0,, -2019-01-02 20:00:00,24.0,, -2019-01-02 21:00:00,24.0,, -2019-01-02 22:00:00,24.0,, -2019-01-02 23:00:00,33.875,, -2019-01-03 00:00:00,24.0,, -2019-01-03 01:00:00,24.0,, -2019-01-03 02:00:00,20.0,, -2019-01-03 03:00:00,11.666666666666666,, -2019-01-03 04:00:00,3.333333333333333,, -2019-01-03 05:00:00,3.333333333333333,, -2019-01-03 06:00:00,3.333333333333333,, -2019-01-03 07:00:00,24.0,, -2019-01-03 08:00:00,24.0,, -2019-01-03 09:00:00,24.0,, -2019-01-03 10:00:00,33.875,, -2019-01-03 11:00:00,24.0,, -2019-01-03 12:00:00,22.0,, -2019-01-03 13:00:00,20.0,, -2019-01-03 14:00:00,24.0,, -2019-01-03 15:00:00,24.0,, -2019-01-03 16:00:00,24.0,, -2019-01-03 17:00:00,24.0,, -2019-01-03 18:00:00,24.0,, -2019-01-03 19:00:00,24.0,, -2019-01-03 20:00:00,24.0,, -2019-01-03 21:00:00,33.875,, -2019-01-03 22:00:00,24.0,, -2019-01-03 23:00:00,24.0,, -2019-01-04 00:00:00,24.0,, -2019-01-04 01:00:00,24.0,, -2019-01-04 02:00:00,24.0,, -2019-01-04 03:00:00,33.875,, -2019-01-04 04:00:00,24.0,, -2019-01-04 05:00:00,24.0,, -2019-01-04 06:00:00,24.0,, -2019-01-04 07:00:00,24.0,, -2019-01-04 08:00:00,13.666666666666666,, -2019-01-04 09:00:00,3.333333333333333,, -2019-01-04 10:00:00,3.333333333333333,, -2019-01-04 11:00:00,3.333333333333333,, -2019-01-04 12:00:00,24.0,, -2019-01-04 13:00:00,20.0,, -2019-01-04 14:00:00,20.0,, -2019-01-04 15:00:00,3.333333333333333,, -2019-01-04 16:00:00,11.666666666666666,, -2019-01-04 17:00:00,3.333333333333333,, -2019-01-04 18:00:00,20.0,, -2019-01-04 19:00:00,13.666666666666666,, -2019-01-04 20:00:00,24.0,, -2019-01-04 21:00:00,11.666666666666666,, -2019-01-04 22:00:00,3.333333333333333,, -2019-01-04 23:00:00,11.666666666666666,, -2019-01-05 00:00:00,3.333333333333333,, -2019-01-05 01:00:00,11.666666666666666,, -2019-01-05 02:00:00,3.333333333333333,, -2019-01-05 03:00:00,3.333333333333333,, -2019-01-05 04:00:00,3.333333333333333,, -2019-01-05 05:00:00,24.0,, -2019-01-05 06:00:00,33.875,, -2019-01-05 07:00:00,24.0,, -2019-01-05 08:00:00,24.0,, -2019-01-05 09:00:00,24.0,, -2019-01-05 10:00:00,24.0,, -2019-01-05 11:00:00,24.0,, -2019-01-05 12:00:00,24.0,, -2019-01-05 13:00:00,20.0,, -2019-01-05 14:00:00,22.0,, -2019-01-05 15:00:00,24.0,, -2019-01-05 16:00:00,24.0,, -2019-01-05 17:00:00,24.0,, -2019-01-05 18:00:00,43.75,, -2019-01-05 19:00:00,24.0,, -2019-01-05 20:00:00,24.0,, -2019-01-05 21:00:00,22.0,, -2019-01-05 22:00:00,3.333333333333333,, -2019-01-05 23:00:00,24.0,, -2019-01-06 00:00:00,24.0,, -2019-01-06 01:00:00,24.0,, -2019-01-06 02:00:00,24.0,, -2019-01-06 03:00:00,24.0,, -2019-01-06 04:00:00,24.0,, -2019-01-06 05:00:00,24.0,, -2019-01-06 06:00:00,20.0,, -2019-01-06 07:00:00,20.0,, -2019-01-06 08:00:00,24.0,, -2019-01-06 09:00:00,24.0,, -2019-01-06 10:00:00,24.0,, -2019-01-06 11:00:00,24.0,, -2019-01-06 12:00:00,24.0,, -2019-01-06 13:00:00,24.0,, -2019-01-06 14:00:00,24.0,, -2019-01-06 15:00:00,22.0,, -2019-01-06 16:00:00,20.0,, -2019-01-06 17:00:00,22.0,, -2019-01-06 18:00:00,20.0,, -2019-01-06 19:00:00,20.0,, -2019-01-06 20:00:00,24.0,, -2019-01-06 21:00:00,22.0,, -2019-01-06 22:00:00,3.333333333333333,, -2019-01-06 23:00:00,3.333333333333333,, -2019-01-07 00:00:00,11.666666666666666,, -2019-01-07 01:00:00,20.0,, -2019-01-07 02:00:00,3.333333333333333,, -2019-01-07 03:00:00,3.333333333333333,, -2019-01-07 04:00:00,3.333333333333333,, -2019-01-07 05:00:00,3.333333333333333,, -2019-01-07 06:00:00,20.0,, -2019-01-07 07:00:00,20.0,, -2019-01-07 08:00:00,24.0,, -2019-01-07 09:00:00,24.0,, -2019-01-07 10:00:00,22.0,, -2019-01-07 11:00:00,22.0,, -2019-01-07 12:00:00,24.0,, -2019-01-07 13:00:00,24.0,, -2019-01-07 14:00:00,24.0,, -2019-01-07 15:00:00,3.333333333333333,, -2019-01-07 16:00:00,20.0,, -2019-01-07 17:00:00,22.0,, -2019-01-07 18:00:00,24.0,, -2019-01-07 19:00:00,20.0,, -2019-01-07 20:00:00,20.0,, -2019-01-07 21:00:00,22.0,, -2019-01-07 22:00:00,24.0,, -2019-01-07 23:00:00,24.0,, -2019-01-08 00:00:00,24.0,, -2019-01-08 01:00:00,24.0,, -2019-01-08 02:00:00,24.0,, -2019-01-08 03:00:00,24.0,, -2019-01-08 04:00:00,22.0,, -2019-01-08 05:00:00,3.333333333333333,, -2019-01-08 06:00:00,11.666666666666666,, -2019-01-08 07:00:00,20.0,, -2019-01-08 08:00:00,24.0,, -2019-01-08 09:00:00,24.0,, -2019-01-08 10:00:00,24.0,, -2019-01-08 11:00:00,24.0,, -2019-01-08 12:00:00,24.0,, -2019-01-08 13:00:00,22.0,, -2019-01-08 14:00:00,24.0,, -2019-01-08 15:00:00,20.0,, -2019-01-08 16:00:00,24.0,, -2019-01-08 17:00:00,24.0,, -2019-01-08 18:00:00,24.0,, -2019-01-08 19:00:00,24.0,, -2019-01-08 20:00:00,24.0,, -2019-01-08 21:00:00,24.0,, -2019-01-08 22:00:00,24.0,, -2019-01-08 23:00:00,20.0,, -2019-01-09 00:00:00,3.333333333333333,, -2019-01-09 01:00:00,20.0,, -2019-01-09 02:00:00,24.0,, -2019-01-09 03:00:00,24.0,, -2019-01-09 04:00:00,24.0,, -2019-01-09 05:00:00,20.0,, -2019-01-09 06:00:00,11.666666666666666,, -2019-01-09 07:00:00,24.0,, -2019-01-09 08:00:00,3.333333333333333,, -2019-01-09 09:00:00,13.666666666666666,, -2019-01-09 10:00:00,20.0,, -2019-01-09 11:00:00,24.0,, -2019-01-09 12:00:00,24.0,, -2019-01-09 13:00:00,24.0,, -2019-01-09 14:00:00,24.0,, -2019-01-09 15:00:00,24.0,, -2019-01-09 16:00:00,24.0,, -2019-01-09 17:00:00,11.666666666666666,, -2019-01-09 18:00:00,3.333333333333333,, -2019-01-09 19:00:00,20.0,, -2019-01-09 20:00:00,11.666666666666666,, -2019-01-09 21:00:00,24.0,, -2019-01-09 22:00:00,24.0,, -2019-01-09 23:00:00,24.0,, -2019-01-10 00:00:00,24.0,, -2019-01-10 01:00:00,24.0,, -2019-01-10 02:00:00,22.0,, -2019-01-10 03:00:00,3.333333333333333,, -2019-01-10 04:00:00,20.0,, -2019-01-10 05:00:00,24.0,, -2019-01-10 06:00:00,24.0,, -2019-01-10 07:00:00,24.0,, -2019-01-10 08:00:00,24.0,, -2019-01-10 09:00:00,22.0,, -2019-01-10 10:00:00,24.0,, -2019-01-10 11:00:00,24.0,, -2019-01-10 12:00:00,20.0,, -2019-01-10 13:00:00,11.666666666666666,, -2019-01-10 14:00:00,20.0,, -2019-01-10 15:00:00,22.0,, -2019-01-10 16:00:00,24.0,, -2019-01-10 17:00:00,24.0,, -2019-01-10 18:00:00,22.0,, -2019-01-10 19:00:00,24.0,, -2019-01-10 20:00:00,24.0,, -2019-01-10 21:00:00,20.0,, -2019-01-10 22:00:00,13.666666666666666,, -2019-01-10 23:00:00,3.333333333333333,, -2019-01-11 00:00:00,22.0,, -2019-01-11 01:00:00,20.0,, -2019-01-11 02:00:00,24.0,, -2019-01-11 03:00:00,24.0,, -2019-01-11 04:00:00,24.0,, -2019-01-11 05:00:00,24.0,, -2019-01-11 06:00:00,24.0,, -2019-01-11 07:00:00,3.333333333333333,, -2019-01-11 08:00:00,3.333333333333333,, -2019-01-11 09:00:00,24.0,, -2019-01-11 10:00:00,24.0,, -2019-01-11 11:00:00,24.0,, -2019-01-11 12:00:00,24.0,, -2019-01-11 13:00:00,24.0,, -2019-01-11 14:00:00,24.0,, -2019-01-11 15:00:00,24.0,, -2019-01-11 16:00:00,20.0,, -2019-01-11 17:00:00,20.0,, -2019-01-11 18:00:00,24.0,, -2019-01-11 19:00:00,24.0,, -2019-01-11 20:00:00,24.0,, -2019-01-11 21:00:00,24.0,, -2019-01-11 22:00:00,24.0,, -2019-01-11 23:00:00,24.0,, -2019-01-12 00:00:00,24.0,, -2019-01-12 01:00:00,20.0,, -2019-01-12 02:00:00,3.333333333333333,, -2019-01-12 03:00:00,22.0,, -2019-01-12 04:00:00,24.0,, -2019-01-12 05:00:00,24.0,, -2019-01-12 06:00:00,24.0,, -2019-01-12 07:00:00,24.0,, -2019-01-12 08:00:00,24.0,, -2019-01-12 09:00:00,22.0,, -2019-01-12 10:00:00,20.0,, -2019-01-12 11:00:00,11.666666666666666,, -2019-01-12 12:00:00,22.0,, -2019-01-12 13:00:00,24.0,, -2019-01-12 14:00:00,24.0,, -2019-01-12 15:00:00,24.0,, -2019-01-12 16:00:00,24.0,, -2019-01-12 17:00:00,24.0,, -2019-01-12 18:00:00,24.0,, -2019-01-12 19:00:00,24.0,, -2019-01-12 20:00:00,22.0,, -2019-01-12 21:00:00,3.333333333333333,, -2019-01-12 22:00:00,24.0,, -2019-01-12 23:00:00,24.0,, -2019-01-13 00:00:00,24.0,, -2019-01-13 01:00:00,24.0,, -2019-01-13 02:00:00,20.0,, -2019-01-13 03:00:00,24.0,, -2019-01-13 04:00:00,24.0,, -2019-01-13 05:00:00,3.333333333333333,, -2019-01-13 06:00:00,3.333333333333333,, -2019-01-13 07:00:00,22.0,, -2019-01-13 08:00:00,24.0,, -2019-01-13 09:00:00,24.0,, -2019-01-13 10:00:00,24.0,, -2019-01-13 11:00:00,20.0,, -2019-01-13 12:00:00,24.0,, -2019-01-13 13:00:00,24.0,, -2019-01-13 14:00:00,20.0,, -2019-01-13 15:00:00,20.0,, -2019-01-13 16:00:00,11.666666666666666,, -2019-01-13 17:00:00,24.0,, -2019-01-13 18:00:00,24.0,, -2019-01-13 19:00:00,24.0,, -2019-01-13 20:00:00,24.0,, -2019-01-13 21:00:00,24.0,, -2019-01-13 22:00:00,24.0,, -2019-01-13 23:00:00,22.0,, -2019-01-14 00:00:00,20.0,, -2019-01-14 01:00:00,13.666666666666666,, -2019-01-14 02:00:00,24.0,, -2019-01-14 03:00:00,24.0,, -2019-01-14 04:00:00,24.0,, -2019-01-14 05:00:00,24.0,, -2019-01-14 06:00:00,24.0,, -2019-01-14 07:00:00,24.0,, -2019-01-14 08:00:00,22.0,, -2019-01-14 09:00:00,20.0,, -2019-01-14 10:00:00,20.0,, -2019-01-14 11:00:00,24.0,, -2019-01-14 12:00:00,24.0,, -2019-01-14 13:00:00,24.0,, -2019-01-14 14:00:00,24.0,, -2019-01-14 15:00:00,24.0,, -2019-01-14 16:00:00,24.0,, -2019-01-14 17:00:00,24.0,, -2019-01-14 18:00:00,24.0,, -2019-01-14 19:00:00,24.0,, -2019-01-14 20:00:00,24.0,, -2019-01-14 21:00:00,24.0,, -2019-01-14 22:00:00,24.0,, -2019-01-14 23:00:00,24.0,, -2019-01-15 00:00:00,24.0,, -2019-01-15 01:00:00,24.0,, -2019-01-15 02:00:00,24.0,, -2019-01-15 03:00:00,22.0,, -2019-01-15 04:00:00,24.0,, -2019-01-15 05:00:00,11.666666666666666,, -2019-01-15 06:00:00,24.0,, -2019-01-15 07:00:00,24.0,, -2019-01-15 08:00:00,20.0,, -2019-01-15 09:00:00,24.0,, -2019-01-15 10:00:00,24.0,, -2019-01-15 11:00:00,24.0,, -2019-01-15 12:00:00,24.0,, -2019-01-15 13:00:00,20.0,, -2019-01-15 14:00:00,20.0,, -2019-01-15 15:00:00,24.0,, -2019-01-15 16:00:00,24.0,, -2019-01-15 17:00:00,24.0,, -2019-01-15 18:00:00,24.0,, -2019-01-15 19:00:00,24.0,, -2019-01-15 20:00:00,24.0,, -2019-01-15 21:00:00,13.666666666666666,, -2019-01-15 22:00:00,3.333333333333333,, -2019-01-15 23:00:00,3.333333333333333,, -2019-01-16 00:00:00,3.333333333333333,, -2019-01-16 01:00:00,13.666666666666666,, -2019-01-16 02:00:00,24.0,, -2019-01-16 03:00:00,24.0,, -2019-01-16 04:00:00,20.0,, -2019-01-16 05:00:00,24.0,, -2019-01-16 06:00:00,22.0,, -2019-01-16 07:00:00,20.0,, -2019-01-16 08:00:00,22.0,, -2019-01-16 09:00:00,20.0,, -2019-01-16 10:00:00,24.0,, -2019-01-16 11:00:00,24.0,, -2019-01-16 12:00:00,24.0,, -2019-01-16 13:00:00,24.0,, -2019-01-16 14:00:00,20.0,, -2019-01-16 15:00:00,11.666666666666666,, -2019-01-16 16:00:00,3.333333333333333,, -2019-01-16 17:00:00,20.0,, -2019-01-16 18:00:00,3.333333333333333,, -2019-01-16 19:00:00,22.0,, -2019-01-16 20:00:00,24.0,, -2019-01-16 21:00:00,24.0,, -2019-01-16 22:00:00,24.0,, -2019-01-16 23:00:00,24.0,, -2019-01-17 00:00:00,24.0,, -2019-01-17 01:00:00,22.0,, -2019-01-17 02:00:00,22.0,, -2019-01-17 03:00:00,24.0,, -2019-01-17 04:00:00,33.875,, -2019-01-17 05:00:00,24.0,, -2019-01-17 06:00:00,24.0,, -2019-01-17 07:00:00,43.75,, -2019-01-17 08:00:00,24.0,, -2019-01-17 09:00:00,24.0,, -2019-01-17 10:00:00,22.0,, -2019-01-17 11:00:00,3.333333333333333,, -2019-01-17 12:00:00,3.333333333333333,, -2019-01-17 13:00:00,22.0,, -2019-01-17 14:00:00,24.0,, -2019-01-17 15:00:00,33.875,, -2019-01-17 16:00:00,24.0,, -2019-01-17 17:00:00,33.875,, -2019-01-17 18:00:00,24.0,, -2019-01-17 19:00:00,22.0,, -2019-01-17 20:00:00,3.333333333333333,, -2019-01-17 21:00:00,11.666666666666666,, -2019-01-17 22:00:00,24.0,, -2019-01-17 23:00:00,33.875,, -2019-01-18 00:00:00,33.875,, -2019-01-18 01:00:00,24.0,, -2019-01-18 02:00:00,33.875,, -2019-01-18 03:00:00,24.0,, -2019-01-18 04:00:00,33.875,, -2019-01-18 05:00:00,24.0,, -2019-01-18 06:00:00,24.0,, -2019-01-18 07:00:00,24.0,, -2019-01-18 08:00:00,24.0,, -2019-01-18 09:00:00,24.0,, -2019-01-18 10:00:00,24.0,, -2019-01-18 11:00:00,24.0,, -2019-01-18 12:00:00,24.0,, -2019-01-18 13:00:00,24.0,, -2019-01-18 14:00:00,24.0,, -2019-01-18 15:00:00,24.0,, -2019-01-18 16:00:00,24.0,, -2019-01-18 17:00:00,24.0,, -2019-01-18 18:00:00,24.0,, -2019-01-18 19:00:00,33.875,, -2019-01-18 20:00:00,43.75,, -2019-01-18 21:00:00,43.75,, -2019-01-18 22:00:00,33.875,, -2019-01-18 23:00:00,24.0,, -2019-01-19 00:00:00,20.0,, -2019-01-19 01:00:00,20.0,, -2019-01-19 02:00:00,24.0,, -2019-01-19 03:00:00,43.75,, -2019-01-19 04:00:00,33.875,, -2019-01-19 05:00:00,24.0,, -2019-01-19 06:00:00,22.0,, -2019-01-19 07:00:00,24.0,, -2019-01-19 08:00:00,22.0,, -2019-01-19 09:00:00,24.0,, -2019-01-19 10:00:00,24.0,, -2019-01-19 11:00:00,24.0,, -2019-01-19 12:00:00,24.0,, -2019-01-19 13:00:00,33.875,, -2019-01-19 14:00:00,24.0,, -2019-01-19 15:00:00,24.0,, -2019-01-19 16:00:00,24.0,, -2019-01-19 17:00:00,24.0,, -2019-01-19 18:00:00,20.0,, -2019-01-19 19:00:00,11.666666666666666,, -2019-01-19 20:00:00,3.333333333333333,, -2019-01-19 21:00:00,20.0,, -2019-01-19 22:00:00,24.0,, -2019-01-19 23:00:00,24.0,, -2019-01-20 00:00:00,33.875,, -2019-01-20 01:00:00,24.0,, -2019-01-20 02:00:00,22.0,, -2019-01-20 03:00:00,24.0,, -2019-01-20 04:00:00,11.666666666666666,, -2019-01-20 05:00:00,3.333333333333333,, -2019-01-20 06:00:00,11.666666666666666,, -2019-01-20 07:00:00,24.0,, -2019-01-20 08:00:00,24.0,, -2019-01-20 09:00:00,31.875,, -2019-01-20 10:00:00,24.0,, -2019-01-20 11:00:00,22.0,, -2019-01-20 12:00:00,24.0,, -2019-01-20 13:00:00,13.666666666666666,, -2019-01-20 14:00:00,20.0,, -2019-01-20 15:00:00,20.0,, -2019-01-20 16:00:00,20.0,, -2019-01-20 17:00:00,11.666666666666666,, -2019-01-20 18:00:00,20.0,, -2019-01-20 19:00:00,24.0,, -2019-01-20 20:00:00,24.0,, -2019-01-20 21:00:00,24.0,, -2019-01-20 22:00:00,24.0,, -2019-01-20 23:00:00,24.0,, -2019-01-21 00:00:00,20.0,, -2019-01-21 01:00:00,3.333333333333333,, -2019-01-21 02:00:00,3.333333333333333,, -2019-01-21 03:00:00,24.0,, -2019-01-21 04:00:00,24.0,, diff --git a/tests/test_complex_market_mechanisms.py b/tests/test_complex_market_mechanisms.py index 6d417a25..0b58fc72 100644 --- a/tests/test_complex_market_mechanisms.py +++ b/tests/test_complex_market_mechanisms.py @@ -57,7 +57,7 @@ def test_complex_clearing(): accepted_orders, rejected_orders, meta = mr.clear(orderbook, products) assert math.isclose(meta[0]["supply_volume"], 1000, abs_tol=eps) - assert math.isclose(meta[0]["demand_volume"], -1000, abs_tol=eps) + assert math.isclose(meta[0]["demand_volume"], 1000, abs_tol=eps) assert math.isclose(meta[0]["price"], 100, abs_tol=eps) assert rejected_orders == [] assert accepted_orders[0]["agent_id"] == "dem1" diff --git a/tests/test_flexable_storage_strategies.py b/tests/test_flexable_storage_strategies.py index 7322bec6..83d35850 100644 --- a/tests/test_flexable_storage_strategies.py +++ b/tests/test_flexable_storage_strategies.py @@ -1,6 +1,7 @@ import math -from datetime import datetime +from datetime import datetime, timedelta +import numpy as np import pandas as pd import pytest @@ -19,7 +20,7 @@ @pytest.fixture def storage() -> Storage: # Create a PowerPlant instance with some example parameters - index = pd.date_range("2023-07-01", periods=4, freq="H") + index = pd.date_range("2023-07-01", periods=48, freq="H") # constant price of 50 ff = NaiveForecast(index, availability=1, price_forecast=50) return Storage( @@ -74,6 +75,53 @@ def test_flexable_eom_storage(mock_market_config, storage): assert bids[0]["price"] == 47.5 assert bids[0]["volume"] == -60 + # change to dam bidding + day = pd.date_range(start, start + timedelta(hours=23), freq="H") + index = pd.date_range("2023-07-01", periods=24, freq="H") + product_tuples = [(start, start + timedelta(hours=1), None) for start in day] + storage.foresight = pd.Timedelta(hours=4) + forecast = [ + 20, + 50, + 50, + 50, + 80, + 50, + 50, + 50, + 80, + 50, + 50, + 50, + 80, + 50, + 50, + 50, + 20, + 50, + 50, + 50, + 20, + 50, + 50, + 50, + ] + storage.forecaster = NaiveForecast(index, availability=1, price_forecast=forecast) + bids = strategy.calculate_bids(storage, mc, product_tuples=product_tuples) + assert len(bids) == 6 + assert math.isclose(bids[0]["price"], np.mean(forecast[0:13]), abs_tol=0.01) + assert bids[0]["volume"] == -60 + assert math.isclose(bids[1]["price"], np.mean(forecast[0:17]), abs_tol=0.01) + assert bids[1]["volume"] == 60 + assert math.isclose(bids[2]["price"], np.mean(forecast[0:21]), abs_tol=0.01) + assert bids[2]["volume"] == 60 + assert math.isclose(bids[3]["price"], np.mean(forecast[0:25]), abs_tol=0.01) + assert bids[3]["volume"] == 60 + assert math.isclose(bids[4]["price"], np.mean(forecast[4:]), abs_tol=0.01) + assert bids[4]["volume"] == -60 + assert math.isclose(bids[5]["price"], np.mean(forecast[8:]), abs_tol=0.01) + assert bids[5]["volume"] == -60 + def test_flexable_pos_crm_storage(mock_market_config, storage): index = pd.date_range("2023-07-01", periods=4, freq="H") @@ -84,7 +132,7 @@ def test_flexable_pos_crm_storage(mock_market_config, storage): product_tuples = [(start, end, None)] # constant price of 50 - specific_revenue = (50 - (4 / 0.95 + 1)) * 360 / (0.36 * 1000) + specific_revenue = (50 - (4 / 0.95)) * 360 / (0.36 * 1000) storage.forecaster = NaiveForecast(index, availability=1, price_forecast=50) bids = strategy.calculate_bids(storage, mc, product_tuples=product_tuples) @@ -100,9 +148,7 @@ def test_flexable_pos_crm_storage(mock_market_config, storage): assert bids[0]["volume"] == 60 # specific revenue < 0 - storage.forecaster = NaiveForecast( - index, availability=1, price_forecast=[5, 5, 5, 5] - ) + storage.forecaster = NaiveForecast(index, availability=1, price_forecast=3) bids = strategy.calculate_bids(storage, mc, product_tuples=product_tuples) assert len(bids) == 1 assert bids[0]["price"] == 0 diff --git a/tests/test_flexable_strategies.py b/tests/test_flexable_strategies.py index 94d7dcc5..dea124bf 100644 --- a/tests/test_flexable_strategies.py +++ b/tests/test_flexable_strategies.py @@ -25,7 +25,7 @@ def power_plant() -> PowerPlant: max_power=1000, min_power=200, efficiency=0.5, - fixed_cost=10, + variable_cost=10, bidding_strategies={}, fuel_type="lignite", emission_factor=0.5, diff --git a/tests/test_powerplant.py b/tests/test_powerplant.py index 8e91585e..6e3e7ca8 100644 --- a/tests/test_powerplant.py +++ b/tests/test_powerplant.py @@ -24,7 +24,7 @@ def power_plant_1() -> PowerPlant: max_power=1000, min_power=200, efficiency=0.5, - fixed_cost=10, + variable_cost=10, fuel_type="lignite", emission_factor=0.5, forecaster=ff, @@ -45,7 +45,7 @@ def power_plant_2() -> PowerPlant: max_power=1000, min_power=0, efficiency=0.5, - fixed_cost=10, + variable_cost=10, fuel_type="lignite", forecaster=ff, emission_factor=0.5, @@ -66,7 +66,7 @@ def power_plant_3() -> PowerPlant: max_power=1000, min_power=0, efficiency=0.5, - fixed_cost=10, + variable_cost=10, fuel_type="lignite", emission_factor=0.5, forecaster=ff, @@ -81,7 +81,7 @@ def test_init_function(power_plant_1, power_plant_2, power_plant_3): assert power_plant_1.max_power == 1000 assert power_plant_1.min_power == 200 assert power_plant_1.efficiency == 0.5 - assert power_plant_1.fixed_cost == 10 + assert power_plant_1.variable_cost == 10 assert power_plant_1.fuel_type == "lignite" assert power_plant_1.emission_factor == 0.5 assert power_plant_1.ramp_up == 1000 diff --git a/tests/test_storage.py b/tests/test_storage.py index 3a0be473..27a83f92 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -28,6 +28,7 @@ def storage_unit() -> Storage: variable_cost_charge=3, variable_cost_discharge=4, fixed_cost=1, + initial_soc=0.5, ) @@ -43,6 +44,7 @@ def test_init_function(storage_unit): assert storage_unit.ramp_down_discharge == 50 assert storage_unit.ramp_up_charge == -60 assert storage_unit.ramp_up_discharge == 60 + assert storage_unit.initial_soc == 0.5 def test_reset_function(storage_unit): @@ -57,6 +59,9 @@ def test_reset_function(storage_unit): assert storage_unit.outputs["neg_capacity"].equals( pd.Series(0.0, index=pd.date_range("2022-01-01", periods=4, freq="H")) ) + assert storage_unit.outputs["soc"].equals( + pd.Series(0.5, index=pd.date_range("2022-01-01", periods=4, freq="H")) + ) def test_calculate_operational_window(storage_unit): @@ -76,7 +81,7 @@ def test_calculate_operational_window(storage_unit): assert min_power_discharge[start] == 0 assert max_power_discharge[start] == 100 - assert cost_discharge == 4 / 0.95 + 1 + assert cost_discharge == 4 / 0.95 min_power_charge, max_power_charge = storage_unit.calculate_min_max_charge( start, end, product_type="energy" @@ -85,7 +90,7 @@ def test_calculate_operational_window(storage_unit): assert min_power_charge[start] == 0 assert max_power_charge[start] == -100 - assert math.isclose(cost_charge, 3 / 0.9 + 1) + assert math.isclose(cost_charge, 3 / 0.9) assert storage_unit.outputs["energy"].at[start] == 0 @@ -122,7 +127,7 @@ def test_soc_constraint(storage_unit): storage_unit.outputs["capacity_neg"][start] = -50 storage_unit.outputs["capacity_pos"][start] = 30 - storage_unit.outputs["soc"][start - storage_unit.index.freq] = 0.05 + storage_unit.outputs["soc"][start] = 0.05 min_power_discharge, max_power_discharge = storage_unit.calculate_min_max_discharge( start, end ) @@ -131,7 +136,7 @@ def test_soc_constraint(storage_unit): max_power_discharge.iloc[0], (50 * storage_unit.efficiency_discharge) ) - storage_unit.outputs["soc"][start - storage_unit.index.freq] = 0.95 + storage_unit.outputs["soc"][start] = 0.95 min_power_charge, max_power_charge = storage_unit.calculate_min_max_charge( start, end ) @@ -302,45 +307,47 @@ def test_execute_dispatch(storage_unit): end = product_tuple[1] storage_unit.outputs["energy"][start] = 100 - storage_unit.outputs["soc"][start - storage_unit.index.freq] = 0.5 + storage_unit.outputs["soc"][start] = 0.5 # dispatch full discharge dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert dispatched_energy.iloc[0] == 100 assert math.isclose( - storage_unit.outputs["soc"][start], + storage_unit.outputs["soc"][start + storage_unit.index.freq], 0.5 - 100 / storage_unit.efficiency_discharge / storage_unit.max_volume, ) # dispatch full charging storage_unit.outputs["energy"][start] = -100 - storage_unit.outputs["soc"][start - storage_unit.index.freq] = 0.5 + storage_unit.outputs["soc"][start] = 0.5 dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert dispatched_energy.iloc[0] == -100 assert math.isclose( - storage_unit.outputs["soc"][start], + storage_unit.outputs["soc"][end], 0.5 + 100 * storage_unit.efficiency_charge / storage_unit.max_volume, ) storage_unit.outputs["energy"][start] = 100 - storage_unit.outputs["soc"][start - storage_unit.index.freq] = 0.05 + storage_unit.outputs["soc"][start] = 0.05 + storage_unit.outputs["soc"][start] = 0.05 dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert math.isclose( dispatched_energy.iloc[0], 50 * storage_unit.efficiency_discharge, abs_tol=0.1 ) storage_unit.outputs["energy"][start] = -100 - storage_unit.outputs["soc"][start - storage_unit.index.freq] = 0.95 + storage_unit.outputs["soc"][start] = 0.95 + storage_unit.outputs["soc"][start] = 0.95 dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert math.isclose( dispatched_energy.iloc[0], -50 / storage_unit.efficiency_charge, abs_tol=0.1 ) - storage_unit.outputs["soc"][start] = 1 + assert math.isclose(storage_unit.outputs["soc"][end], 1, abs_tol=0.001) start = start + storage_unit.index.freq end = end + storage_unit.index.freq storage_unit.outputs["energy"][start] = -100 dispatched_energy = storage_unit.execute_current_dispatch(start, end) assert dispatched_energy.iloc[0] == 0 - storage_unit.outputs["soc"][start] = 1 + assert math.isclose(storage_unit.outputs["soc"][end], 1, abs_tol=0.001) if __name__ == "__main__":