From fd8a16d88b2888da2286b4805d807b22fb47b9a5 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 2 May 2024 15:53:43 -0400 Subject: [PATCH 01/26] improve the PEM parameterized bidder --- idaes/apps/grid_integration/bidder.py | 300 ++++++++++++++++++++++ idaes/apps/grid_integration/forecaster.py | 39 +++ 2 files changed, 339 insertions(+) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index c1843cc458..87830673ac 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1333,3 +1333,303 @@ def _record_bids(self, bids, date, hour, **kwargs): self.bids_result_list.append(pd.concat(df_list)) return + + +class ParametrizedBidder(AbstractBidder): + + ''' + Create a parameterized bidder. + Bid the resource at different prices. + ''' + def __init__( + self, + bidding_model_object, + day_ahead_horizon, + real_time_horizon, + solver, + forecaster, + ): + ''' + Arguments: + bidding_model_object: the model object for bidding + + day_ahead_horizon: number of time periods in the day-ahead bidding problem + + real_time_horizon: number of time periods in the real-time bidding problem + + solver: a Pyomo mathematical programming solver object + ''' + self.bidding_model_object = bidding_model_object + self.day_ahead_horizon = day_ahead_horizon + self.real_time_horizon = real_time_horizon + self.solver = solver + self.forecaster = forecaster + + self.n_scenario = 1 # there must be a n_scenario attribute in this class + self._check_inputs() + + self.generator = self.bidding_model_object.model_data.gen_name + self.bids_result_list = [] + + @property + def generator(self): + return self._generator + + @generator.setter + def generator(self, name): + self._generator = name + + + def formulate_DA_bidding_problem(self): + ''' + No need to formulate a DA bidding problem here. + ''' + pass + + def formulate_RT_bidding_problem(self): + ''' + No need to formulate a RT bidding problem here. + ''' + pass + + def compute_day_ahead_bids(self, date, hour=0): + raise NotImplementedError + + def compute_real_time_bids( + self, date, hour, realized_day_ahead_prices, realized_day_ahead_dispatches, tracker_profile + ): + raise NotImplementedError + + def update_day_ahead_model(self, **kwargs): + ''' + No need to update the RT bidding problem here. + ''' + pass + + def update_real_time_model(self, **kwargs): + ''' + No need to update the RT bidding problem here. + ''' + pass + + def record_bids(self, bids, model, date, hour, market): + + """ + This function records the bids and the details in the underlying bidding model. + + Arguments: + bids: the obtained bids for this date. + + model: bidding model + + date: the date we bid into + + hour: the hour we bid into + + Returns: + None + + """ + + # record bids + self._record_bids(bids, date, hour, Market=market) + + # record the details of bidding model + for i in model.SCENARIOS: + self.bidding_model_object.record_results( + model.fs[i], date=date, hour=hour, Scenario=i, Market=market + ) + + return + + def _record_bids(self, bids, date, hour, **kwargs): + df_list = [] + for t in bids: + for gen in bids[t]: + + result_dict = {} + result_dict["Generator"] = gen + result_dict["Date"] = date + result_dict["Hour"] = t + + for k, v in kwargs.items(): + result_dict[k] = v + + pair_cnt = len(bids[t][gen]["p_cost"]) + + for idx, (power, cost) in enumerate(bids[t][gen]["p_cost"]): + result_dict[f"Power {idx} [MW]"] = power + result_dict[f"Cost {idx} [$]"] = cost + + # place holder, in case different len of bids + while pair_cnt < self.n_scenario: + result_dict[f"Power {pair_cnt} [MW]"] = None + result_dict[f"Cost {pair_cnt} [$]"] = None + + pair_cnt += 1 + + result_df = pd.DataFrame.from_dict(result_dict, orient="index") + df_list.append(result_df.T) + + # save the result to object property + # wait to be written when simulation ends + self.bids_result_list.append(pd.concat(df_list)) + + return + + def write_results(self, path): + """ + This methods writes the saved operation stats into an csv file. + + Arguments: + path: the path to write the results. + + Return: + None + """ + + print("") + print("Saving bidding results to disk...") + pd.concat(self.bids_result_list).to_csv( + os.path.join(path, "bidder_detail.csv"), index=False + ) + return + + +class PEMParametrizedBidder(ParametrizedBidder): + + """ + Renewable (PV or Wind) + PEM bidder that uses parameterized bid curve. + Every timestep for RT or DA, max energy bid is the available wind resource. + Please use the + """ + def __init__( + self, + bidding_model_object, + day_ahead_horizon, + real_time_horizon, + solver, + forecaster, + renewable_mw, + pem_marginal_cost, + pem_mw, + real_time_bidding_only = False + ): + + ''' + Arguments: + renewable_mw: maximum renewable energy system capacity + + pem_marginal_cost: the cost/MW above which all available wind energy will be sold to grid; + below which, make hydrogen and sell remainder of wind to grid + + pem_mw: maximum PEM capacity limits how much energy is bid at the `pem_marginal_cost` + + ''' + super().__init__(bidding_model_object, + day_ahead_horizon, + real_time_horizon, + solver, + forecaster) + self.renewable_marginal_cost = 0 + self.renewable_mw = renewable_mw + self.pem_marginal_cost = pem_marginal_cost + self.pem_mw = pem_mw + self.real_time_bidding_only = real_time_bidding_only + + + def compute_day_ahead_bids(self, date, hour=0): + """ + DA Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. + from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' + + If Wind resource at some time is less than PEM capacity, then reduce to available resource + """ + gen = self.generator + # Forecast the day-ahead wind generation + forecast = self.forecaster.forecast_day_ahead_capacity_factor(date, hour, gen, self.day_ahead_horizon) + + full_bids = {} + + for t_idx in range(self.day_ahead_horizon): + da_wind = forecast[t_idx] * self.wind_mw + grid_wind = max(0, da_wind - self.pem_mw) + # gird wind are bidded at marginal cost = 0 + # The rest of the power is bidded at the pem marginal cost + if grid_wind == 0: + bids = [(0, 0), (da_wind, self.pem_marginal_cost)] + else: + bids = [(0, 0), (grid_wind, 0), (da_wind, self.pem_marginal_cost)] + cost_curve = convert_marginal_costs_to_actual_costs(bids) + + temp_curve = { + "data_type": "cost_curve", + "cost_curve_type": "piecewise", + "values": cost_curve, + } + tx_utils.validate_and_clean_cost_curve( + curve=temp_curve, + curve_type="cost_curve", + p_min=0, + p_max=max([p[0] for p in cost_curve]), + gen_name=gen, + t=t_idx, + ) + + t = t_idx + hour + full_bids[t] = {} + full_bids[t][gen] = {} + full_bids[t][gen]["p_cost"] = cost_curve + full_bids[t][gen]["p_min"] = 0 + full_bids[t][gen]["p_max"] = da_wind + full_bids[t][gen]["startup_capacity"] = da_wind + full_bids[t][gen]["shutdown_capacity"] = da_wind + + self._record_bids(full_bids, date, hour, Market="Day-ahead") + return full_bids + + + def compute_real_time_bids( + self, date, hour, realized_day_ahead_dispatches + ): + """ + RT Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. + from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' + + If Wind resource at some time is less than PEM capacity, then reduce to available resource + """ + + gen = self.generator + forecast = self.forecaster.forecast_real_time_capacity_factor(date, hour, gen, self.real_time_horizon) + full_bids = {} + + for t_idx in range(self.real_time_horizon): + rt_wind = forecast[t_idx] * self.wind_mw + # if we participate in both DA and RT market + if not self.real_time_bidding_only: + da_dispatch = realized_day_ahead_dispatches[t_idx + hour] + # if we only participates in the RT market, then we do not consider the DA commitment + if self.real_time_bidding_only: + da_dispatch = 0 + + avail_rt_wind = max(0, rt_wind - da_dispatch) + grid_wind = max(0 , avail_rt_wind - self.pem_mw) + + if grid_wind == 0: + bids = [(0, 0), (rt_wind, self.pem_marginal_cost)] + else: + bids = [(0, 0), (grid_wind, 0), (rt_wind, self.pem_marginal_cost)] + + t = t_idx + hour + full_bids[t] = {} + full_bids[t][gen] = {} + full_bids[t][gen]["p_cost"] = convert_marginal_costs_to_actual_costs(bids) + full_bids[t][gen]["p_min"] = 0 + full_bids[t][gen]["p_max"] = rt_wind + full_bids[t][gen]["startup_capacity"] = rt_wind + full_bids[t][gen]["shutdown_capacity"] = rt_wind + + self._record_bids(full_bids, date, hour, Market="Real-time") + + + return full_bids diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index c22de243e3..6ece20967c 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -705,3 +705,42 @@ def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_resul self._historical_da_prices[b] = self._historical_da_prices[b][24:] return + + +class PerfectForecaster(AbstractPrescientPriceForecaster): + + ''' + Forecast the perfect capacity factor of renewables. + ''' + + def __init__(self, data_path_or_df): + + """ + Perfect forecaster that reads the data from a Dataframe containing: + - {bus}-DALMP + - {bus}-RTLMP + - {bus}-DACF and {bus}-RTCF for renewable plants + """ + if isinstance(data_path_or_df, str): + self.data = pd.read_csv(data_path_or_df, index_col="Datetime", parse_dates=True) + elif isinstance(data_path_or_df, pd.DataFrame): + self.data = data_path_or_df + else: + raise ValueError + + def __getitem__(self, index): + return self.data[index] + + def fetch_hourly_stats_from_prescient(self, prescient_hourly_stats): + pass + + def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_result): + pass + + def forecast_day_ahead_prices(self): + pass + + def forecast_real_time_prices(Self): + pass + + \ No newline at end of file From ac96407d4284a073f2106af8c566cac88e6f3ebc Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 7 May 2024 17:45:46 -0400 Subject: [PATCH 02/26] improve pem bidder and add some tests --- idaes/apps/grid_integration/bidder.py | 18 ++- idaes/apps/grid_integration/forecaster.py | 41 ++++-- .../tests/test_PEM_Parameterized_bidder | 25 ++++ .../tests/test_perfectforecaster.py | 121 ++++++++++++++++++ 4 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder create mode 100644 idaes/apps/grid_integration/tests/test_perfectforecaster.py diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 87830673ac..da7e8a4b3b 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1531,8 +1531,8 @@ def __init__( real_time_horizon, solver, forecaster) - self.renewable_marginal_cost = 0 self.renewable_mw = renewable_mw + self.renewable_marginal_cost = 0 self.pem_marginal_cost = pem_marginal_cost self.pem_mw = pem_mw self.real_time_bidding_only = real_time_bidding_only @@ -1552,7 +1552,7 @@ def compute_day_ahead_bids(self, date, hour=0): full_bids = {} for t_idx in range(self.day_ahead_horizon): - da_wind = forecast[t_idx] * self.wind_mw + da_wind = forecast[t_idx] * self.renewable_mw grid_wind = max(0, da_wind - self.pem_mw) # gird wind are bidded at marginal cost = 0 # The rest of the power is bidded at the pem marginal cost @@ -1590,7 +1590,7 @@ def compute_day_ahead_bids(self, date, hour=0): def compute_real_time_bids( - self, date, hour, realized_day_ahead_dispatches + self, date, hour, realized_day_ahead_dispatches,realized_day_ahead_prices ): """ RT Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. @@ -1604,10 +1604,14 @@ def compute_real_time_bids( full_bids = {} for t_idx in range(self.real_time_horizon): - rt_wind = forecast[t_idx] * self.wind_mw + rt_wind = forecast[t_idx] * self.renewable_mw # if we participate in both DA and RT market if not self.real_time_bidding_only: - da_dispatch = realized_day_ahead_dispatches[t_idx + hour] + try: + da_dispatch = realized_day_ahead_dispatches[t_idx + hour] + except IndexError: + # When having indexerror, it must be the period that we are looking ahead. It is ok to set da_dispatch to 0 + da_dispatch = 0 # if we only participates in the RT market, then we do not consider the DA commitment if self.real_time_bidding_only: da_dispatch = 0 @@ -1616,9 +1620,9 @@ def compute_real_time_bids( grid_wind = max(0 , avail_rt_wind - self.pem_mw) if grid_wind == 0: - bids = [(0, 0), (rt_wind, self.pem_marginal_cost)] + bids = [(0, 0), (avail_rt_wind, self.pem_marginal_cost)] else: - bids = [(0, 0), (grid_wind, 0), (rt_wind, self.pem_marginal_cost)] + bids = [(0, 0), (grid_wind, 0), (avail_rt_wind, self.pem_marginal_cost)] t = t_idx + hour full_bids[t] = {} diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index 6ece20967c..e5931269f0 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -14,6 +14,7 @@ from numbers import Real import numpy as np import idaes.logger as idaeslog +import pandas as pd _logger = idaeslog.getLogger(__name__) @@ -709,12 +710,7 @@ def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_resul class PerfectForecaster(AbstractPrescientPriceForecaster): - ''' - Forecast the perfect capacity factor of renewables. - ''' - def __init__(self, data_path_or_df): - """ Perfect forecaster that reads the data from a Dataframe containing: - {bus}-DALMP @@ -727,7 +723,7 @@ def __init__(self, data_path_or_df): self.data = data_path_or_df else: raise ValueError - + def __getitem__(self, index): return self.data[index] @@ -737,10 +733,31 @@ def fetch_hourly_stats_from_prescient(self, prescient_hourly_stats): def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_result): pass - def forecast_day_ahead_prices(self): - pass - - def forecast_real_time_prices(Self): - pass + def forecast_day_ahead_and_real_time_prices(self, date, hour, bus, horizon, _): + rt_forecast = self.forecast_real_time_prices( + date, hour, bus, horizon, _ + ) + da_forecast = self.forecast_day_ahead_prices( + date, hour, bus, horizon, _ + ) + return da_forecast, rt_forecast + + def get_column_from_data(self, date, hour, horizon, col): + datetime_index = pd.to_datetime(date) + pd.Timedelta(hours=hour) + forecast = self.data[self.data.index >= datetime_index].head(horizon) + values = forecast[col].values + if len(values) < horizon: + values = np.append(values, self.data[col].values[:horizon - len(values)]) + return values + + def forecast_day_ahead_prices(self, date, hour, bus, horizon, _): + return self.get_column_from_data(date, hour, horizon, f'{bus}-DALMP') + + def forecast_real_time_prices(self, date, hour, bus, horizon, _): + return self.get_column_from_data(date, hour, horizon, f'{bus}-RTLMP') + + def forecast_day_ahead_capacity_factor(self, date, hour, gen, horizon): + return self.get_column_from_data(date, hour, horizon, f'{gen}-DACF') - \ No newline at end of file + def forecast_real_time_capacity_factor(self, date, hour, gen, horizon): + return self.get_column_from_data(date, hour, horizon, f'{gen}-RTCF') \ No newline at end of file diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder new file mode 100644 index 0000000000..90229b419f --- /dev/null +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder @@ -0,0 +1,25 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# + +import pytest +import pyomo.environ as pyo +from idaes.apps.grid_integration.bidder import Bidder +from idaes.apps.grid_integration.tests.util import ( + TestingModel, + TestingForecaster, + testing_model_data, +) +from pyomo.common import unittest as pyo_unittest +from idaes.apps.grid_integration.coordinator import prescient_avail + +# @pytest.fixture \ No newline at end of file diff --git a/idaes/apps/grid_integration/tests/test_perfectforecaster.py b/idaes/apps/grid_integration/tests/test_perfectforecaster.py new file mode 100644 index 0000000000..2c3f813993 --- /dev/null +++ b/idaes/apps/grid_integration/tests/test_perfectforecaster.py @@ -0,0 +1,121 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# + +import pytest +from pyomo.common import unittest as pyo_unittest +from idaes.apps.grid_integration.forecaster import PerfectForecaster +import idaes.logger as idaeslog +import pandas as pd +import numpy as np + +@pytest.fixture +def wind_df(): + start_year = 2020 + start_mon = 1 + start_day = 1 + start_date = pd.Timestamp(f"{start_year}-{start_mon:02d}-{start_day:02d} 00:00:00") + ix = pd.date_range(start=start_date, + end=start_date + + pd.offsets.DateOffset(days=1) + - pd.offsets.DateOffset(hours=1), + freq='1H') + df = pd.DataFrame(index = ix) + df["303_WIND_1-DACF"] = list(i/100 for i in range(24)) + df["303_WIND_1-RTCF"] = list((i+1)/100 for i in range(24)) + df["Caesar-DALMP"] = list(i for i in range(24)) + df["Caesar-RTLMP"] = list(i+1 for i in range(24)) + return df + +@pytest.fixture +def base_perfectforecaster(wind_df): + return PerfectForecaster(wind_df) + +@pytest.mark.unit +def test_create_perfectforecaster(wind_df): + perfectforecaster = PerfectForecaster(data_path_or_df=wind_df) + assert perfectforecaster.data is wind_df + + +@pytest.mark.unit +@pytest.mark.parametrize("value", [np.array([1,2,3,4,5]), [1,2,3,4,5]]) +def test_create_perfectforecaster_with_ndarray_and_list(value): + with pytest.raises(ValueError): + perfectforecaster = PerfectForecaster(value) + +@pytest.mark.unit +def test_get_column_from_data(base_perfectforecaster): + date = "2020-01-01" + hour = 0 + horizon = 24 + col = '303_WIND_1-DACF' + expected_forecast = [i/100 for i in range(24)] + result_forecast = base_perfectforecaster.get_column_from_data(date, hour, horizon, col) + + pyo_unittest.assertStructuredAlmostEqual( + first=result_forecast.tolist(), second=expected_forecast + ) + return + +@pytest.mark.unit +def test_forecast_day_ahead_prices(base_perfectforecaster): + date = "2020-01-01" + hour = 0 + horizon = 24 + bus = 'Caesar' + nsp = 0 # this nsp is not used in the self.forecast_day_ahead_prices(), but we have it to make this func consistent with the stochastic bidder and coordinator. + result_forecast = base_perfectforecaster.forecast_day_ahead_prices(date, hour, bus, horizon, nsp) + expected_forecast = [i for i in range(24)] + pyo_unittest.assertStructuredAlmostEqual( + first=result_forecast.tolist(), second=expected_forecast + ) + return + +@pytest.mark.unit +def test_forecast_real_time_prices(base_perfectforecaster): + date = "2020-01-01" + hour = 0 + horizon = 4 + bus = 'Caesar' + nsp = 0 + result_forecast = base_perfectforecaster.forecast_real_time_prices(date, hour, bus, horizon, nsp) + expected_forecast = [i+1 for i in range(4)] + pyo_unittest.assertStructuredAlmostEqual( + first=result_forecast.tolist(), second=expected_forecast + ) + return + +@pytest.mark.unit +def test_forecast_day_ahead_capacity_factor(base_perfectforecaster): + date = "2020-01-01" + hour = 0 + horizon = 24 + gen = '303_WIND_1' + result_forecast = base_perfectforecaster.forecast_day_ahead_capacity_factor(date, hour, gen, horizon) + expected_forecast = [i/100 for i in range(24)] + pyo_unittest.assertStructuredAlmostEqual( + first=result_forecast.tolist(), second=expected_forecast + ) + return + +@pytest.mark.unit +def test_forecast_real_time_capacity_factor(base_perfectforecaster): + date = "2020-01-01" + hour = 0 + horizon = 4 + gen = '303_WIND_1' + result_forecast = base_perfectforecaster.forecast_real_time_capacity_factor(date, hour, gen, horizon) + expected_forecast = [(i+1)/100 for i in range(4)] + pyo_unittest.assertStructuredAlmostEqual( + first=result_forecast.tolist(), second=expected_forecast + ) + return \ No newline at end of file From a9db27dfc7c821c6267e19cd9cfce96bdcec6f9b Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 9 May 2024 12:56:21 -0400 Subject: [PATCH 03/26] update the logic and fix bugs --- idaes/apps/grid_integration/bidder.py | 37 ++++++++++++++--- .../tests/test_PEM_Parameterized_bidder | 40 ++++++++++++++++++- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index da7e8a4b3b..7d573f6b51 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1524,7 +1524,8 @@ def __init__( below which, make hydrogen and sell remainder of wind to grid pem_mw: maximum PEM capacity limits how much energy is bid at the `pem_marginal_cost` - + + real_time_bidding_only: bool, if True, do real-time bidding only. ''' super().__init__(bidding_model_object, day_ahead_horizon, @@ -1536,7 +1537,16 @@ def __init__( self.pem_marginal_cost = pem_marginal_cost self.pem_mw = pem_mw self.real_time_bidding_only = real_time_bidding_only + self._check_power() + def _check_power(self): + ''' + Check the power of PEM should not exceed the power of renewables + ''' + if self.pem_mw >= self.renewable_mw: + raise ValueError( + f"The power of PEM is greater than the renewabele power." + ) def compute_day_ahead_bids(self, date, hour=0): """ @@ -1619,17 +1629,34 @@ def compute_real_time_bids( avail_rt_wind = max(0, rt_wind - da_dispatch) grid_wind = max(0 , avail_rt_wind - self.pem_mw) - if grid_wind == 0: + if avail_rt_wind == 0: + bids = [(0, 0), (0, 0)] + if avail_rt_wind > 0 and grid_wind == 0: bids = [(0, 0), (avail_rt_wind, self.pem_marginal_cost)] - else: + if avail_rt_wind > 0 and grid_wind > 0: bids = [(0, 0), (grid_wind, 0), (avail_rt_wind, self.pem_marginal_cost)] + cost_curve = convert_marginal_costs_to_actual_costs(bids) + print(bids) + temp_curve = { + "data_type": "cost_curve", + "cost_curve_type": "piecewise", + "values": cost_curve, + } + tx_utils.validate_and_clean_cost_curve( + curve=temp_curve, + curve_type="cost_curve", + p_min=0, + p_max=max([p[0] for p in cost_curve]), + gen_name=gen, + t=t_idx, + ) t = t_idx + hour full_bids[t] = {} full_bids[t][gen] = {} - full_bids[t][gen]["p_cost"] = convert_marginal_costs_to_actual_costs(bids) + full_bids[t][gen]["p_cost"] = cost_curve full_bids[t][gen]["p_min"] = 0 - full_bids[t][gen]["p_max"] = rt_wind + full_bids[t][gen]["p_max"] = max([p[0] for p in cost_curve]) full_bids[t][gen]["startup_capacity"] = rt_wind full_bids[t][gen]["shutdown_capacity"] = rt_wind diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder index 90229b419f..79203d9eaf 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder @@ -13,7 +13,8 @@ import pytest import pyomo.environ as pyo -from idaes.apps.grid_integration.bidder import Bidder +from idaes.apps.grid_integration.bidder import PEMParametrizedBidder +from idaes.apps.grid_integration.forecaster import PerfectForecaster from idaes.apps.grid_integration.tests.util import ( TestingModel, TestingForecaster, @@ -22,4 +23,39 @@ from idaes.apps.grid_integration.tests.util import ( from pyomo.common import unittest as pyo_unittest from idaes.apps.grid_integration.coordinator import prescient_avail -# @pytest.fixture \ No newline at end of file +day_ahead_horizon = 24 +real_time_horizon = 4 +solver = pyo.SolverFactory("cbc") + + +@pytest.mark.unit +def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): + bidding_model_object = TestingModel(model_data=testing_model_data) + forecaster = TestingForecaster(prediction=30) + renewable_mw = 200 + pem_mw = 300 + pem_marginal_cost = 30 + with pytest.raises(ValueError, match=r".*The power of PEM is greater than the renewabele power.*"): + PEM_bidder = PEMParametrizedBidder(bidding_model_object, day_ahead_horizon, real_time_horizon, solver, forecaster, renewable_mw, pem_marginal_cost, pem_mw) + + +@pytest.fixture +def bidder_object(): + forecaster = TestingForecaster(prediction=30) + bidding_model_object = TestingModel(model_data=testing_model_data) + bidder_object = PEMParametrizedBidder( + bidding_model_object=bidding_model_object, + day_ahead_horizon=day_ahead_horizon, + real_time_horizon=real_time_horizon, + solver=solver, + forecaster=forecaster, + renewable_mw=400, + pem_marginal_cost=30, + pem_mw=200, + ) + return bidder_object + + +# @pytest.mark.unit +# def test_compute_day_ahead_bids(): +# return \ No newline at end of file From 7477b289ca2766513856a526bcf044f141a5a029 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 9 May 2024 13:04:59 -0400 Subject: [PATCH 04/26] change the test file names --- .../tests/test_PEM_Parameterized_bidder | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder deleted file mode 100644 index 79203d9eaf..0000000000 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder +++ /dev/null @@ -1,61 +0,0 @@ -################################################################################# -# The Institute for the Design of Advanced Energy Systems Integrated Platform -# Framework (IDAES IP) was produced under the DOE Institute for the -# Design of Advanced Energy Systems (IDAES). -# -# Copyright (c) 2018-2023 by the software owners: The Regents of the -# University of California, through Lawrence Berkeley National Laboratory, -# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon -# University, West Virginia University Research Corporation, et al. -# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md -# for full copyright and license information. -################################################################################# - -import pytest -import pyomo.environ as pyo -from idaes.apps.grid_integration.bidder import PEMParametrizedBidder -from idaes.apps.grid_integration.forecaster import PerfectForecaster -from idaes.apps.grid_integration.tests.util import ( - TestingModel, - TestingForecaster, - testing_model_data, -) -from pyomo.common import unittest as pyo_unittest -from idaes.apps.grid_integration.coordinator import prescient_avail - -day_ahead_horizon = 24 -real_time_horizon = 4 -solver = pyo.SolverFactory("cbc") - - -@pytest.mark.unit -def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): - bidding_model_object = TestingModel(model_data=testing_model_data) - forecaster = TestingForecaster(prediction=30) - renewable_mw = 200 - pem_mw = 300 - pem_marginal_cost = 30 - with pytest.raises(ValueError, match=r".*The power of PEM is greater than the renewabele power.*"): - PEM_bidder = PEMParametrizedBidder(bidding_model_object, day_ahead_horizon, real_time_horizon, solver, forecaster, renewable_mw, pem_marginal_cost, pem_mw) - - -@pytest.fixture -def bidder_object(): - forecaster = TestingForecaster(prediction=30) - bidding_model_object = TestingModel(model_data=testing_model_data) - bidder_object = PEMParametrizedBidder( - bidding_model_object=bidding_model_object, - day_ahead_horizon=day_ahead_horizon, - real_time_horizon=real_time_horizon, - solver=solver, - forecaster=forecaster, - renewable_mw=400, - pem_marginal_cost=30, - pem_mw=200, - ) - return bidder_object - - -# @pytest.mark.unit -# def test_compute_day_ahead_bids(): -# return \ No newline at end of file From f30b2b5a65f16d472dc555931e88c5dbd93ddb21 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 9 May 2024 13:09:44 -0400 Subject: [PATCH 05/26] use black --- idaes/apps/grid_integration/bidder.py | 110 ++++++++++-------- idaes/apps/grid_integration/forecaster.py | 23 ++-- .../tests/test_PEM_Parameterized_bidder.py | 72 ++++++++++++ .../tests/test_perfectforecaster.py | 70 ++++++----- 4 files changed, 185 insertions(+), 90 deletions(-) create mode 100644 idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index d54b2b4845..063815ef97 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1294,11 +1294,12 @@ def _record_bids(self, bids, date, hour, **kwargs): class ParametrizedBidder(AbstractBidder): - - ''' + + """ Create a parameterized bidder. Bid the resource at different prices. - ''' + """ + def __init__( self, bidding_model_object, @@ -1307,28 +1308,28 @@ def __init__( solver, forecaster, ): - ''' + """ Arguments: bidding_model_object: the model object for bidding day_ahead_horizon: number of time periods in the day-ahead bidding problem real_time_horizon: number of time periods in the real-time bidding problem - + solver: a Pyomo mathematical programming solver object - ''' + """ self.bidding_model_object = bidding_model_object self.day_ahead_horizon = day_ahead_horizon self.real_time_horizon = real_time_horizon self.solver = solver self.forecaster = forecaster - self.n_scenario = 1 # there must be a n_scenario attribute in this class + self.n_scenario = 1 # there must be a n_scenario attribute in this class self._check_inputs() self.generator = self.bidding_model_object.model_data.gen_name self.bids_result_list = [] - + @property def generator(self): return self._generator @@ -1337,37 +1338,41 @@ def generator(self): def generator(self, name): self._generator = name - def formulate_DA_bidding_problem(self): - ''' + """ No need to formulate a DA bidding problem here. - ''' + """ pass def formulate_RT_bidding_problem(self): - ''' + """ No need to formulate a RT bidding problem here. - ''' + """ pass def compute_day_ahead_bids(self, date, hour=0): raise NotImplementedError def compute_real_time_bids( - self, date, hour, realized_day_ahead_prices, realized_day_ahead_dispatches, tracker_profile + self, + date, + hour, + realized_day_ahead_prices, + realized_day_ahead_dispatches, + tracker_profile, ): raise NotImplementedError def update_day_ahead_model(self, **kwargs): - ''' + """ No need to update the RT bidding problem here. - ''' + """ pass def update_real_time_model(self, **kwargs): - ''' + """ No need to update the RT bidding problem here. - ''' + """ pass def record_bids(self, bids, model, date, hour, market): @@ -1455,12 +1460,13 @@ def write_results(self, path): class PEMParametrizedBidder(ParametrizedBidder): - + """ Renewable (PV or Wind) + PEM bidder that uses parameterized bid curve. Every timestep for RT or DA, max energy bid is the available wind resource. - Please use the + Please use the """ + def __init__( self, bidding_model_object, @@ -1471,25 +1477,27 @@ def __init__( renewable_mw, pem_marginal_cost, pem_mw, - real_time_bidding_only = False + real_time_bidding_only=False, ): - - ''' + + """ Arguments: renewable_mw: maximum renewable energy system capacity - + pem_marginal_cost: the cost/MW above which all available wind energy will be sold to grid; below which, make hydrogen and sell remainder of wind to grid pem_mw: maximum PEM capacity limits how much energy is bid at the `pem_marginal_cost` real_time_bidding_only: bool, if True, do real-time bidding only. - ''' - super().__init__(bidding_model_object, - day_ahead_horizon, - real_time_horizon, - solver, - forecaster) + """ + super().__init__( + bidding_model_object, + day_ahead_horizon, + real_time_horizon, + solver, + forecaster, + ) self.renewable_mw = renewable_mw self.renewable_marginal_cost = 0 self.pem_marginal_cost = pem_marginal_cost @@ -1498,13 +1506,11 @@ def __init__( self._check_power() def _check_power(self): - ''' + """ Check the power of PEM should not exceed the power of renewables - ''' + """ if self.pem_mw >= self.renewable_mw: - raise ValueError( - f"The power of PEM is greater than the renewabele power." - ) + raise ValueError(f"The power of PEM is greater than the renewabele power.") def compute_day_ahead_bids(self, date, hour=0): """ @@ -1515,7 +1521,9 @@ def compute_day_ahead_bids(self, date, hour=0): """ gen = self.generator # Forecast the day-ahead wind generation - forecast = self.forecaster.forecast_day_ahead_capacity_factor(date, hour, gen, self.day_ahead_horizon) + forecast = self.forecaster.forecast_day_ahead_capacity_factor( + date, hour, gen, self.day_ahead_horizon + ) full_bids = {} @@ -1531,9 +1539,9 @@ def compute_day_ahead_bids(self, date, hour=0): cost_curve = convert_marginal_costs_to_actual_costs(bids) temp_curve = { - "data_type": "cost_curve", - "cost_curve_type": "piecewise", - "values": cost_curve, + "data_type": "cost_curve", + "cost_curve_type": "piecewise", + "values": cost_curve, } tx_utils.validate_and_clean_cost_curve( curve=temp_curve, @@ -1554,11 +1562,10 @@ def compute_day_ahead_bids(self, date, hour=0): full_bids[t][gen]["shutdown_capacity"] = da_wind self._record_bids(full_bids, date, hour, Market="Day-ahead") - return full_bids - + return full_bids def compute_real_time_bids( - self, date, hour, realized_day_ahead_dispatches,realized_day_ahead_prices + self, date, hour, realized_day_ahead_dispatches, realized_day_ahead_prices ): """ RT Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. @@ -1566,9 +1573,11 @@ def compute_real_time_bids( If Wind resource at some time is less than PEM capacity, then reduce to available resource """ - + gen = self.generator - forecast = self.forecaster.forecast_real_time_capacity_factor(date, hour, gen, self.real_time_horizon) + forecast = self.forecaster.forecast_real_time_capacity_factor( + date, hour, gen, self.real_time_horizon + ) full_bids = {} for t_idx in range(self.real_time_horizon): @@ -1578,15 +1587,15 @@ def compute_real_time_bids( try: da_dispatch = realized_day_ahead_dispatches[t_idx + hour] except IndexError: - # When having indexerror, it must be the period that we are looking ahead. It is ok to set da_dispatch to 0 + # When having indexerror, it must be the period that we are looking ahead. It is ok to set da_dispatch to 0 da_dispatch = 0 # if we only participates in the RT market, then we do not consider the DA commitment if self.real_time_bidding_only: da_dispatch = 0 avail_rt_wind = max(0, rt_wind - da_dispatch) - grid_wind = max(0 , avail_rt_wind - self.pem_mw) - + grid_wind = max(0, avail_rt_wind - self.pem_mw) + if avail_rt_wind == 0: bids = [(0, 0), (0, 0)] if avail_rt_wind > 0 and grid_wind == 0: @@ -1596,9 +1605,9 @@ def compute_real_time_bids( cost_curve = convert_marginal_costs_to_actual_costs(bids) print(bids) temp_curve = { - "data_type": "cost_curve", - "cost_curve_type": "piecewise", - "values": cost_curve, + "data_type": "cost_curve", + "cost_curve_type": "piecewise", + "values": cost_curve, } tx_utils.validate_and_clean_cost_curve( curve=temp_curve, @@ -1619,6 +1628,5 @@ def compute_real_time_bids( full_bids[t][gen]["shutdown_capacity"] = rt_wind self._record_bids(full_bids, date, hour, Market="Real-time") - return full_bids diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index e15782aab6..d11e0bf8b3 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -685,7 +685,6 @@ def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_resul class PerfectForecaster(AbstractPrescientPriceForecaster): - def __init__(self, data_path_or_df): """ Perfect forecaster that reads the data from a Dataframe containing: @@ -694,7 +693,9 @@ def __init__(self, data_path_or_df): - {bus}-DACF and {bus}-RTCF for renewable plants """ if isinstance(data_path_or_df, str): - self.data = pd.read_csv(data_path_or_df, index_col="Datetime", parse_dates=True) + self.data = pd.read_csv( + data_path_or_df, index_col="Datetime", parse_dates=True + ) elif isinstance(data_path_or_df, pd.DataFrame): self.data = data_path_or_df else: @@ -710,12 +711,8 @@ def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_resul pass def forecast_day_ahead_and_real_time_prices(self, date, hour, bus, horizon, _): - rt_forecast = self.forecast_real_time_prices( - date, hour, bus, horizon, _ - ) - da_forecast = self.forecast_day_ahead_prices( - date, hour, bus, horizon, _ - ) + rt_forecast = self.forecast_real_time_prices(date, hour, bus, horizon, _) + da_forecast = self.forecast_day_ahead_prices(date, hour, bus, horizon, _) return da_forecast, rt_forecast def get_column_from_data(self, date, hour, horizon, col): @@ -723,17 +720,17 @@ def get_column_from_data(self, date, hour, horizon, col): forecast = self.data[self.data.index >= datetime_index].head(horizon) values = forecast[col].values if len(values) < horizon: - values = np.append(values, self.data[col].values[:horizon - len(values)]) + values = np.append(values, self.data[col].values[: horizon - len(values)]) return values def forecast_day_ahead_prices(self, date, hour, bus, horizon, _): - return self.get_column_from_data(date, hour, horizon, f'{bus}-DALMP') + return self.get_column_from_data(date, hour, horizon, f"{bus}-DALMP") def forecast_real_time_prices(self, date, hour, bus, horizon, _): - return self.get_column_from_data(date, hour, horizon, f'{bus}-RTLMP') + return self.get_column_from_data(date, hour, horizon, f"{bus}-RTLMP") def forecast_day_ahead_capacity_factor(self, date, hour, gen, horizon): - return self.get_column_from_data(date, hour, horizon, f'{gen}-DACF') + return self.get_column_from_data(date, hour, horizon, f"{gen}-DACF") def forecast_real_time_capacity_factor(self, date, hour, gen, horizon): - return self.get_column_from_data(date, hour, horizon, f'{gen}-RTCF') \ No newline at end of file + return self.get_column_from_data(date, hour, horizon, f"{gen}-RTCF") diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py new file mode 100644 index 0000000000..cdf419ebe6 --- /dev/null +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -0,0 +1,72 @@ +################################################################################# +# The Institute for the Design of Advanced Energy Systems Integrated Platform +# Framework (IDAES IP) was produced under the DOE Institute for the +# Design of Advanced Energy Systems (IDAES). +# +# Copyright (c) 2018-2023 by the software owners: The Regents of the +# University of California, through Lawrence Berkeley National Laboratory, +# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon +# University, West Virginia University Research Corporation, et al. +# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md +# for full copyright and license information. +################################################################################# + +import pytest +import pyomo.environ as pyo +from idaes.apps.grid_integration.bidder import PEMParametrizedBidder +from idaes.apps.grid_integration.forecaster import PerfectForecaster +from idaes.apps.grid_integration.tests.util import ( + TestingModel, + TestingForecaster, + testing_model_data, +) +from pyomo.common import unittest as pyo_unittest +from idaes.apps.grid_integration.coordinator import prescient_avail + +day_ahead_horizon = 24 +real_time_horizon = 4 +solver = pyo.SolverFactory("cbc") + + +@pytest.mark.unit +def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): + bidding_model_object = TestingModel(model_data=testing_model_data) + forecaster = TestingForecaster(prediction=30) + renewable_mw = 200 + pem_mw = 300 + pem_marginal_cost = 30 + with pytest.raises( + ValueError, match=r".*The power of PEM is greater than the renewabele power.*" + ): + PEM_bidder = PEMParametrizedBidder( + bidding_model_object, + day_ahead_horizon, + real_time_horizon, + solver, + forecaster, + renewable_mw, + pem_marginal_cost, + pem_mw, + ) + + +@pytest.fixture +def bidder_object(): + forecaster = TestingForecaster(prediction=30) + bidding_model_object = TestingModel(model_data=testing_model_data) + bidder_object = PEMParametrizedBidder( + bidding_model_object=bidding_model_object, + day_ahead_horizon=day_ahead_horizon, + real_time_horizon=real_time_horizon, + solver=solver, + forecaster=forecaster, + renewable_mw=400, + pem_marginal_cost=30, + pem_mw=200, + ) + return bidder_object + + +# @pytest.mark.unit +# def test_compute_day_ahead_bids(): +# return diff --git a/idaes/apps/grid_integration/tests/test_perfectforecaster.py b/idaes/apps/grid_integration/tests/test_perfectforecaster.py index 2c3f813993..e953111b6b 100644 --- a/idaes/apps/grid_integration/tests/test_perfectforecaster.py +++ b/idaes/apps/grid_integration/tests/test_perfectforecaster.py @@ -18,28 +18,31 @@ import pandas as pd import numpy as np + @pytest.fixture def wind_df(): start_year = 2020 start_mon = 1 start_day = 1 start_date = pd.Timestamp(f"{start_year}-{start_mon:02d}-{start_day:02d} 00:00:00") - ix = pd.date_range(start=start_date, - end=start_date - + pd.offsets.DateOffset(days=1) - - pd.offsets.DateOffset(hours=1), - freq='1H') - df = pd.DataFrame(index = ix) - df["303_WIND_1-DACF"] = list(i/100 for i in range(24)) - df["303_WIND_1-RTCF"] = list((i+1)/100 for i in range(24)) + ix = pd.date_range( + start=start_date, + end=start_date + pd.offsets.DateOffset(days=1) - pd.offsets.DateOffset(hours=1), + freq="1H", + ) + df = pd.DataFrame(index=ix) + df["303_WIND_1-DACF"] = list(i / 100 for i in range(24)) + df["303_WIND_1-RTCF"] = list((i + 1) / 100 for i in range(24)) df["Caesar-DALMP"] = list(i for i in range(24)) - df["Caesar-RTLMP"] = list(i+1 for i in range(24)) + df["Caesar-RTLMP"] = list(i + 1 for i in range(24)) return df + @pytest.fixture def base_perfectforecaster(wind_df): return PerfectForecaster(wind_df) + @pytest.mark.unit def test_create_perfectforecaster(wind_df): perfectforecaster = PerfectForecaster(data_path_or_df=wind_df) @@ -47,75 +50,90 @@ def test_create_perfectforecaster(wind_df): @pytest.mark.unit -@pytest.mark.parametrize("value", [np.array([1,2,3,4,5]), [1,2,3,4,5]]) +@pytest.mark.parametrize("value", [np.array([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]]) def test_create_perfectforecaster_with_ndarray_and_list(value): with pytest.raises(ValueError): perfectforecaster = PerfectForecaster(value) + @pytest.mark.unit def test_get_column_from_data(base_perfectforecaster): date = "2020-01-01" hour = 0 horizon = 24 - col = '303_WIND_1-DACF' - expected_forecast = [i/100 for i in range(24)] - result_forecast = base_perfectforecaster.get_column_from_data(date, hour, horizon, col) + col = "303_WIND_1-DACF" + expected_forecast = [i / 100 for i in range(24)] + result_forecast = base_perfectforecaster.get_column_from_data( + date, hour, horizon, col + ) pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) return + @pytest.mark.unit def test_forecast_day_ahead_prices(base_perfectforecaster): date = "2020-01-01" hour = 0 horizon = 24 - bus = 'Caesar' - nsp = 0 # this nsp is not used in the self.forecast_day_ahead_prices(), but we have it to make this func consistent with the stochastic bidder and coordinator. - result_forecast = base_perfectforecaster.forecast_day_ahead_prices(date, hour, bus, horizon, nsp) + bus = "Caesar" + nsp = 0 # this nsp is not used in the self.forecast_day_ahead_prices(), but we have it to make this func consistent with the stochastic bidder and coordinator. + result_forecast = base_perfectforecaster.forecast_day_ahead_prices( + date, hour, bus, horizon, nsp + ) expected_forecast = [i for i in range(24)] pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) return + @pytest.mark.unit def test_forecast_real_time_prices(base_perfectforecaster): date = "2020-01-01" hour = 0 horizon = 4 - bus = 'Caesar' + bus = "Caesar" nsp = 0 - result_forecast = base_perfectforecaster.forecast_real_time_prices(date, hour, bus, horizon, nsp) - expected_forecast = [i+1 for i in range(4)] + result_forecast = base_perfectforecaster.forecast_real_time_prices( + date, hour, bus, horizon, nsp + ) + expected_forecast = [i + 1 for i in range(4)] pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) return + @pytest.mark.unit def test_forecast_day_ahead_capacity_factor(base_perfectforecaster): date = "2020-01-01" hour = 0 horizon = 24 - gen = '303_WIND_1' - result_forecast = base_perfectforecaster.forecast_day_ahead_capacity_factor(date, hour, gen, horizon) - expected_forecast = [i/100 for i in range(24)] + gen = "303_WIND_1" + result_forecast = base_perfectforecaster.forecast_day_ahead_capacity_factor( + date, hour, gen, horizon + ) + expected_forecast = [i / 100 for i in range(24)] pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) return + @pytest.mark.unit def test_forecast_real_time_capacity_factor(base_perfectforecaster): date = "2020-01-01" hour = 0 horizon = 4 - gen = '303_WIND_1' - result_forecast = base_perfectforecaster.forecast_real_time_capacity_factor(date, hour, gen, horizon) - expected_forecast = [(i+1)/100 for i in range(4)] + gen = "303_WIND_1" + result_forecast = base_perfectforecaster.forecast_real_time_capacity_factor( + date, hour, gen, horizon + ) + expected_forecast = [(i + 1) / 100 for i in range(4)] pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) - return \ No newline at end of file + return From 351bde0d85c6253ea2d13633833b32e23a5d3f7a Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 9 May 2024 13:59:22 -0400 Subject: [PATCH 06/26] update tests --- idaes/apps/grid_integration/forecaster.py | 7 ++++--- .../tests/test_PEM_Parameterized_bidder.py | 9 ++------- .../grid_integration/tests/test_perfectforecaster.py | 8 +++----- 3 files changed, 9 insertions(+), 15 deletions(-) diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index d11e0bf8b3..98ec31de76 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -10,12 +10,11 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# +import pandas as pd from abc import ABC, abstractmethod from numbers import Real import numpy as np import idaes.logger as idaeslog -import pandas as pd - _logger = idaeslog.getLogger(__name__) @@ -699,7 +698,9 @@ def __init__(self, data_path_or_df): elif isinstance(data_path_or_df, pd.DataFrame): self.data = data_path_or_df else: - raise ValueError + raise ValueError( + "The data_path_or_df should be pandas DataFrame or a string of the csv path" + ) def __getitem__(self, index): return self.data[index] diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index cdf419ebe6..35ad79a723 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -13,6 +13,7 @@ import pytest import pyomo.environ as pyo +from pyomo.common import unittest as pyo_unittest from idaes.apps.grid_integration.bidder import PEMParametrizedBidder from idaes.apps.grid_integration.forecaster import PerfectForecaster from idaes.apps.grid_integration.tests.util import ( @@ -20,7 +21,6 @@ TestingForecaster, testing_model_data, ) -from pyomo.common import unittest as pyo_unittest from idaes.apps.grid_integration.coordinator import prescient_avail day_ahead_horizon = 24 @@ -64,9 +64,4 @@ def bidder_object(): pem_marginal_cost=30, pem_mw=200, ) - return bidder_object - - -# @pytest.mark.unit -# def test_compute_day_ahead_bids(): -# return + return bidder_object \ No newline at end of file diff --git a/idaes/apps/grid_integration/tests/test_perfectforecaster.py b/idaes/apps/grid_integration/tests/test_perfectforecaster.py index e953111b6b..9a777fcde5 100644 --- a/idaes/apps/grid_integration/tests/test_perfectforecaster.py +++ b/idaes/apps/grid_integration/tests/test_perfectforecaster.py @@ -12,11 +12,11 @@ ################################################################################# import pytest +import pandas as pd +import numpy as np from pyomo.common import unittest as pyo_unittest from idaes.apps.grid_integration.forecaster import PerfectForecaster import idaes.logger as idaeslog -import pandas as pd -import numpy as np @pytest.fixture @@ -52,7 +52,7 @@ def test_create_perfectforecaster(wind_df): @pytest.mark.unit @pytest.mark.parametrize("value", [np.array([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]]) def test_create_perfectforecaster_with_ndarray_and_list(value): - with pytest.raises(ValueError): + with pytest.raises(ValueError, match=r".*The data_path_or_df should be pandas DataFrame or a string of the csv path.*"): perfectforecaster = PerfectForecaster(value) @@ -70,7 +70,6 @@ def test_get_column_from_data(base_perfectforecaster): pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) - return @pytest.mark.unit @@ -87,7 +86,6 @@ def test_forecast_day_ahead_prices(base_perfectforecaster): pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) - return @pytest.mark.unit From f6ac3e07421b0adae0562748e61f82819ec35e12 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 14 May 2024 13:14:15 -0400 Subject: [PATCH 07/26] update tests --- idaes/apps/grid_integration/bidder.py | 8 ++++---- idaes/apps/grid_integration/forecaster.py | 2 +- .../tests/test_PEM_Parameterized_bidder.py | 8 ++++---- .../apps/grid_integration/tests/test_perfectforecaster.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 063815ef97..c409531568 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -10,14 +10,14 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# -import pandas as pd -import pyomo.environ as pyo -from pyomo.opt.base.solvers import OptSolver import os from abc import ABC, abstractmethod -from idaes.apps.grid_integration.utils import convert_marginal_costs_to_actual_costs import datetime +import pandas as pd +import pyomo.environ as pyo +from pyomo.opt.base.solvers import OptSolver from pyomo.common.dependencies import attempt_import +from idaes.apps.grid_integration.utils import convert_marginal_costs_to_actual_costs egret, egret_avail = attempt_import("egret") if egret_avail: diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index 98ec31de76..73b384b715 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -10,9 +10,9 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# -import pandas as pd from abc import ABC, abstractmethod from numbers import Real +import pandas as pd import numpy as np import idaes.logger as idaeslog diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index 35ad79a723..e2d9463c5f 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -10,10 +10,9 @@ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md # for full copyright and license information. ################################################################################# - import pytest -import pyomo.environ as pyo from pyomo.common import unittest as pyo_unittest +from pyomo.opt.base.solvers import OptSolver from idaes.apps.grid_integration.bidder import PEMParametrizedBidder from idaes.apps.grid_integration.forecaster import PerfectForecaster from idaes.apps.grid_integration.tests.util import ( @@ -25,7 +24,8 @@ day_ahead_horizon = 24 real_time_horizon = 4 -solver = pyo.SolverFactory("cbc") +# instead of using cbc, use a quasi solver to pass the _check_solver. +solver = OptSolver(type="solver") @pytest.mark.unit @@ -64,4 +64,4 @@ def bidder_object(): pem_marginal_cost=30, pem_mw=200, ) - return bidder_object \ No newline at end of file + return bidder_object diff --git a/idaes/apps/grid_integration/tests/test_perfectforecaster.py b/idaes/apps/grid_integration/tests/test_perfectforecaster.py index 9a777fcde5..b31e118b29 100644 --- a/idaes/apps/grid_integration/tests/test_perfectforecaster.py +++ b/idaes/apps/grid_integration/tests/test_perfectforecaster.py @@ -52,7 +52,10 @@ def test_create_perfectforecaster(wind_df): @pytest.mark.unit @pytest.mark.parametrize("value", [np.array([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]]) def test_create_perfectforecaster_with_ndarray_and_list(value): - with pytest.raises(ValueError, match=r".*The data_path_or_df should be pandas DataFrame or a string of the csv path.*"): + with pytest.raises( + ValueError, + match=r".*The data_path_or_df should be pandas DataFrame or a string of the csv path.*", + ): perfectforecaster = PerfectForecaster(value) @@ -102,7 +105,6 @@ def test_forecast_real_time_prices(base_perfectforecaster): pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) - return @pytest.mark.unit @@ -118,7 +120,6 @@ def test_forecast_day_ahead_capacity_factor(base_perfectforecaster): pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) - return @pytest.mark.unit @@ -134,4 +135,3 @@ def test_forecast_real_time_capacity_factor(base_perfectforecaster): pyo_unittest.assertStructuredAlmostEqual( first=result_forecast.tolist(), second=expected_forecast ) - return From 5d9a205610f584440b75a743afa053e6b074068d Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 14 May 2024 13:56:21 -0400 Subject: [PATCH 08/26] update black and test --- idaes/apps/grid_integration/bidder.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index c409531568..f1ccf78e49 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1294,7 +1294,6 @@ def _record_bids(self, bids, date, hour, **kwargs): class ParametrizedBidder(AbstractBidder): - """ Create a parameterized bidder. Bid the resource at different prices. @@ -1376,7 +1375,6 @@ def update_real_time_model(self, **kwargs): pass def record_bids(self, bids, model, date, hour, market): - """ This function records the bids and the details in the underlying bidding model. @@ -1460,7 +1458,6 @@ def write_results(self, path): class PEMParametrizedBidder(ParametrizedBidder): - """ Renewable (PV or Wind) + PEM bidder that uses parameterized bid curve. Every timestep for RT or DA, max energy bid is the available wind resource. @@ -1479,7 +1476,6 @@ def __init__( pem_mw, real_time_bidding_only=False, ): - """ Arguments: renewable_mw: maximum renewable energy system capacity From 0011b8ff09ab04460da14165c8e93c6e3db3620d Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 14 May 2024 16:09:46 -0400 Subject: [PATCH 09/26] add more tests --- .../tests/test_PEM_Parameterized_bidder.py | 136 ++++++++++++++++-- .../tests/test_perfectforecaster.py | 2 +- 2 files changed, 126 insertions(+), 12 deletions(-) diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index e2d9463c5f..e722620e70 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -11,27 +11,29 @@ # for full copyright and license information. ################################################################################# import pytest +import pandas as pd from pyomo.common import unittest as pyo_unittest from pyomo.opt.base.solvers import OptSolver from idaes.apps.grid_integration.bidder import PEMParametrizedBidder from idaes.apps.grid_integration.forecaster import PerfectForecaster from idaes.apps.grid_integration.tests.util import ( - TestingModel, - TestingForecaster, - testing_model_data, + ExampleModel, + ExampleForecaster, + testing_renewable_data, ) from idaes.apps.grid_integration.coordinator import prescient_avail +from idaes.apps.grid_integration.utils import convert_marginal_costs_to_actual_costs -day_ahead_horizon = 24 -real_time_horizon = 4 +day_ahead_horizon = 6 +real_time_horizon = 6 # instead of using cbc, use a quasi solver to pass the _check_solver. solver = OptSolver(type="solver") @pytest.mark.unit def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): - bidding_model_object = TestingModel(model_data=testing_model_data) - forecaster = TestingForecaster(prediction=30) + bidding_model_object = ExampleModel(model_data=testing_renewable_data) + forecaster = ExampleForecaster(prediction=30) renewable_mw = 200 pem_mw = 300 pem_marginal_cost = 30 @@ -50,18 +52,130 @@ def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): ) +def wind_df(): + start_year = 2020 + start_mon = 1 + start_day = 1 + start_date = pd.Timestamp(f"{start_year}-{start_mon:02d}-{start_day:02d} 00:00:00") + ix = pd.date_range( + start=start_date, + end=start_date + + pd.offsets.DateOffset(days=1) + - pd.offsets.DateOffset(hours=24 - day_ahead_horizon + 1), + freq="1H", + ) + df = pd.DataFrame(index=ix) + gen = testing_renewable_data.gen_name + bus = testing_renewable_data.bus + df[f"{gen}-DACF"] = [0.1, 0.1, 0.1, 0.8, 0.8, 0.8] + df[f"{gen}-RTCF"] = [1.0, 0.2, 0.2, 0.75, 0.75, 0.75] + df[f"{bus}-DALMP"] = [10] * day_ahead_horizon + df[f"{bus}-RTLMP"] = [20] * real_time_horizon + return df + + @pytest.fixture def bidder_object(): - forecaster = TestingForecaster(prediction=30) - bidding_model_object = TestingModel(model_data=testing_model_data) + example_wind_df = wind_df() + forecaster = PerfectForecaster(example_wind_df) + bidding_model_object = ExampleModel(model_data=testing_renewable_data) bidder_object = PEMParametrizedBidder( bidding_model_object=bidding_model_object, day_ahead_horizon=day_ahead_horizon, real_time_horizon=real_time_horizon, solver=solver, forecaster=forecaster, - renewable_mw=400, + renewable_mw=200, pem_marginal_cost=30, - pem_mw=200, + pem_mw=100, ) return bidder_object + + +@pytest.mark.component +def test_compute_DA_bids(bidder_object): + gen = bidder_object.generator + pmin = bidder_object.bidding_model_object.pmin + pmax = bidder_object.bidding_model_object.pmax + pem_pmax = bidder_object.pem_mw + pem_marginal_cost = bidder_object.pem_marginal_cost + date = "2020-01-01" + + # test DA bidding + bids = bidder_object.compute_day_ahead_bids(date=date, hour=0) + expected_DA_cf = [0.1, 0.1, 0.1, 0.8, 0.8, 0.8] + expected_bids = {} + for t in range(day_ahead_horizon): + expect_da_wind = expected_DA_cf[t] * pmax + if t <= 2: + expect_bids_curve = [(0, 0), (expected_DA_cf[t] * pmax, pem_marginal_cost)] + else: + expect_bids_curve = [ + (0, 0), + (expected_DA_cf[t] * pmax - pem_pmax, 0), + (expected_DA_cf[t] * pmax, pem_marginal_cost), + ] + expect_cost_curve = convert_marginal_costs_to_actual_costs(expect_bids_curve) + expected_bids[t] = { + gen: { + "p_cost": expect_cost_curve, + "p_min": pmin, + "p_max": expect_da_wind, + "startup_capacity": expect_da_wind, + "shutdown_capacity": expect_da_wind, + } + } + pyo_unittest.assertStructuredAlmostEqual(first=expected_bids, second=bids) + + +@pytest.mark.component +def test_compute_RT_bids(bidder_object): + gen = bidder_object.generator + pmin = bidder_object.bidding_model_object.pmin + pmax = bidder_object.bidding_model_object.pmax + pem_pmax = bidder_object.pem_mw + pem_marginal_cost = bidder_object.pem_marginal_cost + date = "2020-01-01" + # totally 3 cases: + # rt_wind - realized_day_ahead_dispatches > P_pem_max (t = 0) + # 0 <= rt_wind - realized_day_ahead_dispatches <= P_pem_max (t = 1, 2, 3, 4) + # rt_wind <= realized_day_ahead_dispatches (t = 5) + realized_day_ahead_dispatches = [20, 20, 20, 120, 120, 180] + realized_day_ahead_prices = None + + # testing RT bidding + bids = bidder_object.compute_real_time_bids( + date=date, + hour=0, + realized_day_ahead_dispatches=realized_day_ahead_dispatches, + realized_day_ahead_prices=realized_day_ahead_prices, + ) + + expected_RT_cf = [1.0, 0.2, 0.2, 0.75, 0.75, 0.75] + expected_bids = {} + for t in range(real_time_horizon): + expect_rt_wind = expected_RT_cf[t] * pmax + if t == 0: + expect_bids_curve = [ + (0, 0), + (expect_rt_wind - realized_day_ahead_dispatches[t] - pem_pmax, 0), + (expect_rt_wind - realized_day_ahead_dispatches[t], pem_marginal_cost), + ] + elif t in [1, 2, 3, 4]: + expect_bids_curve = [ + (0, 0), + (expect_rt_wind - realized_day_ahead_dispatches[t], pem_marginal_cost), + ] + else: + expect_bids_curve = [(0, 0)] + expect_cost_curve = convert_marginal_costs_to_actual_costs(expect_bids_curve) + expected_bids[t] = { + gen: { + "p_cost": expect_cost_curve, + "p_min": pmin, + "p_max": max([p[0] for p in expect_cost_curve]), + "startup_capacity": expect_rt_wind, + "shutdown_capacity": expect_rt_wind, + } + } + pyo_unittest.assertStructuredAlmostEqual(first=expected_bids, second=bids) diff --git a/idaes/apps/grid_integration/tests/test_perfectforecaster.py b/idaes/apps/grid_integration/tests/test_perfectforecaster.py index b31e118b29..832a0979f1 100644 --- a/idaes/apps/grid_integration/tests/test_perfectforecaster.py +++ b/idaes/apps/grid_integration/tests/test_perfectforecaster.py @@ -43,7 +43,7 @@ def base_perfectforecaster(wind_df): return PerfectForecaster(wind_df) -@pytest.mark.unit +@pytest.mark.component def test_create_perfectforecaster(wind_df): perfectforecaster = PerfectForecaster(data_path_or_df=wind_df) assert perfectforecaster.data is wind_df From aed28b8d6edbbe67e767d4969c837313b05c8855 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 14 May 2024 18:09:34 -0400 Subject: [PATCH 10/26] move the docs to this branch --- .../apps/grid_integration/Bidder.rst | 149 ++++++++++++++++++ .../apps/grid_integration/Tracker.rst | 53 +++++++ .../apps/grid_integration/index.rst | 2 +- 3 files changed, 203 insertions(+), 1 deletion(-) diff --git a/docs/reference_guides/apps/grid_integration/Bidder.rst b/docs/reference_guides/apps/grid_integration/Bidder.rst index 993c51f9d9..446d468ada 100644 --- a/docs/reference_guides/apps/grid_integration/Bidder.rst +++ b/docs/reference_guides/apps/grid_integration/Bidder.rst @@ -13,6 +13,155 @@ uncertain price scenario has a corresponding power output. As shown in the figur each of these uncertain price and power output pairs formulates a segment in the bidding curves. +Here we present a stochastic bidding model for a renewable integated energy system (Wind generator + PEM). + + +Day-Ahead Bidding Problem for Wind + PEM IES +--------------------------------------------- + +The objective function is the expected profit, which equals the revenue substracts the cost. +We want to consider the revenue from the electricity market and the hydrogen market. + +.. math:: \max \quad \sum_{s \in S, t \in T^{DA}}\omega_{s}[{(\pi_{t,s}^{DA}P_{t,s}^{DA} + \pi_{t,s}^{RT}(P_{t,s}^{RT}-P_{t,s}^{DA}))\Delta t + Pr^{H}m_{t,s}^{H}- c_{t,s}}] - C_{fix} + +s.t. + +.. math:: P_{t,s}^{DA} \leq P_{t,s}^{RT} \quad \forall t, s \quad \quad (1) +.. math:: P_{t,s} = P^{RT}_{t,s} \quad \forall t, s \quad \quad (2) +.. math:: (\pi_{t,s'}^{DA} - \pi_{t,s}^{DA})(P_{t,s'}^{DA} - P_{t,s}^{DA}) \geq 0 \quad \forall s \in S, \forall s' \in S \backslash s, \forall t \in T^{DA} \quad \quad (3) +.. math:: P_{t,s}^{DA} \leq P_{t,s}^{wind} \quad \forall t, s \quad \quad (4) +.. math:: P_{t,s}^{RT} \leq P_{t,s}^{wind} \quad \forall t, s \quad \quad (5) +.. math:: P_{t,s}^{wind} \leq f_{t} P_{max}^{wind} \quad \forall t, s \quad \quad (6) +.. math:: P_{t,s}^{DA} + P_{t,s}^{PEM} \leq P_{t, s}^{wind} \quad \forall t, s \quad \quad (7) +.. math:: P_{t,s}^{PEM} \leq P_{max}^{PEM} \quad \forall t, s \quad \quad (8) +.. math:: m_{t,s}^{H} = P_{t,s}^{PEM}C_{H}\Delta t \quad \forall t, s \quad \quad (9) +.. math:: c_{t,s} = C^{op} P_{t,s}^{PEM}\quad \forall t, s \quad \quad (10) +.. math:: C_{fix} = C_{fix}^{wind}P_{max}^{wind} + C_{fix}^{PEM}P_{max}^{PEM} \quad \quad (11) + +Equation (1) requires the day-ahead offering power is less or equal to the real-time offering power +in order to avoid underbidding. Equation (2) states that the RT offering power is the same as the +IES power output to the grid. In the bidding mode, the market rules require the offering power is +non-decreasing (convex) with its marginal cost in an energy bid. This rule is represented by equation (3). +Equation (4) to equation (9) are process model constraints. Equation (10) calculates the operation costs for IES +and equation (11) calculate the fixed cost for IES. + +**Parameters** + +:math:`\omega_{s}`: Frequency of each scenario. + +:math:`\pi^{DA}_{t,s}`: Day-ahead LMP forecasting from forecaster at hour t for scenario s, \$/MWh. + +:math:`\pi^{RT}_{t,s}`: Real-time LMP forecasting from forecaster at hour t for scenario s, \$/MWh. + +:math:`Pr^{H}`: Market price for hydrogen, \$/kg. + +:math:`P_{max}^{PEM}`: PEM max capacity, MW. + +:math:`f_{t}`: Wind power generation capacity factor at hour t, MW/MW. + +:math:`P_{max}^{wind}`: Wind generator max capacity, MW. + +:math:`C^{op}`: PEM operation cost coefficient, \$/MW. + +:math:`C_{fix}^{wind}`: Wind generator fixed cost coefficient, \$/MW. + +:math:`C_{fix}^{PEM}`: PEM fixed cost coefficient, \$/MW. + +:math:`C_{H}`: Electricity to hydrogen conversion rate, kg/MWh. + + +**Variables** + +:math:`P_{t,s}`: IES power output to the grid at hour t in scenario s, MW. + +:math:`P_{t,s}^{DA}`: Day-ahead offering power at hour t in scenario s, MW. + +:math:`P_{t,s}^{RT}`: Real-time offering power at hour t in scenario s, MW. + +:math:`P_{t,s}^{wind}`: Wind power generation at hour t in scenario s, MW. + +:math:`P_{t,s}^{PEM}`: Power delivered to PEM at hour t in scenario s, MW. + +:math:`m_{t,s}^{H}`: Hydrogen production mass at hour t in scenario s, kg. + +:math:`c_{t,s}`: IES operational cost at hour t in scenario s, \$. + + +Real-time Bidding Problem for Wind+PEM IES +------------------------------------------ + +.. math:: \max \quad \sum_{t \in T_{DA}}\hat{\pi}_{t}^{DA}\hat{P}_{t}^{DA}\Delta t + \sum_{t\in T_{RT}, s\in S}\omega_{s}[\pi_{t,s}^{RT}(P_{t,s}^{RT} -\hat{P}_{t}^{DA})\Delta t + Pr^{H}m_{t,s}^{H} - c_{t,s} - \omega_{t}^{RT} P_{t,s}^{underbid}] - C_{fix} + +s.t. + +.. math:: \hat{P}^{DA}_{t} \leq P_{t,s}^{RT} + P_{t,s}^{underbid} \quad \forall t, s \quad \quad (12) +.. math:: P_{t,s}^{RT} = P_{t,s} \quad \forall t, s \quad \quad (13) +.. math:: (\pi_{t,s'}^{RT} - \pi_{t,s}^{RT})(P_{t,s'}^{RT} - P_{t,s}^{RT}) \geq 0 \quad \forall s \in S, \forall s' \in S \backslash s, \forall t \in T^{RT} \quad \quad (14) +.. math:: P_{t,s}^{RT} \leq P_{t,s}^{wind} \quad \forall t, s \quad \quad (15) +.. math:: P_{t,s}^{wind} \leq f_{t}P_{wind}^{max} \quad \forall t, s \quad \quad (16) +.. math:: P_{t,s}^{RT} + P_{t,s}^{PEM} \leq P_{t,s}^{wind} \quad \forall t, s \quad \quad (17) +.. math:: P_{t,s}^{PEM} \leq P_{max}^{PEM} \quad \forall t, s \quad \quad (18) +.. math:: m_{t,s}^{H} = P_{t,s}^{PEM}C_{H}\Delta t \quad \forall t, s \quad \quad (19) +.. math:: c_{t,s} = C^{op} P_{t,s}^{PEM}\quad \forall t, s \quad \quad (20) +.. math:: C_{fix} = C_{fix}^{wind}P_{max}^{wind} + C_{fix}^{PEM}P_{max}^{PEM} \quad \quad (21) + +Before the actual operations, electricity markets allow the resources to submit real-time energy bids to +correct deviations from the day-ahead market. At this time, both day-ahead LMP :math:`\hat{\pi}_{t}^{DA}` +and day-ahead dispatch level :math:`\hat{P}_{t}^{DA}` have been realized as a result of the +day-ahead market clearing. In real-time market, due to the forecaster error and some other reasons, the +real-time offering power may not realize promises that generator owner makes in the day-ahead market. We +call this 'underbidding' and underbiding energy will be penaltized by the ISO. To prevent the underbidding +and loss of revenue, we add a relaxed lower bound for the real-time offering power with a slack +variable :math:`P_{t,s}^{underbid}` for underbidding in equation (12) and penalized in the objective function. + +**Parameters** + +:math:`\omega_{s}`: Frequency of each scenario. + +:math:`\omega_{t}^{RT}`: Penalty for underbidding at real-time at hour t, \$/MWh. + +:math:`\hat{\pi}_{t}^{DA}`: Realized day-ahead energy LMP signals at hour t, \$/MWh. + +:math:`\hat{P}_{t}^{DA}`: Realized day-ahead dispatch level at hour t, \$/MWH. + +:math:`\pi^{RT}_{t,s}`: Real-time LMP forecasting from forecaster at hour t for scenario s, \$/MWh. + +:math:`Pr^{H}`: Market price for hydrogen, \$/kg. + +:math:`P^{PEM}_{max}`: PEM max capacity, MW. + +:math:`f_{t}`: Wind power generation capacity factor at hour t, MW/MW. + +:math:`P_{max}^{wind}`: Wind generator max capacity, MW. + +:math:`C^{op}`: PEM operation cost coefficient, \$/MW. + +:math:`C_{fix}^{wind}`: Wind generator fixed cost coefficient, \$/MW. + +:math:`C_{fix}^{PEM}`: PEM fixed cost coefficient, \$/MW. + +:math:`C_{H}`: Electricity to hydrogen conversion rate, kg/MWh. + +**Variables** + +:math:`P_{t,s}`: IES power output to the grid at hour t in scenario s, MW. + +:math:`P_{t,s}^{underbid}`: The amount of underbidding power in real-time at hour t in scenario s, MW. + +:math:`P_{t,s}^{RT}`: Real-time offering power at hour t in scenario s, MW. + +:math:`P_{t,s}^{wind}`: Wind power generation at hour t in scenario s, MW. + +:math:`P_{t,s}^{PEM}`: Power delivered to PEM at hour t in scenario s, MW. + +:math:`m_{t,s}^{H}`: Hydrogen production mass at hour t in scenario s, kg. + +:math:`c_{t,s}`: IES operational cost at hour t in scenario s, \$. + +Some wind, battery, PEM models and the double-loop simulation example can be found in Dispatches GitHub repository. + +https://dispatches.readthedocs.io/en/main/models/renewables/index.html + .. |example_bid| image:: images/example_bid.png :width: 800 :alt: Alternative text diff --git a/docs/reference_guides/apps/grid_integration/Tracker.rst b/docs/reference_guides/apps/grid_integration/Tracker.rst index 85479420c8..8e44d2a1cf 100644 --- a/docs/reference_guides/apps/grid_integration/Tracker.rst +++ b/docs/reference_guides/apps/grid_integration/Tracker.rst @@ -9,6 +9,59 @@ integrated energy system which consists of a thermal generator and an energy sto The figure shows that to track the dispatch (load) the energy system can optimally use power output from charging and discharging cycle. + +Tracking Problem for Wind+PEM IES +---------------------------------- + +Here we present a tracking problem example for wind + PEM IES. + + +.. math:: \min \quad \sum_{t \in T^{RT}} (c_{t,0} + \omega_{t}(P^{+}_{t,0} + P^{-}_{t,0})) - C_{fix} + +s.t. + +.. math:: P_{t,0} + P_{t,0}^{-} = \hat{P}^{RT}_{t} + P^{+}_{t,0} \quad \forall t + +.. math:: P_{t,0} + \hat{P}_{t,0}^{PEM} \leq \hat{P}_{wind}^{RT} \quad \forall t + +.. math:: P_{t,0}^{PEM} \leq P_{max}^{PEM} \quad \forall t + +.. math:: m_{t,0}^{H} = P_{t,0}^{PEM}C_{H}\Delta t \quad \forall t + +.. math:: c_{t,0} = C^{op} P_{t,0}^{PEM}\quad \forall t + +.. math:: C_{fix} = C_{fix}^{wind}P_{max}^{wind} + C_{fix}^{PEM}P_{max}^{PEM} + +**Parameters** + +:math:`\hat{P}^{RT}_{t}`: Realized real-time dispatch level at hour t, MW. + +:math:`\hat{P}_{wind}^{RT}`: Realized wind generation power at hour t, MW. + +:math:`P_{max}^{PEM}`: PEM max capacity, MW. + +:math:`C^{op}`: PEM operation cost coefficient, \$/MW. + +:math:`C_{fix}^{wind}`: Wind generator fixed cost coefficient, \$/MW. + +:math:`C_{fix}^{PEM}`: PEM fixed cost coefficient, \$/MW. + +:math:`C_{H}`: Electricity to hydrogen conversion rate, kg/MWh. + +**Variables** + +:math:`P_{t,0}`: IES power output to the grid at hour t in scenario 0, MW. + +:math:`P_{t,0}^{PEM}`: Power delivered to PEM at hour t in scenario 0, MW. + +:math:`P_{t,0}^{+}`: Power over-delivered by the IES to the grid at hour t in scenario 0, MW. + +:math:`P_{t,0}^{-}`: Power under-delivered by the IES to the grid at hour t in scenario 0, MW. + +:math:`m_{t,0}^{H}`: Hydrogen production mass at hour t in scenario 0, kg. + +:math:`c_{t,0}`: IES operational cost at hour t in scenario 0, \$. + .. |tracking_example| image:: images/tracking_example.png :width: 1200 :alt: Alternative text diff --git a/docs/reference_guides/apps/grid_integration/index.rst b/docs/reference_guides/apps/grid_integration/index.rst index 0bf8ee19e2..08645e3b36 100644 --- a/docs/reference_guides/apps/grid_integration/index.rst +++ b/docs/reference_guides/apps/grid_integration/index.rst @@ -13,7 +13,7 @@ the operational (hours to year timescale) interactions between energy systems an wholesale electricity markets. For more information, please look at the introduction section. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 Introduction Implementation From fba617102ca867d0287daa8a73fce277d22096a0 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 16 May 2024 11:54:39 -0400 Subject: [PATCH 11/26] update docs --- docs/reference_guides/apps/grid_integration/Bidder.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/reference_guides/apps/grid_integration/Bidder.rst b/docs/reference_guides/apps/grid_integration/Bidder.rst index 446d468ada..88c6648f61 100644 --- a/docs/reference_guides/apps/grid_integration/Bidder.rst +++ b/docs/reference_guides/apps/grid_integration/Bidder.rst @@ -171,8 +171,18 @@ https://dispatches.readthedocs.io/en/main/models/renewables/index.html .. module:: idaes.apps.grid_integration.bidder +PEMParameterizedBidder +============================================ +The ``PEMParameterizedBidder`` bids the renewable-PEM IES at a constant price. +The logic of ``PEMParameterizedBidder`` is to reserve a part of the renewable generation +to co-prodcue the hydrogen. The reserved power can be sold at the marginal cost of the hydrogen +price. + .. autoclass:: Bidder :members: .. autoclass:: SelfScheduler :members: + +.. autoclass:: PEMParameterizedBidder + :members: \ No newline at end of file From 92e4b5e7172da84958803ec39d338ba3ede72bda Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 16 May 2024 12:36:13 -0400 Subject: [PATCH 12/26] update the docs and code comments --- .../apps/grid_integration/Bidder.rst | 16 ++--- idaes/apps/grid_integration/bidder.py | 60 +++++++++++++++++-- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/docs/reference_guides/apps/grid_integration/Bidder.rst b/docs/reference_guides/apps/grid_integration/Bidder.rst index 88c6648f61..33f91ed294 100644 --- a/docs/reference_guides/apps/grid_integration/Bidder.rst +++ b/docs/reference_guides/apps/grid_integration/Bidder.rst @@ -13,13 +13,13 @@ uncertain price scenario has a corresponding power output. As shown in the figur each of these uncertain price and power output pairs formulates a segment in the bidding curves. -Here we present a stochastic bidding model for a renewable integated energy system (Wind generator + PEM). +Here we present a stochastic bidding model for a renewable integrated energy system (Wind generator + PEM). Day-Ahead Bidding Problem for Wind + PEM IES --------------------------------------------- -The objective function is the expected profit, which equals the revenue substracts the cost. +The objective function is the expected profit, which equals the revenue substract the cost. We want to consider the revenue from the electricity market and the hydrogen market. .. math:: \max \quad \sum_{s \in S, t \in T^{DA}}\omega_{s}[{(\pi_{t,s}^{DA}P_{t,s}^{DA} + \pi_{t,s}^{RT}(P_{t,s}^{RT}-P_{t,s}^{DA}))\Delta t + Pr^{H}m_{t,s}^{H}- c_{t,s}}] - C_{fix} @@ -171,6 +171,12 @@ https://dispatches.readthedocs.io/en/main/models/renewables/index.html .. module:: idaes.apps.grid_integration.bidder +.. autoclass:: Bidder + :members: + +.. autoclass:: SelfScheduler + :members: + PEMParameterizedBidder ============================================ The ``PEMParameterizedBidder`` bids the renewable-PEM IES at a constant price. @@ -178,11 +184,5 @@ The logic of ``PEMParameterizedBidder`` is to reserve a part of the renewable ge to co-prodcue the hydrogen. The reserved power can be sold at the marginal cost of the hydrogen price. -.. autoclass:: Bidder - :members: - -.. autoclass:: SelfScheduler - :members: - .. autoclass:: PEMParameterizedBidder :members: \ No newline at end of file diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index f1ccf78e49..938bb69ccf 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1340,12 +1340,24 @@ def generator(self, name): def formulate_DA_bidding_problem(self): """ No need to formulate a DA bidding problem here. + + Arguments: + None + + Returns: + None """ pass def formulate_RT_bidding_problem(self): """ No need to formulate a RT bidding problem here. + + Arguments: + None + + Returns: + None """ pass @@ -1365,12 +1377,24 @@ def compute_real_time_bids( def update_day_ahead_model(self, **kwargs): """ No need to update the RT bidding problem here. + + Arguments: + None + + Returns: + None """ pass def update_real_time_model(self, **kwargs): """ No need to update the RT bidding problem here. + + Arguments: + None + + Returns: + None """ pass @@ -1404,6 +1428,20 @@ def record_bids(self, bids, model, date, hour, market): return def _record_bids(self, bids, date, hour, **kwargs): + """ + Record the bis of each time perid. + + Arguments: + bids: the obtained bids for this date. + + date: the date we bid into + + hour: the hour we bid into + + Returns: + None + + """ df_list = [] for t in bids: for gen in bids[t]: @@ -1459,9 +1497,7 @@ def write_results(self, path): class PEMParametrizedBidder(ParametrizedBidder): """ - Renewable (PV or Wind) + PEM bidder that uses parameterized bid curve. - Every timestep for RT or DA, max energy bid is the available wind resource. - Please use the + Renewable (PV or Wind) + PEM bidder that uses parameterized bid curves. """ def __init__( @@ -1514,6 +1550,15 @@ def compute_day_ahead_bids(self, date, hour=0): from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' If Wind resource at some time is less than PEM capacity, then reduce to available resource + + Arguments: + + date: the date we bid into + + hour: the hour we bid into + + Returns: + None """ gen = self.generator # Forecast the day-ahead wind generation @@ -1566,8 +1611,15 @@ def compute_real_time_bids( """ RT Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' + + Arguments: + + date: the date we bid into - If Wind resource at some time is less than PEM capacity, then reduce to available resource + hour: the hour we bid into + + Returns: + None """ gen = self.generator From a0c11be74b12f96e0e46b3bc72869db3c02c26db Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 16 May 2024 12:42:02 -0400 Subject: [PATCH 13/26] updates --- .../apps/grid_integration/Bidder.rst | 2 +- idaes/apps/grid_integration/bidder.py | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/reference_guides/apps/grid_integration/Bidder.rst b/docs/reference_guides/apps/grid_integration/Bidder.rst index 33f91ed294..ce96574d57 100644 --- a/docs/reference_guides/apps/grid_integration/Bidder.rst +++ b/docs/reference_guides/apps/grid_integration/Bidder.rst @@ -19,7 +19,7 @@ Here we present a stochastic bidding model for a renewable integrated energy sys Day-Ahead Bidding Problem for Wind + PEM IES --------------------------------------------- -The objective function is the expected profit, which equals the revenue substract the cost. +The objective function is the expected profit, which equals the revenue subtract the cost. We want to consider the revenue from the electricity market and the hydrogen market. .. math:: \max \quad \sum_{s \in S, t \in T^{DA}}\omega_{s}[{(\pi_{t,s}^{DA}P_{t,s}^{DA} + \pi_{t,s}^{RT}(P_{t,s}^{RT}-P_{t,s}^{DA}))\Delta t + Pr^{H}m_{t,s}^{H}- c_{t,s}}] - C_{fix} diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 938bb69ccf..d85d8d26d3 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1340,7 +1340,7 @@ def generator(self, name): def formulate_DA_bidding_problem(self): """ No need to formulate a DA bidding problem here. - + Arguments: None @@ -1352,7 +1352,7 @@ def formulate_DA_bidding_problem(self): def formulate_RT_bidding_problem(self): """ No need to formulate a RT bidding problem here. - + Arguments: None @@ -1552,11 +1552,11 @@ def compute_day_ahead_bids(self, date, hour=0): If Wind resource at some time is less than PEM capacity, then reduce to available resource Arguments: - + date: the date we bid into hour: the hour we bid into - + Returns: None """ @@ -1611,13 +1611,13 @@ def compute_real_time_bids( """ RT Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' - + Arguments: - + date: the date we bid into hour: the hour we bid into - + Returns: None """ From 4639ecce9f5923f7a4de1500fad09544d9edaba0 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 16 May 2024 13:04:53 -0400 Subject: [PATCH 14/26] fix doc typos --- docs/reference_guides/apps/grid_integration/Bidder.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/reference_guides/apps/grid_integration/Bidder.rst b/docs/reference_guides/apps/grid_integration/Bidder.rst index ce96574d57..8d3bb618c2 100644 --- a/docs/reference_guides/apps/grid_integration/Bidder.rst +++ b/docs/reference_guides/apps/grid_integration/Bidder.rst @@ -177,12 +177,12 @@ https://dispatches.readthedocs.io/en/main/models/renewables/index.html .. autoclass:: SelfScheduler :members: -PEMParameterizedBidder +PEMParametrizedBidder ============================================ -The ``PEMParameterizedBidder`` bids the renewable-PEM IES at a constant price. -The logic of ``PEMParameterizedBidder`` is to reserve a part of the renewable generation +The ``PEMParametrizedBidder`` bids the renewable-PEM IES at a constant price. +The logic of ``PEMParametrizedBidder`` is to reserve a part of the renewable generation to co-prodcue the hydrogen. The reserved power can be sold at the marginal cost of the hydrogen price. -.. autoclass:: PEMParameterizedBidder +.. autoclass:: PEMParametrizedBidder :members: \ No newline at end of file From 59cc8b0f4c6d19143ea4b9d2682536a0207f7bdf Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Wed, 29 May 2024 18:08:33 -0400 Subject: [PATCH 15/26] update the comments for tests --- .../tests/test_PEM_Parameterized_bidder.py | 17 ++++++++++++++ .../tests/test_perfectforecaster.py | 22 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index e722620e70..10c35b611f 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -32,6 +32,10 @@ @pytest.mark.unit def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): + """ + This is to test when we creat the PEM bidder with a PEM power greater than the renewable power, + there will be an error. + """ bidding_model_object = ExampleModel(model_data=testing_renewable_data) forecaster = ExampleForecaster(prediction=30) renewable_mw = 200 @@ -53,6 +57,10 @@ def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): def wind_df(): + """ + This is to define a dataframe fed to the PerfectForecaster with example DA/RT + capacity factors and LMPs. + """ start_year = 2020 start_mon = 1 start_day = 1 @@ -76,6 +84,9 @@ def wind_df(): @pytest.fixture def bidder_object(): + """ + This is to define a bidder object. + """ example_wind_df = wind_df() forecaster = PerfectForecaster(example_wind_df) bidding_model_object = ExampleModel(model_data=testing_renewable_data) @@ -94,6 +105,9 @@ def bidder_object(): @pytest.mark.component def test_compute_DA_bids(bidder_object): + """ + This is to test the if the compute_DA_bids function works correctly. + """ gen = bidder_object.generator pmin = bidder_object.bidding_model_object.pmin pmax = bidder_object.bidding_model_object.pmax @@ -130,6 +144,9 @@ def test_compute_DA_bids(bidder_object): @pytest.mark.component def test_compute_RT_bids(bidder_object): + """ + This is to test the if the compute_DA_bids function works correctly. + """ gen = bidder_object.generator pmin = bidder_object.bidding_model_object.pmin pmax = bidder_object.bidding_model_object.pmax diff --git a/idaes/apps/grid_integration/tests/test_perfectforecaster.py b/idaes/apps/grid_integration/tests/test_perfectforecaster.py index 832a0979f1..b0a464af90 100644 --- a/idaes/apps/grid_integration/tests/test_perfectforecaster.py +++ b/idaes/apps/grid_integration/tests/test_perfectforecaster.py @@ -21,6 +21,10 @@ @pytest.fixture def wind_df(): + """ + This is to define a dataframe fed to the PerfectForecaster with example DA/RT + capacity factors and LMPs. + """ start_year = 2020 start_mon = 1 start_day = 1 @@ -52,6 +56,9 @@ def test_create_perfectforecaster(wind_df): @pytest.mark.unit @pytest.mark.parametrize("value", [np.array([1, 2, 3, 4, 5]), [1, 2, 3, 4, 5]]) def test_create_perfectforecaster_with_ndarray_and_list(value): + """ + This is to test when we creat the PerfectForecaster with array or list, there will be an error. + """ with pytest.raises( ValueError, match=r".*The data_path_or_df should be pandas DataFrame or a string of the csv path.*", @@ -61,6 +68,9 @@ def test_create_perfectforecaster_with_ndarray_and_list(value): @pytest.mark.unit def test_get_column_from_data(base_perfectforecaster): + """ + This is to test the if the dataframe can be read correctly. + """ date = "2020-01-01" hour = 0 horizon = 24 @@ -77,6 +87,9 @@ def test_get_column_from_data(base_perfectforecaster): @pytest.mark.unit def test_forecast_day_ahead_prices(base_perfectforecaster): + """ + This is to test if the forecast_day_ahead_price function works correctly. + """ date = "2020-01-01" hour = 0 horizon = 24 @@ -93,6 +106,9 @@ def test_forecast_day_ahead_prices(base_perfectforecaster): @pytest.mark.unit def test_forecast_real_time_prices(base_perfectforecaster): + """ + This is to test if the forecast_real_time_price function works correctly. + """ date = "2020-01-01" hour = 0 horizon = 4 @@ -109,6 +125,9 @@ def test_forecast_real_time_prices(base_perfectforecaster): @pytest.mark.unit def test_forecast_day_ahead_capacity_factor(base_perfectforecaster): + """ + This is to test if the forecast_day_ahead_capacity_factor function works correctly. + """ date = "2020-01-01" hour = 0 horizon = 24 @@ -124,6 +143,9 @@ def test_forecast_day_ahead_capacity_factor(base_perfectforecaster): @pytest.mark.unit def test_forecast_real_time_capacity_factor(base_perfectforecaster): + """ + This is to test if the forecast_real_time_capacity_factor function works correctly. + """ date = "2020-01-01" hour = 0 horizon = 4 From 31b454c2b4bc7b742590dce1ad578c4814cd4791 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 18 Jun 2024 13:51:02 -0400 Subject: [PATCH 16/26] fix typos and update bidder tests use real numbers --- idaes/apps/grid_integration/bidder.py | 17 ++++++++------- .../tests/test_PEM_Parameterized_bidder.py | 21 ++++++++++++------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index d85d8d26d3..06d5ff822b 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1542,7 +1542,7 @@ def _check_power(self): Check the power of PEM should not exceed the power of renewables """ if self.pem_mw >= self.renewable_mw: - raise ValueError(f"The power of PEM is greater than the renewabele power.") + raise ValueError(f"The power of PEM is greater than the renewable power.") def compute_day_ahead_bids(self, date, hour=0): """ @@ -1571,7 +1571,7 @@ def compute_day_ahead_bids(self, date, hour=0): for t_idx in range(self.day_ahead_horizon): da_wind = forecast[t_idx] * self.renewable_mw grid_wind = max(0, da_wind - self.pem_mw) - # gird wind are bidded at marginal cost = 0 + # grid wind are bidded at marginal cost = 0 # The rest of the power is bidded at the pem marginal cost if grid_wind == 0: bids = [(0, 0), (da_wind, self.pem_marginal_cost)] @@ -1638,7 +1638,7 @@ def compute_real_time_bids( # When having indexerror, it must be the period that we are looking ahead. It is ok to set da_dispatch to 0 da_dispatch = 0 # if we only participates in the RT market, then we do not consider the DA commitment - if self.real_time_bidding_only: + else: da_dispatch = 0 avail_rt_wind = max(0, rt_wind - da_dispatch) @@ -1646,12 +1646,13 @@ def compute_real_time_bids( if avail_rt_wind == 0: bids = [(0, 0), (0, 0)] - if avail_rt_wind > 0 and grid_wind == 0: - bids = [(0, 0), (avail_rt_wind, self.pem_marginal_cost)] - if avail_rt_wind > 0 and grid_wind > 0: - bids = [(0, 0), (grid_wind, 0), (avail_rt_wind, self.pem_marginal_cost)] + else: + if grid_wind == 0: + bids = [(0, 0), (avail_rt_wind, self.pem_marginal_cost)] + else: + bids = [(0, 0), (grid_wind, 0), (avail_rt_wind, self.pem_marginal_cost)] cost_curve = convert_marginal_costs_to_actual_costs(bids) - print(bids) + temp_curve = { "data_type": "cost_curve", "cost_curve_type": "piecewise", diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index 10c35b611f..2991a87a44 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -42,7 +42,7 @@ def test_creat_PEMParametrizedBidder_with_wrong_PEM_power(): pem_mw = 300 pem_marginal_cost = 30 with pytest.raises( - ValueError, match=r".*The power of PEM is greater than the renewabele power.*" + ValueError, match=r".*The power of PEM is greater than the renewable power.*" ): PEM_bidder = PEMParametrizedBidder( bidding_model_object, @@ -122,12 +122,12 @@ def test_compute_DA_bids(bidder_object): for t in range(day_ahead_horizon): expect_da_wind = expected_DA_cf[t] * pmax if t <= 2: - expect_bids_curve = [(0, 0), (expected_DA_cf[t] * pmax, pem_marginal_cost)] + expect_bids_curve = [(0, 0), (20, 30)] else: expect_bids_curve = [ (0, 0), - (expected_DA_cf[t] * pmax - pem_pmax, 0), - (expected_DA_cf[t] * pmax, pem_marginal_cost), + (60, 0), + (160, 30), ] expect_cost_curve = convert_marginal_costs_to_actual_costs(expect_bids_curve) expected_bids[t] = { @@ -175,13 +175,18 @@ def test_compute_RT_bids(bidder_object): if t == 0: expect_bids_curve = [ (0, 0), - (expect_rt_wind - realized_day_ahead_dispatches[t] - pem_pmax, 0), - (expect_rt_wind - realized_day_ahead_dispatches[t], pem_marginal_cost), + (80, 0), + (180, 30), ] - elif t in [1, 2, 3, 4]: + elif t in [1, 2]: expect_bids_curve = [ (0, 0), - (expect_rt_wind - realized_day_ahead_dispatches[t], pem_marginal_cost), + (20, 30) + ] + elif t in [3, 4]: + expect_bids_curve = [ + (0, 0), + (30, 30) ] else: expect_bids_curve = [(0, 0)] From b2551a12b7eb9a58e866e162edaf1314b8352b4f Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 18 Jun 2024 14:35:11 -0400 Subject: [PATCH 17/26] use black to format codes --- idaes/apps/grid_integration/bidder.py | 8 ++++++-- .../tests/test_PEM_Parameterized_bidder.py | 10 ++-------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 06d5ff822b..41773d91ae 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1650,9 +1650,13 @@ def compute_real_time_bids( if grid_wind == 0: bids = [(0, 0), (avail_rt_wind, self.pem_marginal_cost)] else: - bids = [(0, 0), (grid_wind, 0), (avail_rt_wind, self.pem_marginal_cost)] + bids = [ + (0, 0), + (grid_wind, 0), + (avail_rt_wind, self.pem_marginal_cost), + ] cost_curve = convert_marginal_costs_to_actual_costs(bids) - + temp_curve = { "data_type": "cost_curve", "cost_curve_type": "piecewise", diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index 2991a87a44..e8fa10aa03 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -179,15 +179,9 @@ def test_compute_RT_bids(bidder_object): (180, 30), ] elif t in [1, 2]: - expect_bids_curve = [ - (0, 0), - (20, 30) - ] + expect_bids_curve = [(0, 0), (20, 30)] elif t in [3, 4]: - expect_bids_curve = [ - (0, 0), - (30, 30) - ] + expect_bids_curve = [(0, 0), (30, 30)] else: expect_bids_curve = [(0, 0)] expect_cost_curve = convert_marginal_costs_to_actual_costs(expect_bids_curve) From 25048f5e107fa72cd62e971801cea8c1a5f831ac Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Fri, 28 Jun 2024 15:16:29 -0400 Subject: [PATCH 18/26] add/change comments as Alex suggests --- idaes/apps/grid_integration/bidder.py | 122 +++++++++++++--------- idaes/apps/grid_integration/forecaster.py | 58 +++++++++- 2 files changed, 128 insertions(+), 52 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 41773d91ae..b9245e3b5b 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1309,21 +1309,28 @@ def __init__( ): """ Arguments: - bidding_model_object: the model object for bidding - - day_ahead_horizon: number of time periods in the day-ahead bidding problem + bidding_model_object: pyomo model object, + the IES model object for bidding. + day_ahead_horizon: int, + number of time periods in the day-ahead bidding problem. + real_time_horizon: int, + number of time periods in the real-time bidding problem. + solver: a Pyomo mathematical programming solver object, + solver for solving the bidding problem. In this class we do not need a solver. + forecaster: forecaster object, + the forecaster to predict the generator/IES capacity factor. - real_time_horizon: number of time periods in the real-time bidding problem - - solver: a Pyomo mathematical programming solver object """ self.bidding_model_object = bidding_model_object self.day_ahead_horizon = day_ahead_horizon self.real_time_horizon = real_time_horizon self.solver = solver self.forecaster = forecaster - - self.n_scenario = 1 # there must be a n_scenario attribute in this class + # Because ParameterizedBidder is inherited from the AbstractBidder, and we want to + # use the _check_inputs() function to check the solver and bidding_model_object. + # We must have the self.scenario attribute. In this case, I set the self.n_scenario = 1 when initializing. + # However, self.n_scenario will never be used in this class. + self.n_scenario = 1 self._check_inputs() self.generator = self.bidding_model_object.model_data.gen_name @@ -1398,18 +1405,22 @@ def update_real_time_model(self, **kwargs): """ pass - def record_bids(self, bids, model, date, hour, market): + def record_bids(self, bids: dict, model, date: str, hour: int, market): """ This function records the bids and the details in the underlying bidding model. Arguments: - bids: the obtained bids for this date. - - model: bidding model - - date: the date we bid into - - hour: the hour we bid into + bids: dictionary, + the obtained bids for this date. Keys are time step t, example as following: + bids = {t: {gen: {p_cost: float, p_max: float, p_min: float, startup_capacity: float, shutdown_capacity: float}}} + model: pyomo model object, + our bidding model. + date: str, + the date we bid into. + hour: int, + the hour we bid into. + market: str, + the market we participate. Returns: None @@ -1427,16 +1438,18 @@ def record_bids(self, bids, model, date, hour, market): return - def _record_bids(self, bids, date, hour, **kwargs): + def _record_bids(self, bids: dict, date: str, hour: int, **kwargs): """ Record the bis of each time perid. Arguments: - bids: the obtained bids for this date. - - date: the date we bid into - - hour: the hour we bid into + bids: dictionary, + the obtained bids for this date. Keys are time step t, example as following: + bids = {t: {gen: {p_cost: float, p_max: float, p_min: float, startup_capacity: float, shutdown_capacity: float}}} + date: str, + the date we bid into. + hour: int, + the hour we bid into. Returns: None @@ -1454,19 +1467,12 @@ def _record_bids(self, bids, date, hour, **kwargs): for k, v in kwargs.items(): result_dict[k] = v - pair_cnt = len(bids[t][gen]["p_cost"]) + num_bid_pairs = len(bids[t][gen]["p_cost"]) for idx, (power, cost) in enumerate(bids[t][gen]["p_cost"]): result_dict[f"Power {idx} [MW]"] = power result_dict[f"Cost {idx} [$]"] = cost - # place holder, in case different len of bids - while pair_cnt < self.n_scenario: - result_dict[f"Power {pair_cnt} [MW]"] = None - result_dict[f"Cost {pair_cnt} [$]"] = None - - pair_cnt += 1 - result_df = pd.DataFrame.from_dict(result_dict, orient="index") df_list.append(result_df.T) @@ -1481,7 +1487,8 @@ def write_results(self, path): This methods writes the saved operation stats into an csv file. Arguments: - path: the path to write the results. + path: str or Pathlib object, + the path to write the results. Return: None @@ -1514,14 +1521,25 @@ def __init__( ): """ Arguments: - renewable_mw: maximum renewable energy system capacity - - pem_marginal_cost: the cost/MW above which all available wind energy will be sold to grid; - below which, make hydrogen and sell remainder of wind to grid - - pem_mw: maximum PEM capacity limits how much energy is bid at the `pem_marginal_cost` - - real_time_bidding_only: bool, if True, do real-time bidding only. + bidding_model_object: pyomo model object, + the IES model object for bidding. + day_ahead_horizon: int, + number of time periods in the day-ahead bidding problem. + real_time_horizon: int, + number of time periods in the real-time bidding problem. + solver: a Pyomo mathematical programming solver object, + solver for solving the bidding problem. In this class we do not need a solver. + forecaster: forecaster object, + the forecaster to predict the generator/IES capacity factor. + renewable_mw: int or float, + maximum renewable energy system capacity. + pem_marginal_cost: int or float, + cost/MW, above which all available wind energy will be sold to grid; + below which, make hydrogen and sell remainder of wind to grid. + pem_mw: int or float, + maximum PEM capacity limits how much energy is bid at the `pem_marginal_cost`. + real_time_bidding_only: bool, + if True, do real-time bidding only. """ super().__init__( bidding_model_object, @@ -1544,7 +1562,7 @@ def _check_power(self): if self.pem_mw >= self.renewable_mw: raise ValueError(f"The power of PEM is greater than the renewable power.") - def compute_day_ahead_bids(self, date, hour=0): + def compute_day_ahead_bids(self, date: str, hour=0): """ DA Bid: from 0 MW to (Wind Resource - PEM capacity) MW, bid $0/MWh. from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' @@ -1552,13 +1570,15 @@ def compute_day_ahead_bids(self, date, hour=0): If Wind resource at some time is less than PEM capacity, then reduce to available resource Arguments: - - date: the date we bid into - - hour: the hour we bid into + date: str, + the date we bid into. + hour: int, + the hour we bid into. Returns: - None + full_bids: dictionary, + the obtained bids. Keys are time step t, example as following: + bids = {t: {gen: {p_cost: float, p_max: float, p_min: float, startup_capacity: float, shutdown_capacity: float}}} """ gen = self.generator # Forecast the day-ahead wind generation @@ -1613,13 +1633,15 @@ def compute_real_time_bids( from (Wind Resource - PEM capacity) MW to Wind Resource MW, bid 'pem_marginal_cost' Arguments: - - date: the date we bid into - - hour: the hour we bid into + date: str, + the date we bid into + hour: int, + the hour we bid into Returns: - None + full_bids: dictionary, + the obtained bids. Keys are time step t, example as following: + bids = {t: {gen: {p_cost: float, p_max: float, p_min: float, startup_capacity: float, shutdown_capacity: float}}} """ gen = self.generator diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index 73b384b715..fc0053eb47 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -684,12 +684,23 @@ def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_resul class PerfectForecaster(AbstractPrescientPriceForecaster): + ''' + PerfectForecaster is designed for the renewable-PEM Parameterized Bidder. + To bid into the renewable-PEM, user need to forecaster the renewable generation at each time period. + ''' def __init__(self, data_path_or_df): """ Perfect forecaster that reads the data from a Dataframe containing: - {bus}-DALMP - {bus}-RTLMP - {bus}-DACF and {bus}-RTCF for renewable plants + Perfect forecaster has no forecaster errors when forecasting the capacity factors + unless the user add errors in the data_path_or_df + + Argument: + data_path_or_df: str or pandas DataFrame, + if it is a str, it is the path of the file that stores the data of capacity factors; + if it is a DataFrame, that df should store the data of capacity factors. """ if isinstance(data_path_or_df, str): self.data = pd.read_csv( @@ -706,17 +717,60 @@ def __getitem__(self, index): return self.data[index] def fetch_hourly_stats_from_prescient(self, prescient_hourly_stats): + ''' + Fetch the hourly stats from prescient. + No need to have it here but forecaster should have this function to aviod an error from Coordinator. + ''' pass def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_result): + ''' + Fetch the day-ahead stats from prescient. + No need to have it here but forecaster should have this function to aviod an error from Coordinator. + ''' pass - def forecast_day_ahead_and_real_time_prices(self, date, hour, bus, horizon, _): + def forecast_day_ahead_and_real_time_prices(self, date: str, hour: int, bus: str, horizon: int, _): + ''' + Forecast day-ahead and real-time prices. + Not necessary to forecast prices when using PEMParameterized bidder. + But we build a function here for the future development. + + Arguments + date: str, + intended date of the forecasts. + hour: int, + intended hour of the forecasts. + bus: str, + intended bus of the forecasts. + horizon: int, + number of the time periods of the forecasts. + + Returns: + da_forecast: numpyp array, + the day-ahead price forecasts. + rt_forecast: numpyp array, + the real-time price forecasts. + ''' rt_forecast = self.forecast_real_time_prices(date, hour, bus, horizon, _) da_forecast = self.forecast_day_ahead_prices(date, hour, bus, horizon, _) return da_forecast, rt_forecast - def get_column_from_data(self, date, hour, horizon, col): + def get_column_from_data(self,date: str, hour: int, horizon: int, col: str): + ''' + Get the data from the dataframe. + + Arguments + date: str, + intended date of the forecasts. + hour: int, + intended hour of the forecasts. + horizon: int, + number of the time periods of the forecasts. + col: str, + the name of the column. + + ''' datetime_index = pd.to_datetime(date) + pd.Timedelta(hours=hour) forecast = self.data[self.data.index >= datetime_index].head(horizon) values = forecast[col].values From 0e13af64bb09e9fa453da4d445dd7a518dbc5e6a Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Fri, 28 Jun 2024 15:40:04 -0400 Subject: [PATCH 19/26] update docs --- docs/reference_guides/apps/grid_integration/Bidder.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/reference_guides/apps/grid_integration/Bidder.rst b/docs/reference_guides/apps/grid_integration/Bidder.rst index 8d3bb618c2..3ceda5855b 100644 --- a/docs/reference_guides/apps/grid_integration/Bidder.rst +++ b/docs/reference_guides/apps/grid_integration/Bidder.rst @@ -13,7 +13,7 @@ uncertain price scenario has a corresponding power output. As shown in the figur each of these uncertain price and power output pairs formulates a segment in the bidding curves. -Here we present a stochastic bidding model for a renewable integrated energy system (Wind generator + PEM). +Here we present a stochastic bidding model for a renewable integrated energy system (wind generator + polymer electrolyte membrane (PEM) electrolyzer). Day-Ahead Bidding Problem for Wind + PEM IES @@ -42,7 +42,7 @@ Equation (1) requires the day-ahead offering power is less or equal to the real- in order to avoid underbidding. Equation (2) states that the RT offering power is the same as the IES power output to the grid. In the bidding mode, the market rules require the offering power is non-decreasing (convex) with its marginal cost in an energy bid. This rule is represented by equation (3). -Equation (4) to equation (9) are process model constraints. Equation (10) calculates the operation costs for IES +Equation (4) to (9) are process model constraints. Equation (10) calculates the operation costs for IES and equation (11) calculate the fixed cost for IES. **Parameters** From 4db3cac5fa3563bfa4654ddd30f1403080bf0995 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Fri, 28 Jun 2024 15:41:03 -0400 Subject: [PATCH 20/26] use black to format codes --- idaes/apps/grid_integration/bidder.py | 16 +++--- idaes/apps/grid_integration/forecaster.py | 59 ++++++++++++----------- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index b9245e3b5b..fc699dfd07 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1329,8 +1329,8 @@ def __init__( # Because ParameterizedBidder is inherited from the AbstractBidder, and we want to # use the _check_inputs() function to check the solver and bidding_model_object. # We must have the self.scenario attribute. In this case, I set the self.n_scenario = 1 when initializing. - # However, self.n_scenario will never be used in this class. - self.n_scenario = 1 + # However, self.n_scenario will never be used in this class. + self.n_scenario = 1 self._check_inputs() self.generator = self.bidding_model_object.model_data.gen_name @@ -1410,7 +1410,7 @@ def record_bids(self, bids: dict, model, date: str, hour: int, market): This function records the bids and the details in the underlying bidding model. Arguments: - bids: dictionary, + bids: dictionary, the obtained bids for this date. Keys are time step t, example as following: bids = {t: {gen: {p_cost: float, p_max: float, p_min: float, startup_capacity: float, shutdown_capacity: float}}} model: pyomo model object, @@ -1443,12 +1443,12 @@ def _record_bids(self, bids: dict, date: str, hour: int, **kwargs): Record the bis of each time perid. Arguments: - bids: dictionary, + bids: dictionary, the obtained bids for this date. Keys are time step t, example as following: bids = {t: {gen: {p_cost: float, p_max: float, p_min: float, startup_capacity: float, shutdown_capacity: float}}} - date: str, + date: str, the date we bid into. - hour: int, + hour: int, the hour we bid into. Returns: @@ -1487,7 +1487,7 @@ def write_results(self, path): This methods writes the saved operation stats into an csv file. Arguments: - path: str or Pathlib object, + path: str or Pathlib object, the path to write the results. Return: @@ -1538,7 +1538,7 @@ def __init__( below which, make hydrogen and sell remainder of wind to grid. pem_mw: int or float, maximum PEM capacity limits how much energy is bid at the `pem_marginal_cost`. - real_time_bidding_only: bool, + real_time_bidding_only: bool, if True, do real-time bidding only. """ super().__init__( diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index fc0053eb47..0fb9b04019 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -684,10 +684,11 @@ def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_resul class PerfectForecaster(AbstractPrescientPriceForecaster): - ''' - PerfectForecaster is designed for the renewable-PEM Parameterized Bidder. + """ + PerfectForecaster is designed for the renewable-PEM Parameterized Bidder. To bid into the renewable-PEM, user need to forecaster the renewable generation at each time period. - ''' + """ + def __init__(self, data_path_or_df): """ Perfect forecaster that reads the data from a Dataframe containing: @@ -717,60 +718,62 @@ def __getitem__(self, index): return self.data[index] def fetch_hourly_stats_from_prescient(self, prescient_hourly_stats): - ''' - Fetch the hourly stats from prescient. + """ + Fetch the hourly stats from prescient. No need to have it here but forecaster should have this function to aviod an error from Coordinator. - ''' + """ pass def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_result): - ''' - Fetch the day-ahead stats from prescient. + """ + Fetch the day-ahead stats from prescient. No need to have it here but forecaster should have this function to aviod an error from Coordinator. - ''' + """ pass - def forecast_day_ahead_and_real_time_prices(self, date: str, hour: int, bus: str, horizon: int, _): - ''' - Forecast day-ahead and real-time prices. - Not necessary to forecast prices when using PEMParameterized bidder. + def forecast_day_ahead_and_real_time_prices( + self, date: str, hour: int, bus: str, horizon: int, _ + ): + """ + Forecast day-ahead and real-time prices. + Not necessary to forecast prices when using PEMParameterized bidder. But we build a function here for the future development. - + Arguments - date: str, + date: str, intended date of the forecasts. - hour: int, + hour: int, intended hour of the forecasts. - bus: str, + bus: str, intended bus of the forecasts. - horizon: int, + horizon: int, number of the time periods of the forecasts. Returns: - da_forecast: numpyp array, + da_forecast: numpyp array, the day-ahead price forecasts. - rt_forecast: numpyp array, - the real-time price forecasts. - ''' + rt_forecast: numpyp array, + the real-time price forecasts. + """ rt_forecast = self.forecast_real_time_prices(date, hour, bus, horizon, _) da_forecast = self.forecast_day_ahead_prices(date, hour, bus, horizon, _) return da_forecast, rt_forecast - def get_column_from_data(self,date: str, hour: int, horizon: int, col: str): - ''' + def get_column_from_data(self, date: str, hour: int, horizon: int, col: str): + """ Get the data from the dataframe. Arguments - date: str, + date: str, intended date of the forecasts. - hour: int, + hour: int, intended hour of the forecasts. - horizon: int, + horizon: int, number of the time periods of the forecasts. col: str, the name of the column. - ''' + """ datetime_index = pd.to_datetime(date) + pd.Timedelta(hours=hour) forecast = self.data[self.data.index >= datetime_index].head(horizon) values = forecast[col].values From a157a5803b0519c6754e76aabce5f9a5104dec2d Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Fri, 28 Jun 2024 15:44:04 -0400 Subject: [PATCH 21/26] fix typos --- idaes/apps/grid_integration/forecaster.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/idaes/apps/grid_integration/forecaster.py b/idaes/apps/grid_integration/forecaster.py index 0fb9b04019..407523ff94 100644 --- a/idaes/apps/grid_integration/forecaster.py +++ b/idaes/apps/grid_integration/forecaster.py @@ -720,14 +720,14 @@ def __getitem__(self, index): def fetch_hourly_stats_from_prescient(self, prescient_hourly_stats): """ Fetch the hourly stats from prescient. - No need to have it here but forecaster should have this function to aviod an error from Coordinator. + No need to have it here but forecaster should have this function to avoid an error from Coordinator. """ pass def fetch_day_ahead_stats_from_prescient(self, uc_date, uc_hour, day_ahead_result): """ Fetch the day-ahead stats from prescient. - No need to have it here but forecaster should have this function to aviod an error from Coordinator. + No need to have it here but forecaster should have this function to avoid an error from Coordinator. """ pass From a4e9b49322e2f1a52c292718ef17ef250bf027d0 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Mon, 1 Jul 2024 15:03:33 -0400 Subject: [PATCH 22/26] fix typos --- idaes/apps/grid_integration/bidder.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index fc699dfd07..3740c770b0 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1408,6 +1408,8 @@ def update_real_time_model(self, **kwargs): def record_bids(self, bids: dict, model, date: str, hour: int, market): """ This function records the bids and the details in the underlying bidding model. + The detailed bids of each time step at day-ahead and real-time planning horizon will + be recorded at bidder_detail.csv Arguments: bids: dictionary, @@ -1430,17 +1432,11 @@ def record_bids(self, bids: dict, model, date: str, hour: int, market): # record bids self._record_bids(bids, date, hour, Market=market) - # record the details of bidding model - for i in model.SCENARIOS: - self.bidding_model_object.record_results( - model.fs[i], date=date, hour=hour, Scenario=i, Market=market - ) - return def _record_bids(self, bids: dict, date: str, hour: int, **kwargs): """ - Record the bis of each time perid. + Record the bis of each time period. Arguments: bids: dictionary, From 31f6e46cea1a6d918d411d03d7a7b3a0cb7ff10a Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Mon, 1 Jul 2024 17:06:34 -0400 Subject: [PATCH 23/26] minor change --- idaes/apps/grid_integration/bidder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 3740c770b0..97365ecb8c 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -1490,7 +1490,7 @@ def write_results(self, path): None """ - print("") + print("\n") print("Saving bidding results to disk...") pd.concat(self.bids_result_list).to_csv( os.path.join(path, "bidder_detail.csv"), index=False From cbb724400ef1919f21f23096cfaa856ae4921a03 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Tue, 9 Jul 2024 12:20:04 -0400 Subject: [PATCH 24/26] use logger --- idaes/apps/grid_integration/bidder.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 97365ecb8c..22ab2ac115 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -11,6 +11,7 @@ # for full copyright and license information. ################################################################################# import os +import logging from abc import ABC, abstractmethod import datetime import pandas as pd @@ -23,6 +24,9 @@ if egret_avail: from egret.model_library.transmission import tx_utils +_logger = logging.getLogger(__name__) +_logger.setLevel(logging.INFO) + class AbstractBidder(ABC): """ @@ -755,8 +759,8 @@ def write_results(self, path): None """ - print("") - print("Saving bidding results to disk...") + _logger.info("\n") + _logger.info("Saving bidding results to disk...") pd.concat(self.bids_result_list).to_csv( os.path.join(path, "bidder_detail.csv"), index=False ) @@ -1490,8 +1494,8 @@ def write_results(self, path): None """ - print("\n") - print("Saving bidding results to disk...") + _logger.info("\n") + _logger.info("Saving bidding results to disk...") pd.concat(self.bids_result_list).to_csv( os.path.join(path, "bidder_detail.csv"), index=False ) From eaf075f4f6124920fe7b9a764c735df9e22e47c1 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 8 Aug 2024 12:45:08 -0400 Subject: [PATCH 25/26] use idaeslogger --- idaes/apps/grid_integration/bidder.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/idaes/apps/grid_integration/bidder.py b/idaes/apps/grid_integration/bidder.py index 89aa973cc3..e25947a269 100644 --- a/idaes/apps/grid_integration/bidder.py +++ b/idaes/apps/grid_integration/bidder.py @@ -11,7 +11,6 @@ # for full copyright and license information. ################################################################################# import os -import logging from abc import ABC, abstractmethod import datetime import pandas as pd @@ -19,13 +18,13 @@ from pyomo.opt.base.solvers import OptSolver from pyomo.common.dependencies import attempt_import from idaes.apps.grid_integration.utils import convert_marginal_costs_to_actual_costs +import idaes.logger as idaeslog egret, egret_avail = attempt_import("egret") if egret_avail: from egret.model_library.transmission import tx_utils -_logger = logging.getLogger(__name__) -_logger.setLevel(logging.INFO) +_logger = idaeslog.getLogger(__name__) class AbstractBidder(ABC): @@ -759,8 +758,7 @@ def write_results(self, path): None """ - _logger.info("\n") - _logger.info("Saving bidding results to disk...") + _logger.info("Saving bidding results to disk.") pd.concat(self.bids_result_list).to_csv( os.path.join(path, "bidder_detail.csv"), index=False ) @@ -1494,8 +1492,7 @@ def write_results(self, path): None """ - _logger.info("\n") - _logger.info("Saving bidding results to disk...") + _logger.info("Saving bidding results to disk.") pd.concat(self.bids_result_list).to_csv( os.path.join(path, "bidder_detail.csv"), index=False ) From c22317a7f731bc3ef02fcea360b28abc6a7e9a11 Mon Sep 17 00:00:00 2001 From: Xinhe Chen Date: Thu, 8 Aug 2024 15:09:10 -0400 Subject: [PATCH 26/26] update for site-packages installation --- .../grid_integration/tests/test_PEM_Parameterized_bidder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py index e8fa10aa03..ecc1494987 100644 --- a/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py +++ b/idaes/apps/grid_integration/tests/test_PEM_Parameterized_bidder.py @@ -104,6 +104,9 @@ def bidder_object(): @pytest.mark.component +@pytest.mark.skipif( + not prescient_avail, reason="Prescient (optional dependency) not available" +) def test_compute_DA_bids(bidder_object): """ This is to test the if the compute_DA_bids function works correctly. @@ -143,6 +146,9 @@ def test_compute_DA_bids(bidder_object): @pytest.mark.component +@pytest.mark.skipif( + not prescient_avail, reason="Prescient (optional dependency) not available" +) def test_compute_RT_bids(bidder_object): """ This is to test the if the compute_DA_bids function works correctly.