From d6871a3592e352f3ae63168248d66e64d33ecf6c Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Thu, 13 Jul 2023 16:03:31 -0600 Subject: [PATCH 01/27] Adding heuristic load following dispatch method and load following example --- examples/simulate_hybrid_load_following.py | 94 +++++++++++++++++++ .../hybrid_dispatch_builder_solver.py | 17 +++- .../dispatch/hybrid_dispatch_options.py | 6 +- .../dispatch/power_storage/__init__.py | 1 + .../heuristic_load_following_dispatch.py | 58 ++++++++++++ .../simple_battery_dispatch_heuristic.py | 12 +-- hopp/tools/dispatch/plot_tools.py | 9 ++ 7 files changed, 188 insertions(+), 9 deletions(-) create mode 100644 examples/simulate_hybrid_load_following.py create mode 100644 hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py diff --git a/examples/simulate_hybrid_load_following.py b/examples/simulate_hybrid_load_following.py new file mode 100644 index 000000000..1aecbc6d5 --- /dev/null +++ b/examples/simulate_hybrid_load_following.py @@ -0,0 +1,94 @@ +from pathlib import Path +from hopp.simulation.technologies.sites import SiteInfo, flatirons_site +from hopp.simulation.hybrid_simulation import HybridSimulation +from hopp.simulation.technologies.dispatch.plot_tools import plot_battery_output, plot_battery_dispatch_error, plot_generation_profile +from hopp.utilities.keys import set_nrel_key_dot_env +import matplotlib.pyplot as plt +import numpy as np + +# Set API key +set_nrel_key_dot_env() + +examples_dir = Path(__file__).parent.absolute() + +solar_size_mw = 50 +wind_size_mw = 50 +battery_capacity_mw = 20 +interconnection_size_mw = 50 + +technologies = { + 'pv': { + 'system_capacity_kw': solar_size_mw * 1000, + }, + 'wind': { + 'num_turbines': 25, + 'turbine_rating_kw': int(wind_size_mw * 1000 / 25) + }, + 'battery': { + 'system_capacity_kwh': battery_capacity_mw * 1000, + 'system_capacity_kw': battery_capacity_mw * 4 * 1000 + }, + 'grid': { + 'interconnect_kw': interconnection_size_mw * 1000 + } +} + + + +# Get resource +lat = flatirons_site['lat'] +lon = flatirons_site['lon'] +prices_file = examples_dir.parent / "resource_files" / "grid" / "pricing-data-2015-IronMtn-002_factors.csv" + +desired_schedule = np.ones(8760) * 20 +variation = np.linspace(0, 182.5*6, 8760) +for i in range(8760): + desired_schedule[i] = desired_schedule[i] + np.sin(variation[i])*5 + +plt.plot(desired_schedule[0:100]) +plt.show() +dispatch_options = {'battery_dispatch': 'load_following_heuristic'} + +site = SiteInfo(flatirons_site, + grid_resource_file=prices_file, desired_schedule=desired_schedule) +# Create base model + +hybrid_plant = HybridSimulation(technologies, site, dispatch_options=dispatch_options) + +hybrid_plant.pv.dc_degradation = (0,) # year over year degradation +hybrid_plant.wind.wake_model = 3 # constant wake loss, layout-independent +hybrid_plant.wind.value("wake_int_loss", 1) # percent wake loss + +hybrid_plant.pv.system_capacity_kw = solar_size_mw * 1000 +hybrid_plant.wind.system_capacity_by_num_turbines(wind_size_mw * 1000) + +# prices_file are unitless dispatch factors, so add $/kwh here +hybrid_plant.ppa_price = 0.04 + +# use single year for now, multiple years with battery not implemented yet +hybrid_plant.simulate(project_life=20) + +print("output after losses over gross output", + hybrid_plant.wind.value("annual_energy") / hybrid_plant.wind.value("annual_gross_energy")) + +# Save the outputs +annual_energies = hybrid_plant.annual_energies +npvs = hybrid_plant.net_present_values +revs = hybrid_plant.total_revenues +print(annual_energies) +print(npvs) +print(revs) + + +file = 'figures/' +tag = 'simple2_' +#plot_battery_dispatch_error(hybrid_plant, plot_filename=file+tag+'battery_dispatch_error.png') +''' +for d in range(0, 360, 5): + plot_battery_output(hybrid_plant, start_day=d, plot_filename=file+tag+'day'+str(d)+'_battery_gen.png') + plot_generation_profile(hybrid_plant, start_day=d, plot_filename=file+tag+'day'+str(d)+'_system_gen.png') +''' +# plot_battery_dispatch_error(hybrid_plant) +# plot_battery_output(hybrid_plant) +plot_generation_profile(hybrid_plant) +#plot_battery_dispatch_error(hybrid_plant, plot_filename=tag+'battery_dispatch_error.png') diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py index 1c336553c..4067db7fa 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py @@ -509,7 +509,22 @@ def battery_heuristic(self): prices = self.power_sources['grid'].dispatch.electricity_sell_price self.power_sources['battery'].dispatch.prices = prices - self.power_sources['battery'].dispatch.set_fixed_dispatch(tot_gen, grid_limit) + if 'load_following' in self.options.battery_dispatch: + required_keys = ['desired_load'] + if self.site.follow_desired_schedule: + # Get difference between baseload demand and power generation and control scenario variables + load_value = self.site.desired_schedule + load_difference = [(load_value[x] - tot_gen[x]) for x in range(len(tot_gen))] + print('value units test', load_value[0], tot_gen[0], print(len(load_difference))) + self.power_sources['battery'].dispatch.load_difference = load_difference + else: + raise ValueError(type(self).__name__ + " requires the following : desired_schedule") + # Adding goal_power for the simple battery heuristic method for power setpoint tracking + goal_power = [load_value]*self.options.n_look_ahead_periods + ### Note: the inputs grid_limit and goal_power are in MW ### + self.power_sources['battery'].dispatch.set_fixed_dispatch(tot_gen, grid_limit, load_value) + else: + self.power_sources['battery'].dispatch.set_fixed_dispatch(tot_gen, grid_limit) @property def pyomo_model(self) -> pyomo.ConcreteModel: diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py index b6a5fb4e8..e1453986f 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py @@ -5,7 +5,8 @@ SimpleBatteryDispatchHeuristic, SimpleBatteryDispatch, NonConvexLinearVoltageBatteryDispatch, - ConvexLinearVoltageBatteryDispatch + ConvexLinearVoltageBatteryDispatch, + HeuristicLoadFollowingDispatch, ) @@ -103,7 +104,8 @@ def __init__(self, dispatch_options: dict = None): 'heuristic': SimpleBatteryDispatchHeuristic, 'simple': SimpleBatteryDispatch, 'non_convex_LV': NonConvexLinearVoltageBatteryDispatch, - 'convex_LV': ConvexLinearVoltageBatteryDispatch} + 'convex_LV': ConvexLinearVoltageBatteryDispatch, + 'load_following_heuristic': HeuristicLoadFollowingDispatch} if self.battery_dispatch in self._battery_dispatch_model_options: self.battery_dispatch_class = self._battery_dispatch_model_options[self.battery_dispatch] if 'heuristic' in self.battery_dispatch: diff --git a/hopp/simulation/technologies/dispatch/power_storage/__init__.py b/hopp/simulation/technologies/dispatch/power_storage/__init__.py index 15951a51d..73592b774 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/__init__.py +++ b/hopp/simulation/technologies/dispatch/power_storage/__init__.py @@ -4,3 +4,4 @@ from hopp.simulation.technologies.dispatch.power_storage.power_storage_dispatch import PowerStorageDispatch from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import SimpleBatteryDispatch from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import SimpleBatteryDispatchHeuristic +from hopp.simulation.technologies.dispatch.power_storage.heuristic_load_following_dispatch import HeuristicLoadFollowingDispatch diff --git a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py new file mode 100644 index 000000000..9672f8a1e --- /dev/null +++ b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py @@ -0,0 +1,58 @@ +import pyomo.environ as pyomo +from pyomo.environ import units as u + +import PySAM.BatteryStateful as BatteryModel +import PySAM.Singleowner as Singleowner + +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import SimpleBatteryDispatchHeuristic + + +class HeuristicLoadFollowingDispatch(SimpleBatteryDispatchHeuristic): + """Fixes battery dispatch operations based power available from power generation profiles and + power demand profile. + + Currently, enforces available generation and grid limit assuming no battery charging from grid + """ + def __init__(self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: Singleowner.Singleowner, + fixed_dispatch: list = None, + block_set_name: str = 'heuristic_load_following_battery', + include_lifecycle_count: bool = False): + """ + + :param fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) + """ + super().__init__(pyomo_model, + index_set, + system_model, + financial_model, + fixed_dispatch, + block_set_name=block_set_name, + include_lifecycle_count=False) + + def set_fixed_dispatch(self, gen: list, grid_limit: list, goal_power: list): + """Sets charge and discharge power of battery dispatch using fixed_dispatch attribute and enforces available + generation and grid limits. + + """ + self.check_gen_grid_limit(gen, grid_limit) + self._set_power_fraction_limits(gen, grid_limit) + self._heuristic_method(gen, goal_power) + self._fix_dispatch_model_variables() + + def _heuristic_method(self, gen, goal_power): + """ Enforces battery power fraction limits and sets _fixed_dispatch attribute + Sets the _fixed_dispatch based on goal_power and gen (power genration profile) + """ + for t in self.blocks.index_set(): + fd = (goal_power[t] - gen[t]) / self.maximum_power + if fd > 0.0: # Discharging + if fd > self.max_discharge_fraction[t]: + fd = self.max_discharge_fraction[t] + elif fd < 0.0: # Charging + if - fd > self.max_charge_fraction[t]: + fd = - self.max_charge_fraction[t] + self._fixed_dispatch[t] = fd \ No newline at end of file diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index 09077aac8..e69d7f638 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -73,17 +73,17 @@ def _set_power_fraction_limits(self, gen: list, grid_limit: list): @staticmethod def enforce_power_fraction_simple_bounds(power_fraction) -> float: """ Enforces simple bounds (0,1) for battery power fractions.""" - if power_fraction > 1.0: - power_fraction = 1.0 - elif power_fraction < 0.0: - power_fraction = 0.0 + if power_fraction > 0.9: + power_fraction = 0.9 + elif power_fraction < 0: + power_fraction = 0 return power_fraction def update_soc(self, power_fraction, soc0) -> float: - if power_fraction > 0.0: + if power_fraction > 0.1: discharge_power = power_fraction * self.maximum_power soc = soc0 - self.time_duration[0] * (1/(self.discharge_efficiency/100.) * discharge_power) / self.capacity - elif power_fraction < 0.0: + elif power_fraction < 0.9: charge_power = - power_fraction * self.maximum_power soc = soc0 + self.time_duration[0] * (self.charge_efficiency / 100. * charge_power) / self.capacity else: diff --git a/hopp/tools/dispatch/plot_tools.py b/hopp/tools/dispatch/plot_tools.py index 7581eee8e..cff1c3fe3 100644 --- a/hopp/tools/dispatch/plot_tools.py +++ b/hopp/tools/dispatch/plot_tools.py @@ -326,6 +326,12 @@ def plot_generation_profile(hybrid: HybridSimulation, ax1.legend(fontsize=font_size-2, loc='upper left') ax1.set_ylabel('Power (MW)', fontsize=font_size) + if hybrid.site.follow_desired_schedule: + desired_load = [p for p in hybrid.site.desired_schedule[time_slice]] + ax1.plot(time, desired_load, 'b--', label='Price') + ax1.set_ylabel('Desired Load', fontsize=font_size) + ax1.legend(fontsize=font_size-2, loc='upper right') + ax2 = ax1.twinx() price = [p * hybrid.ppa_price[0] for p in hybrid.site.elec_prices.data[time_slice]] @@ -335,6 +341,9 @@ def plot_generation_profile(hybrid: HybridSimulation, plt.xlabel('Time (hours)', fontsize=font_size) plt.title('Net Generation', fontsize=font_size) + + plt.xlabel('Time (hours)', fontsize=font_size) + plt.title('Net Generation', fontsize=font_size) plt.tight_layout() if plot_filename is not None: From 32bdb7df73c2c3cb7502e0c3cab2a2776e0a5774 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 14 Jul 2023 11:45:55 -0600 Subject: [PATCH 02/27] Updating heuristic battery dispatch description --- .../dispatch/power_storage/heuristic_load_following_dispatch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py index 9672f8a1e..de2a46bcc 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py @@ -8,7 +8,7 @@ class HeuristicLoadFollowingDispatch(SimpleBatteryDispatchHeuristic): - """Fixes battery dispatch operations based power available from power generation profiles and + """Operates the battery based on heuristic rules to meet the demand profile based power available from power generation profiles and power demand profile. Currently, enforces available generation and grid limit assuming no battery charging from grid From c1045e2cd8e99673abf3a33be9a9d2efb94a3a9d Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Fri, 14 Jul 2023 14:49:41 -0600 Subject: [PATCH 03/27] Cleaning up the code --- examples/simulate_hybrid_load_following.py | 3 +-- .../technologies/dispatch/hybrid_dispatch_builder_solver.py | 1 - .../power_storage/simple_battery_dispatch_heuristic.py | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/examples/simulate_hybrid_load_following.py b/examples/simulate_hybrid_load_following.py index 1aecbc6d5..c066dc216 100644 --- a/examples/simulate_hybrid_load_following.py +++ b/examples/simulate_hybrid_load_following.py @@ -45,8 +45,7 @@ for i in range(8760): desired_schedule[i] = desired_schedule[i] + np.sin(variation[i])*5 -plt.plot(desired_schedule[0:100]) -plt.show() + dispatch_options = {'battery_dispatch': 'load_following_heuristic'} site = SiteInfo(flatirons_site, diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py index 4067db7fa..457141ff8 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py @@ -515,7 +515,6 @@ def battery_heuristic(self): # Get difference between baseload demand and power generation and control scenario variables load_value = self.site.desired_schedule load_difference = [(load_value[x] - tot_gen[x]) for x in range(len(tot_gen))] - print('value units test', load_value[0], tot_gen[0], print(len(load_difference))) self.power_sources['battery'].dispatch.load_difference = load_difference else: raise ValueError(type(self).__name__ + " requires the following : desired_schedule") diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index e69d7f638..52dc89149 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -88,7 +88,7 @@ def update_soc(self, power_fraction, soc0) -> float: soc = soc0 + self.time_duration[0] * (self.charge_efficiency / 100. * charge_power) / self.capacity else: soc = soc0 - soc = max(0, min(1, soc)) + soc = max(0.1, min(0.9, soc)) return soc def _heuristic_method(self, _): From 284d17702a73ed7e90d2f710cae86e9c6a39ddf0 Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Thu, 21 Dec 2023 10:12:56 -0700 Subject: [PATCH 04/27] Adding updated analysis code and test --- examples/simulate_hybrid_load_following.py | 16 +++-- hopp/simulation/hybrid_simulation.py | 3 +- .../dispatch/hybrid_dispatch_options.py | 7 ++ hopp/simulation/technologies/grid.py | 68 ++++++++++++++++++- tests/hopp/test_dispatch.py | 28 ++++++++ 5 files changed, 113 insertions(+), 9 deletions(-) diff --git a/examples/simulate_hybrid_load_following.py b/examples/simulate_hybrid_load_following.py index c066dc216..978c6d9bc 100644 --- a/examples/simulate_hybrid_load_following.py +++ b/examples/simulate_hybrid_load_following.py @@ -13,7 +13,7 @@ solar_size_mw = 50 wind_size_mw = 50 -battery_capacity_mw = 20 +battery_capacity_mw = 50 interconnection_size_mw = 50 technologies = { @@ -25,8 +25,8 @@ 'turbine_rating_kw': int(wind_size_mw * 1000 / 25) }, 'battery': { - 'system_capacity_kwh': battery_capacity_mw * 1000, - 'system_capacity_kw': battery_capacity_mw * 4 * 1000 + 'system_capacity_kwh': battery_capacity_mw * 4* 1000, + 'system_capacity_kw': battery_capacity_mw * 1000 }, 'grid': { 'interconnect_kw': interconnection_size_mw * 1000 @@ -45,8 +45,14 @@ for i in range(8760): desired_schedule[i] = desired_schedule[i] + np.sin(variation[i])*5 +desired_schedule = 8760*[20] +# variation = np.linspace(0, 182.5*6, 8760) +# for i in range(8760): +# desired_schedule[i] = desired_schedule[i] + np.sin(variation[i])*5 -dispatch_options = {'battery_dispatch': 'load_following_heuristic'} +dispatch_options = {'battery_dispatch': 'load_following_heuristic', + 'use_higher_hours': True, + 'higher_hours': {'min_regulation_hours': 4, 'min_regulation_power': 5000}} site = SiteInfo(flatirons_site, grid_resource_file=prices_file, desired_schedule=desired_schedule) @@ -65,7 +71,7 @@ hybrid_plant.ppa_price = 0.04 # use single year for now, multiple years with battery not implemented yet -hybrid_plant.simulate(project_life=20) +hybrid_plant.simulate(project_life=1) print("output after losses over gross output", hybrid_plant.wind.value("annual_energy") / hybrid_plant.wind.value("annual_gross_energy")) diff --git a/hopp/simulation/hybrid_simulation.py b/hopp/simulation/hybrid_simulation.py index dcad3619e..41dcfb08e 100644 --- a/hopp/simulation/hybrid_simulation.py +++ b/hopp/simulation/hybrid_simulation.py @@ -660,7 +660,8 @@ def simulate_power(self, project_life: int = 25, lifetime_sim=False): # Consolidate grid generation by copying over power and storage generation information if self.battery: self.grid.generation_profile_wo_battery = total_gen_before_battery - self.grid.simulate_grid_connection(hybrid_size_kw, total_gen, project_life, lifetime_sim, total_gen_max_feasible_year1) + self.grid.simulate_grid_connection(hybrid_size_kw, total_gen, project_life, lifetime_sim,\ + total_gen_max_feasible_year1,dispatch_options=self.dispatch_builder.options) self.grid.hybrid_nominal_capacity = hybrid_nominal_capacity self.grid.total_gen_max_feasible_year1 = total_gen_max_feasible_year1 logger.info(f"Hybrid Peformance Simulation Complete. AEPs are {self.annual_energies}.") diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py index e1453986f..69c26f1da 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py @@ -53,6 +53,10 @@ class HybridDispatchOptions: - **clustering_divisions** (dict, default={}): Custom number of averaging periods for classification metrics for data clustering. If empty, default values will be used. + - **use_higher_hours** bool (default = False): if True, the simulation will run extra hours analysis (must be used with load following) + + - **higher_hours** (dict, default = {}): Higher hour count parameters: the value of power that must be available above the schedule and the number of hours in a row + """ def __init__(self, dispatch_options: dict = None): self.solver: str = 'cbc' @@ -75,6 +79,9 @@ def __init__(self, dispatch_options: dict = None): self.clustering_weights: dict = {} self.clustering_divisions: dict = {} + self.use_higher_hours: bool = False + self.higher_hours: dict = {} + if dispatch_options is not None: for key, value in dispatch_options.items(): if hasattr(self, key): diff --git a/hopp/simulation/technologies/grid.py b/hopp/simulation/technologies/grid.py index 701052115..1af1ef4ec 100644 --- a/hopp/simulation/technologies/grid.py +++ b/hopp/simulation/technologies/grid.py @@ -91,7 +91,8 @@ def simulate_grid_connection( total_gen: Union[List[float], NDArrayFloat], project_life: int, lifetime_sim: bool, - total_gen_max_feasible_year1: Union[List[float], NDArrayFloat] + total_gen_max_feasible_year1: Union[List[float], NDArrayFloat], + dispatch_options: Optional[dict] = None ): """ Sets up and simulates hybrid system grid connection. Additionally, @@ -108,6 +109,8 @@ def simulate_grid_connection( data is repeated total_gen_max_feasible_year1: Maximum generation profile of the hybrid system (for capacity payments) [kWh] + dispatch_options: Hybrid dispatch options class, deliminates if the higher + power analysis for frequency regulation is run """ if self.site.follow_desired_schedule: @@ -125,10 +128,69 @@ def simulate_grid_connection( self.schedule_curtailed = np.array([gen - schedule if gen > schedule else 0. for (gen, schedule) in zip(total_gen, lifetime_schedule)]) self.schedule_curtailed_percentage = sum(self.schedule_curtailed)/sum(lifetime_schedule) + + # NOTE: This is currently only happening for load following, would be good to make it more general + # i.e. so that this analysis can be used when load following isn't being used (without storage) + # for comparison + N_hybrid = len(self.generation_profile) + + final_power_production = total_gen + schedule = [x for x in lifetime_schedule] + print(len(final_power_production), len(schedule)) + hybrid_power = [(final_power_production[x] - (schedule[x]*0.95)) for x in range(len(final_power_production))] + + load_met = len([i for i in hybrid_power if i >= 0]) + self.time_load_met = 100 * load_met/N_hybrid + + final_power_array = np.array(final_power_production) + power_met = np.where(final_power_array > schedule, schedule, final_power_array) + self.capacity_factor_load = np.sum(power_met) / np.sum(schedule) * 100 + + print('Percent of time firm power requirement is met: ', np.round(self.time_load_met,2)) + print('Percent total firm power requirement is satisfied: ', np.round(self.capacity_factor_load,2)) + + dispatch_options = dispatch_options or {} + + ERS_keys = ['min_regulation_hours', 'min_regulation_power'] + if "use_higher_hours" in dispatch_options: + """ + Frequency regulation analysis for providing essential reliability services (ERS) availability operating case: + Finds how many hours (in the group specified group size above the specified minimum + power requirement) that the system has available to extra power that could be used to + provide ERS + Args: + :param dispatch_options: need additional ERS arguments + 'min_regulation_hours': minimum size of hours in a group to be considered for ERS (>= 1) + 'min_regulation_power': minimum power available over the whole group of hours to be + considered for ERS (> 0, in kW) + + :returns: total_number_hours + + """ + + # Performing frequency regulation analysis: + # finding how many groups of hours satisfiy the ERS minimum power requirement + min_regulation_hours = dispatch_options["higher_hours"]['min_regulation_hours'] + min_regulation_power = dispatch_options["higher_hours"]['min_regulation_power'] + + frequency_power_array = np.array(hybrid_power) + frequency_test = np.where(frequency_power_array > min_regulation_power, frequency_power_array, 0) + mask = (frequency_test!=0).astype(int) + padded_mask = np.pad(mask,(1,), "constant") + edge_mask = padded_mask[1:] - padded_mask[:-1] # finding the difference between each array value + + group_starts = np.where(edge_mask == 1)[0] + group_stops = np.where(edge_mask == -1)[0] + + # Find groups and drop groups that are too small + groups = [group for group in zip(group_starts,group_stops) if ((group[1]-group[0]) >= min_regulation_hours)] + group_lengths = [len(final_power_production[group[0]:group[1]]) for group in groups] + self.total_number_hours = sum(group_lengths) + + print('Total number of hours available for ERS: ', np.round(self.total_number_hours,2)) else: - self.generation_profile = list(total_gen) + self.generation_profile = total_gen - self.total_gen_max_feasible_year1 = np.array(total_gen_max_feasible_year1) self.system_capacity_kw = hybrid_size_kw # TODO: Should this be interconnection limit? self.gen_max_feasible = list(np.minimum( # TODO: remove list() cast once parent class uses numpy total_gen_max_feasible_year1, diff --git a/tests/hopp/test_dispatch.py b/tests/hopp/test_dispatch.py index 026179b71..b7fe04bfc 100644 --- a/tests/hopp/test_dispatch.py +++ b/tests/hopp/test_dispatch.py @@ -764,6 +764,34 @@ def test_hybrid_dispatch_heuristic(site): assert sum(hybrid_plant.battery.dispatch.discharge_power) > 0.0 +def test_hybrid_dispatch_baseload_heuristic_and_analysis(site): + + desired_schedule = 8760*[20] + + desired_schedule_site = SiteInfo(flatirons_site, + desired_schedule=desired_schedule) + wind_solar_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery')} + + dispatch_options = {'battery_dispatch': 'load_following_heuristic', + 'use_higher_hours': True, + 'higher_hours': {'min_regulation_hours': 4, 'min_regulation_power': 5000}} + + print(wind_solar_battery) + print(site) + print(interconnect_mw) + print(dispatch_options) + hybrid_plant = HybridSimulation(wind_solar_battery, desired_schedule_site, interconnect_mw * 1000, + dispatch_options=dispatch_options) + + + hybrid_plant.simulate(1) + + assert hybrid_plant.grid.time_load_met == pytest.approx(93.9, 1e-2) + assert hybrid_plant.grid.capacity_factor_load == pytest.approx(95.75, 1e-2) + assert hybrid_plant.grid.total_number_hours == pytest.approx(4277, 1e-2) + + + def test_hybrid_dispatch_one_cycle_heuristic(site): dispatch_options = {'battery_dispatch': 'one_cycle_heuristic', 'grid_charging': False} From 46fc75d714cb66c8cf96b6d5cbe69d4c055f60aa Mon Sep 17 00:00:00 2001 From: Genevieve Starke Date: Thu, 21 Dec 2023 10:16:08 -0700 Subject: [PATCH 05/27] Adding todo for battery config file --- .../technologies/dispatch/hybrid_dispatch_builder_solver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py index 457141ff8..cbc5d694f 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py @@ -510,6 +510,7 @@ def battery_heuristic(self): self.power_sources['battery'].dispatch.prices = prices if 'load_following' in self.options.battery_dispatch: + # TODO: Look into how to define a system as load following or not in the config file required_keys = ['desired_load'] if self.site.follow_desired_schedule: # Get difference between baseload demand and power generation and control scenario variables From 65be686dd3427ac471869e79f0b8e6c6661152ae Mon Sep 17 00:00:00 2001 From: Cameron Irmas Date: Wed, 27 Dec 2023 14:07:09 -0800 Subject: [PATCH 06/27] Fix tests --- hopp/simulation/hybrid_simulation.py | 10 ++- .../heuristic_load_following_dispatch.py | 26 ++++---- hopp/simulation/technologies/grid.py | 15 +++-- tests/hopp/test_dispatch.py | 66 +++++++++++-------- 4 files changed, 68 insertions(+), 49 deletions(-) diff --git a/hopp/simulation/hybrid_simulation.py b/hopp/simulation/hybrid_simulation.py index 41dcfb08e..457a9c76e 100644 --- a/hopp/simulation/hybrid_simulation.py +++ b/hopp/simulation/hybrid_simulation.py @@ -660,8 +660,14 @@ def simulate_power(self, project_life: int = 25, lifetime_sim=False): # Consolidate grid generation by copying over power and storage generation information if self.battery: self.grid.generation_profile_wo_battery = total_gen_before_battery - self.grid.simulate_grid_connection(hybrid_size_kw, total_gen, project_life, lifetime_sim,\ - total_gen_max_feasible_year1,dispatch_options=self.dispatch_builder.options) + self.grid.simulate_grid_connection( + hybrid_size_kw, + total_gen, + project_life, + lifetime_sim, + total_gen_max_feasible_year1, + self.dispatch_builder.options + ) self.grid.hybrid_nominal_capacity = hybrid_nominal_capacity self.grid.total_gen_max_feasible_year1 = total_gen_max_feasible_year1 logger.info(f"Hybrid Peformance Simulation Complete. AEPs are {self.annual_energies}.") diff --git a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py index de2a46bcc..ad987cea6 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py @@ -1,6 +1,7 @@ +from typing import Optional, List + import pyomo.environ as pyomo from pyomo.environ import units as u - import PySAM.BatteryStateful as BatteryModel import PySAM.Singleowner as Singleowner @@ -18,20 +19,23 @@ def __init__(self, index_set: pyomo.Set, system_model: BatteryModel.BatteryStateful, financial_model: Singleowner.Singleowner, - fixed_dispatch: list = None, + fixed_dispatch: Optional[List] = None, block_set_name: str = 'heuristic_load_following_battery', - include_lifecycle_count: bool = False): + dispatch_options: Optional[dict] = None): """ - :param fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) + Args: + fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) """ - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - fixed_dispatch, - block_set_name=block_set_name, - include_lifecycle_count=False) + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + fixed_dispatch, + block_set_name, + dispatch_options + ) def set_fixed_dispatch(self, gen: list, grid_limit: list, goal_power: list): """Sets charge and discharge power of battery dispatch using fixed_dispatch attribute and enforces available diff --git a/hopp/simulation/technologies/grid.py b/hopp/simulation/technologies/grid.py index 1af1ef4ec..9dc492f6a 100644 --- a/hopp/simulation/technologies/grid.py +++ b/hopp/simulation/technologies/grid.py @@ -1,4 +1,4 @@ -from typing import Iterable, List, Sequence, Optional, Union +from typing import Iterable, List, Sequence, Optional, Union, TYPE_CHECKING import numpy as np from attrs import define, field @@ -12,6 +12,8 @@ from hopp.type_dec import NDArrayFloat from hopp.utilities.validators import gt_zero +if TYPE_CHECKING: + from hopp.simulation.technologies.dispatch.hybrid_dispatch_options import HybridDispatchOptions @define class GridConfig(BaseClass): @@ -92,7 +94,7 @@ def simulate_grid_connection( project_life: int, lifetime_sim: bool, total_gen_max_feasible_year1: Union[List[float], NDArrayFloat], - dispatch_options: Optional[dict] = None + dispatch_options: Optional["HybridDispatchOptions"] = None ): """ Sets up and simulates hybrid system grid connection. Additionally, @@ -149,10 +151,8 @@ def simulate_grid_connection( print('Percent of time firm power requirement is met: ', np.round(self.time_load_met,2)) print('Percent total firm power requirement is satisfied: ', np.round(self.capacity_factor_load,2)) - dispatch_options = dispatch_options or {} - ERS_keys = ['min_regulation_hours', 'min_regulation_power'] - if "use_higher_hours" in dispatch_options: + if dispatch_options is not None and dispatch_options.use_higher_hours: """ Frequency regulation analysis for providing essential reliability services (ERS) availability operating case: Finds how many hours (in the group specified group size above the specified minimum @@ -170,8 +170,8 @@ def simulate_grid_connection( # Performing frequency regulation analysis: # finding how many groups of hours satisfiy the ERS minimum power requirement - min_regulation_hours = dispatch_options["higher_hours"]['min_regulation_hours'] - min_regulation_power = dispatch_options["higher_hours"]['min_regulation_power'] + min_regulation_hours = dispatch_options.higher_hours['min_regulation_hours'] + min_regulation_power = dispatch_options.higher_hours['min_regulation_power'] frequency_power_array = np.array(hybrid_power) frequency_test = np.where(frequency_power_array > min_regulation_power, frequency_power_array, 0) @@ -191,6 +191,7 @@ def simulate_grid_connection( else: self.generation_profile = total_gen + self.total_gen_max_feasible_year1 = np.array(total_gen_max_feasible_year1) self.system_capacity_kw = hybrid_size_kw # TODO: Should this be interconnection limit? self.gen_max_feasible = list(np.minimum( # TODO: remove list() cast once parent class uses numpy total_gen_max_feasible_year1, diff --git a/tests/hopp/test_dispatch.py b/tests/hopp/test_dispatch.py index b7fe04bfc..2f9af9f0d 100644 --- a/tests/hopp/test_dispatch.py +++ b/tests/hopp/test_dispatch.py @@ -6,7 +6,7 @@ from pyomo.util.check_units import assert_units_consistent from hopp.simulation import HoppInterface -from hopp.simulation.technologies.sites import SiteInfo +from hopp.simulation.technologies.sites import SiteInfo, flatirons_site from hopp.simulation.technologies.financial.custom_financial_model import CustomFinancialModel from hopp.simulation.technologies.wind.wind_plant import WindPlant, WindConfig from hopp.simulation.technologies.pv.pv_plant import PVPlant, PVConfig @@ -764,34 +764,6 @@ def test_hybrid_dispatch_heuristic(site): assert sum(hybrid_plant.battery.dispatch.discharge_power) > 0.0 -def test_hybrid_dispatch_baseload_heuristic_and_analysis(site): - - desired_schedule = 8760*[20] - - desired_schedule_site = SiteInfo(flatirons_site, - desired_schedule=desired_schedule) - wind_solar_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery')} - - dispatch_options = {'battery_dispatch': 'load_following_heuristic', - 'use_higher_hours': True, - 'higher_hours': {'min_regulation_hours': 4, 'min_regulation_power': 5000}} - - print(wind_solar_battery) - print(site) - print(interconnect_mw) - print(dispatch_options) - hybrid_plant = HybridSimulation(wind_solar_battery, desired_schedule_site, interconnect_mw * 1000, - dispatch_options=dispatch_options) - - - hybrid_plant.simulate(1) - - assert hybrid_plant.grid.time_load_met == pytest.approx(93.9, 1e-2) - assert hybrid_plant.grid.capacity_factor_load == pytest.approx(95.75, 1e-2) - assert hybrid_plant.grid.total_number_hours == pytest.approx(4277, 1e-2) - - - def test_hybrid_dispatch_one_cycle_heuristic(site): dispatch_options = {'battery_dispatch': 'one_cycle_heuristic', 'grid_charging': False} @@ -1020,3 +992,39 @@ def create_test_objective_rule(m): assert sum(battery.dispatch.discharge_power) > 0.0 assert (sum(battery.dispatch.charge_power) * battery.dispatch.round_trip_efficiency / 100.0 == pytest.approx(sum(battery.dispatch.discharge_power))) + + +def test_hybrid_dispatch_baseload_heuristic_and_analysis(site): + + desired_schedule = 8760*[20] + + desired_schedule_site = SiteInfo(flatirons_site, + desired_schedule=desired_schedule) + wind_solar_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery')} + + dispatch_options = {'battery_dispatch': 'load_following_heuristic', + 'use_higher_hours': True, + 'higher_hours': {'min_regulation_hours': 4, 'min_regulation_power': 5000}} + + print(wind_solar_battery) + print(site) + print(interconnect_mw) + print(dispatch_options) + hopp_config = { + "site": desired_schedule_site, + "technologies": wind_solar_battery, + "config": { + "dispatch_options": dispatch_options + } + } + hopp_config["technologies"]["grid"] = { + "interconnect_kw": interconnect_mw * 1000 + } + hi = HoppInterface(hopp_config) + hi.simulate(1) + + hybrid_plant = hi.system + + assert hybrid_plant.grid.time_load_met == pytest.approx(91.9, 1e-2) + assert hybrid_plant.grid.capacity_factor_load == pytest.approx(94.45, 1e-2) + assert hybrid_plant.grid.total_number_hours == pytest.approx(3732, 1e-2) \ No newline at end of file From d7e8b4d9487292a3b4dddacac8cbfc97f43d41b1 Mon Sep 17 00:00:00 2001 From: Cameron Irmas Date: Wed, 27 Dec 2023 14:11:15 -0800 Subject: [PATCH 07/27] Temporarily remove example We'll bring this back in a followup issue. --- examples/simulate_hybrid_load_following.py | 99 ---------------------- 1 file changed, 99 deletions(-) delete mode 100644 examples/simulate_hybrid_load_following.py diff --git a/examples/simulate_hybrid_load_following.py b/examples/simulate_hybrid_load_following.py deleted file mode 100644 index 978c6d9bc..000000000 --- a/examples/simulate_hybrid_load_following.py +++ /dev/null @@ -1,99 +0,0 @@ -from pathlib import Path -from hopp.simulation.technologies.sites import SiteInfo, flatirons_site -from hopp.simulation.hybrid_simulation import HybridSimulation -from hopp.simulation.technologies.dispatch.plot_tools import plot_battery_output, plot_battery_dispatch_error, plot_generation_profile -from hopp.utilities.keys import set_nrel_key_dot_env -import matplotlib.pyplot as plt -import numpy as np - -# Set API key -set_nrel_key_dot_env() - -examples_dir = Path(__file__).parent.absolute() - -solar_size_mw = 50 -wind_size_mw = 50 -battery_capacity_mw = 50 -interconnection_size_mw = 50 - -technologies = { - 'pv': { - 'system_capacity_kw': solar_size_mw * 1000, - }, - 'wind': { - 'num_turbines': 25, - 'turbine_rating_kw': int(wind_size_mw * 1000 / 25) - }, - 'battery': { - 'system_capacity_kwh': battery_capacity_mw * 4* 1000, - 'system_capacity_kw': battery_capacity_mw * 1000 - }, - 'grid': { - 'interconnect_kw': interconnection_size_mw * 1000 - } -} - - - -# Get resource -lat = flatirons_site['lat'] -lon = flatirons_site['lon'] -prices_file = examples_dir.parent / "resource_files" / "grid" / "pricing-data-2015-IronMtn-002_factors.csv" - -desired_schedule = np.ones(8760) * 20 -variation = np.linspace(0, 182.5*6, 8760) -for i in range(8760): - desired_schedule[i] = desired_schedule[i] + np.sin(variation[i])*5 - -desired_schedule = 8760*[20] -# variation = np.linspace(0, 182.5*6, 8760) -# for i in range(8760): -# desired_schedule[i] = desired_schedule[i] + np.sin(variation[i])*5 - -dispatch_options = {'battery_dispatch': 'load_following_heuristic', - 'use_higher_hours': True, - 'higher_hours': {'min_regulation_hours': 4, 'min_regulation_power': 5000}} - -site = SiteInfo(flatirons_site, - grid_resource_file=prices_file, desired_schedule=desired_schedule) -# Create base model - -hybrid_plant = HybridSimulation(technologies, site, dispatch_options=dispatch_options) - -hybrid_plant.pv.dc_degradation = (0,) # year over year degradation -hybrid_plant.wind.wake_model = 3 # constant wake loss, layout-independent -hybrid_plant.wind.value("wake_int_loss", 1) # percent wake loss - -hybrid_plant.pv.system_capacity_kw = solar_size_mw * 1000 -hybrid_plant.wind.system_capacity_by_num_turbines(wind_size_mw * 1000) - -# prices_file are unitless dispatch factors, so add $/kwh here -hybrid_plant.ppa_price = 0.04 - -# use single year for now, multiple years with battery not implemented yet -hybrid_plant.simulate(project_life=1) - -print("output after losses over gross output", - hybrid_plant.wind.value("annual_energy") / hybrid_plant.wind.value("annual_gross_energy")) - -# Save the outputs -annual_energies = hybrid_plant.annual_energies -npvs = hybrid_plant.net_present_values -revs = hybrid_plant.total_revenues -print(annual_energies) -print(npvs) -print(revs) - - -file = 'figures/' -tag = 'simple2_' -#plot_battery_dispatch_error(hybrid_plant, plot_filename=file+tag+'battery_dispatch_error.png') -''' -for d in range(0, 360, 5): - plot_battery_output(hybrid_plant, start_day=d, plot_filename=file+tag+'day'+str(d)+'_battery_gen.png') - plot_generation_profile(hybrid_plant, start_day=d, plot_filename=file+tag+'day'+str(d)+'_system_gen.png') -''' -# plot_battery_dispatch_error(hybrid_plant) -# plot_battery_output(hybrid_plant) -plot_generation_profile(hybrid_plant) -#plot_battery_dispatch_error(hybrid_plant, plot_filename=tag+'battery_dispatch_error.png') From 739c72b8949afe6e15ee8d4c89411033147784fe Mon Sep 17 00:00:00 2001 From: Cameron Irmas Date: Thu, 28 Dec 2023 12:26:05 -0800 Subject: [PATCH 08/27] Replace print statements with logs --- hopp/simulation/technologies/grid.py | 8 ++++---- tests/hopp/test_dispatch.py | 4 ---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/hopp/simulation/technologies/grid.py b/hopp/simulation/technologies/grid.py index 9dc492f6a..a080061ab 100644 --- a/hopp/simulation/technologies/grid.py +++ b/hopp/simulation/technologies/grid.py @@ -11,6 +11,7 @@ from hopp.simulation.technologies.financial import FinancialModelType, CustomFinancialModel from hopp.type_dec import NDArrayFloat from hopp.utilities.validators import gt_zero +from hopp.utilities.log import hybrid_logger as logger if TYPE_CHECKING: from hopp.simulation.technologies.dispatch.hybrid_dispatch_options import HybridDispatchOptions @@ -138,7 +139,6 @@ def simulate_grid_connection( final_power_production = total_gen schedule = [x for x in lifetime_schedule] - print(len(final_power_production), len(schedule)) hybrid_power = [(final_power_production[x] - (schedule[x]*0.95)) for x in range(len(final_power_production))] load_met = len([i for i in hybrid_power if i >= 0]) @@ -148,8 +148,8 @@ def simulate_grid_connection( power_met = np.where(final_power_array > schedule, schedule, final_power_array) self.capacity_factor_load = np.sum(power_met) / np.sum(schedule) * 100 - print('Percent of time firm power requirement is met: ', np.round(self.time_load_met,2)) - print('Percent total firm power requirement is satisfied: ', np.round(self.capacity_factor_load,2)) + logger.info('Percent of time firm power requirement is met: ', np.round(self.time_load_met,2)) + logger.info('Percent total firm power requirement is satisfied: ', np.round(self.capacity_factor_load,2)) ERS_keys = ['min_regulation_hours', 'min_regulation_power'] if dispatch_options is not None and dispatch_options.use_higher_hours: @@ -187,7 +187,7 @@ def simulate_grid_connection( group_lengths = [len(final_power_production[group[0]:group[1]]) for group in groups] self.total_number_hours = sum(group_lengths) - print('Total number of hours available for ERS: ', np.round(self.total_number_hours,2)) + logger.info('Total number of hours available for ERS: ', np.round(self.total_number_hours,2)) else: self.generation_profile = total_gen diff --git a/tests/hopp/test_dispatch.py b/tests/hopp/test_dispatch.py index 2f9af9f0d..b42c0c413 100644 --- a/tests/hopp/test_dispatch.py +++ b/tests/hopp/test_dispatch.py @@ -1006,10 +1006,6 @@ def test_hybrid_dispatch_baseload_heuristic_and_analysis(site): 'use_higher_hours': True, 'higher_hours': {'min_regulation_hours': 4, 'min_regulation_power': 5000}} - print(wind_solar_battery) - print(site) - print(interconnect_mw) - print(dispatch_options) hopp_config = { "site": desired_schedule_site, "technologies": wind_solar_battery, From 3f7254b2e1f0f65c6f79fc274ec643136ae8175e Mon Sep 17 00:00:00 2001 From: Cameron Irmas Date: Thu, 28 Dec 2023 13:22:49 -0800 Subject: [PATCH 09/27] Add docs, small fixes --- .../heuristic_load_following_dispatch.py | 4 +- .../simple_battery_dispatch_heuristic.py | 44 +++++++++++++++---- hopp/tools/dispatch/plot_tools.py | 12 ++--- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py index ad987cea6..2d6ae049a 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py @@ -57,6 +57,6 @@ def _heuristic_method(self, gen, goal_power): if fd > self.max_discharge_fraction[t]: fd = self.max_discharge_fraction[t] elif fd < 0.0: # Charging - if - fd > self.max_charge_fraction[t]: - fd = - self.max_charge_fraction[t] + if -fd > self.max_charge_fraction[t]: + fd = -self.max_charge_fraction[t] self._fixed_dispatch[t] = fd \ No newline at end of file diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index 52dc89149..c1d03f3cc 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -60,8 +60,13 @@ def check_gen_grid_limit(self, gen: list, grid_limit: list): raise ValueError("grid_limit must be the same length as fixed_dispatch.") def _set_power_fraction_limits(self, gen: list, grid_limit: list): - """Set battery charge and discharge power fraction limits based on available generation and grid capacity, - respectively. + """ + Set battery charge and discharge power fraction limits based on + available generation and grid capacity, respectively. + + Args: + gen: generation Blocks + grid_limit: grid capacity NOTE: This method assumes that battery cannot be charged by the grid. """ @@ -71,28 +76,49 @@ def _set_power_fraction_limits(self, gen: list, grid_limit: list): / self.maximum_power) @staticmethod - def enforce_power_fraction_simple_bounds(power_fraction) -> float: - """ Enforces simple bounds (0,1) for battery power fractions.""" + def enforce_power_fraction_simple_bounds(power_fraction: float) -> float: + """ + Enforces simple bounds (0, .9) for battery power fractions. + + Args: + power_fraction: power fraction from heuristic method + + Returns: + bounded power fraction + """ if power_fraction > 0.9: power_fraction = 0.9 - elif power_fraction < 0: - power_fraction = 0 + elif power_fraction < 0.0: + power_fraction = 0.0 return power_fraction - def update_soc(self, power_fraction, soc0) -> float: + def update_soc(self, power_fraction: float, soc0: float) -> float: + """ + Updates SOC based on power fraction threshold (0.1). + + Args: + power_fraction: power fraction from heuristic method. Below threshold + is charging, above is discharging + soc0: initial SOC + + Returns: + Updated SOC. + """ if power_fraction > 0.1: discharge_power = power_fraction * self.maximum_power soc = soc0 - self.time_duration[0] * (1/(self.discharge_efficiency/100.) * discharge_power) / self.capacity - elif power_fraction < 0.9: + elif power_fraction < 0.1: charge_power = - power_fraction * self.maximum_power soc = soc0 + self.time_duration[0] * (self.charge_efficiency / 100. * charge_power) / self.capacity else: soc = soc0 + soc = max(0.1, min(0.9, soc)) + return soc def _heuristic_method(self, _): - """ Does specific heuristic method to fix battery dispatch.""" + """Does specific heuristic method to fix battery dispatch.""" self._enforce_power_fraction_limits() def _enforce_power_fraction_limits(self): diff --git a/hopp/tools/dispatch/plot_tools.py b/hopp/tools/dispatch/plot_tools.py index cff1c3fe3..9e99d49d5 100644 --- a/hopp/tools/dispatch/plot_tools.py +++ b/hopp/tools/dispatch/plot_tools.py @@ -306,6 +306,12 @@ def plot_generation_profile(hybrid: HybridSimulation, ax1.legend(fontsize=font_size-2, loc='upper left') ax1.set_ylabel('Power (MW)', fontsize=font_size) + # Load following, if applicable + if hybrid.site.follow_desired_schedule: + desired_load = [p for p in hybrid.site.desired_schedule[time_slice]] + ax1.plot(time, desired_load, 'b--', label='Desired Load') + ax1.set_ylabel('Desired Load', fontsize=font_size) + ax2 = ax1.twinx() ax2.plot(time, hybrid.battery.outputs.SOC[time_slice], 'k', label='State-of-Charge') ax2.plot(time, hybrid.battery.outputs.dispatch_SOC[time_slice], '.', label='Dispatch') @@ -326,12 +332,6 @@ def plot_generation_profile(hybrid: HybridSimulation, ax1.legend(fontsize=font_size-2, loc='upper left') ax1.set_ylabel('Power (MW)', fontsize=font_size) - if hybrid.site.follow_desired_schedule: - desired_load = [p for p in hybrid.site.desired_schedule[time_slice]] - ax1.plot(time, desired_load, 'b--', label='Price') - ax1.set_ylabel('Desired Load', fontsize=font_size) - ax1.legend(fontsize=font_size-2, loc='upper right') - ax2 = ax1.twinx() price = [p * hybrid.ppa_price[0] for p in hybrid.site.elec_prices.data[time_slice]] From 376e565791e8ad2ba1a8d543cb3d5b07b05e9548 Mon Sep 17 00:00:00 2001 From: Cameron Irmas Date: Thu, 28 Dec 2023 15:14:04 -0800 Subject: [PATCH 10/27] Fix update_soc to use configured params --- .../simple_battery_dispatch_heuristic.py | 17 +++++++++++------ hopp/tools/dispatch/plot_tools.py | 5 +++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index c1d03f3cc..8c5760693 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -1,3 +1,5 @@ +from typing import Optional, List, Dict + import pyomo.environ as pyomo from pyomo.environ import units as u @@ -17,9 +19,9 @@ def __init__(self, index_set: pyomo.Set, system_model: BatteryModel.BatteryStateful, financial_model: Singleowner.Singleowner, - fixed_dispatch: list = None, + fixed_dispatch: Optional[List] = None, block_set_name: str = 'heuristic_battery', - dispatch_options: dict = None): + dispatch_options: Optional[Dict] = None): """ :param fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) @@ -104,16 +106,19 @@ def update_soc(self, power_fraction: float, soc0: float) -> float: Returns: Updated SOC. """ - if power_fraction > 0.1: + if power_fraction > 0.0: discharge_power = power_fraction * self.maximum_power soc = soc0 - self.time_duration[0] * (1/(self.discharge_efficiency/100.) * discharge_power) / self.capacity - elif power_fraction < 0.1: - charge_power = - power_fraction * self.maximum_power + elif power_fraction < 0.0: + charge_power = -power_fraction * self.maximum_power soc = soc0 + self.time_duration[0] * (self.charge_efficiency / 100. * charge_power) / self.capacity else: soc = soc0 - soc = max(0.1, min(0.9, soc)) + min_soc = self._system_model.value("minimum_SOC") / 100 + max_soc = self._system_model.value("maximum_SOC") / 100 + + soc = max(min_soc, min(max_soc, soc)) return soc diff --git a/hopp/tools/dispatch/plot_tools.py b/hopp/tools/dispatch/plot_tools.py index 9e99d49d5..3fcf119a8 100644 --- a/hopp/tools/dispatch/plot_tools.py +++ b/hopp/tools/dispatch/plot_tools.py @@ -303,8 +303,6 @@ def plot_generation_profile(hybrid: HybridSimulation, ax.xaxis.set_ticks(list(range(start, end, hybrid.site.n_periods_per_day))) plt.grid() ax1 = plt.gca() - ax1.legend(fontsize=font_size-2, loc='upper left') - ax1.set_ylabel('Power (MW)', fontsize=font_size) # Load following, if applicable if hybrid.site.follow_desired_schedule: @@ -312,6 +310,9 @@ def plot_generation_profile(hybrid: HybridSimulation, ax1.plot(time, desired_load, 'b--', label='Desired Load') ax1.set_ylabel('Desired Load', fontsize=font_size) + ax1.legend(fontsize=font_size-2, loc='upper left') + ax1.set_ylabel('Power (MW)', fontsize=font_size) + ax2 = ax1.twinx() ax2.plot(time, hybrid.battery.outputs.SOC[time_slice], 'k', label='State-of-Charge') ax2.plot(time, hybrid.battery.outputs.dispatch_SOC[time_slice], '.', label='Dispatch') From 3fe5d2b91208a9f9f0e81b7ba77068522291fb7f Mon Sep 17 00:00:00 2001 From: elenya-grant <116225007+elenya-grant@users.noreply.github.com> Date: Thu, 28 Mar 2024 13:41:20 -0600 Subject: [PATCH 11/27] Bug Fix: email for API calls (#291) * added email for API calls to be set in .env file and removed self.email from resource.py * changed EMAIL to NREL_API_EMAIL * added space after comma seperator --- README.md | 6 +++--- .../technologies/resource/resource.py | 1 - .../technologies/resource/solar_resource.py | 4 ++-- .../technologies/resource/wind_resource.py | 4 ++-- hopp/utilities/keys.py | 18 +++++++++++++++++- 5 files changed, 24 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 2fdc54713..93f431cf7 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,12 @@ solar and storage. [https://developer.nrel.gov/signup/](https://developer.nrel.gov/signup/) -7. To set up the `NREL_API_KEY` required for resource downloads, you can create an Environment Variable called - `NREL_API_KEY`. Otherwise, you can keep the key in a new file called ".env" in the root directory of this project. +7. To set up the `NREL_API_KEY` and `NREL_API_EMAIL` required for resource downloads, you can create Environment Variables called `NREL_API_KEY` and `NREL_API_EMAIL`. Otherwise, you can keep the key in a new file called ".env" in the root directory of this project. Create a file ".env" that contains the single line: - ``` + ``` NREL_API_KEY=key + NREL_API_EMAIL=your.name@email.com ``` 8. Verify setup by running tests: diff --git a/hopp/simulation/technologies/resource/resource.py b/hopp/simulation/technologies/resource/resource.py index 4df12be76..607ffa842 100644 --- a/hopp/simulation/technologies/resource/resource.py +++ b/hopp/simulation/technologies/resource/resource.py @@ -36,7 +36,6 @@ def __init__(self, lat, lon, year, **kwargs): self.name = 'hybrid-systems' self.affiliation = 'NREL' self.reason = 'hybrid-analysis' - self.email = 'nicholas.diorio@nrel.gov' self.mailing_list = 'true' # paths diff --git a/hopp/simulation/technologies/resource/solar_resource.py b/hopp/simulation/technologies/resource/solar_resource.py index a69d10be7..781fa9aae 100644 --- a/hopp/simulation/technologies/resource/solar_resource.py +++ b/hopp/simulation/technologies/resource/solar_resource.py @@ -6,7 +6,7 @@ import csv from PySAM.ResourceTools import SAM_CSV_to_solar_data -from hopp.utilities.keys import get_developer_nrel_gov_key +from hopp.utilities.keys import get_developer_nrel_gov_key, get_developer_nrel_gov_email from hopp.utilities.log import hybrid_logger as logger from hopp.simulation.technologies.resource.resource import Resource from hopp import ROOT_DIR @@ -71,7 +71,7 @@ def __init__( def download_resource(self): url = '{base}?wkt=POINT({lon}+{lat})&names={year}&leap_day={leap}&interval={interval}&utc={utc}&full_name={name}&email={email}&affiliation={affiliation}&mailing_list={mailing_list}&reason={reason}&api_key={api}&attributes={attr}'.format( base=BASE_URL, year=self.year, lat=self.latitude, lon=self.longitude, leap=self.leap_year, interval=self.interval, - utc=self.utc, name=self.name, email=self.email, + utc=self.utc, name=self.name, email=get_developer_nrel_gov_email(), mailing_list=self.mailing_list, affiliation=self.affiliation, reason=self.reason, api=get_developer_nrel_gov_key(), attr=self.solar_attributes) diff --git a/hopp/simulation/technologies/resource/wind_resource.py b/hopp/simulation/technologies/resource/wind_resource.py index 0d781351c..a090849d6 100644 --- a/hopp/simulation/technologies/resource/wind_resource.py +++ b/hopp/simulation/technologies/resource/wind_resource.py @@ -3,7 +3,7 @@ from typing import Union from PySAM.ResourceTools import SRW_to_wind_data -from hopp.utilities.keys import get_developer_nrel_gov_key +from hopp.utilities.keys import get_developer_nrel_gov_key, get_developer_nrel_gov_email from hopp.simulation.technologies.resource.resource import Resource from hopp import ROOT_DIR @@ -121,7 +121,7 @@ def download_resource(self): if self.source == "WTK": url = '{base}?year={year}&lat={lat}&lon={lon}&hubheight={hubheight}&api_key={api_key}&email={email}'.format( - base=WTK_BASE_URL, year=self.year, lat=self.latitude, lon=self.longitude, hubheight=height, api_key=get_developer_nrel_gov_key(), email=self.email + base=WTK_BASE_URL, year=self.year, lat=self.latitude, lon=self.longitude, hubheight=height, api_key=get_developer_nrel_gov_key(), email=get_developer_nrel_gov_email() ) elif self.source == "TAP": url = '{base}?height={hubheight}m&lat={lat}&lon={lon}&year={year}'.format( diff --git a/hopp/utilities/keys.py b/hopp/utilities/keys.py index 2895d91f0..bb460af1d 100644 --- a/hopp/utilities/keys.py +++ b/hopp/utilities/keys.py @@ -2,12 +2,15 @@ import os developer_nrel_gov_key = "" +developer_nrel_gov_email = "" def set_developer_nrel_gov_key(key: str): global developer_nrel_gov_key developer_nrel_gov_key = key - +def set_developer_nrel_gov_email(email: str): + global developer_nrel_gov_email + developer_nrel_gov_email = email def get_developer_nrel_gov_key(): global developer_nrel_gov_key @@ -20,6 +23,16 @@ def get_developer_nrel_gov_key(): "Section 2 of 'Installing from Package Repositories' in the README.md") return developer_nrel_gov_key +def get_developer_nrel_gov_email(): + global developer_nrel_gov_email + if developer_nrel_gov_email is None: + raise ValueError("Please provide NREL Developer email using `set_developer_nrel_gov_email`" + "(`from hopp.utilities.keys import set_developer_nrel_gov_email`) \n" + " Ensure your Developer email is set either as a `EMAIL` Environment Variable or" + " using the .env file method. For details on setting up .env, " + "please see Section 7 of 'Installing from Source' or " + "Section 2 of 'Installing from Package Repositories' in the README.md") + return developer_nrel_gov_email def set_nrel_key_dot_env(path=None): if path and os.path.exists(path): @@ -28,5 +41,8 @@ def set_nrel_key_dot_env(path=None): r = find_dotenv(usecwd=True) load_dotenv(r) NREL_API_KEY = os.getenv("NREL_API_KEY") + NREL_API_EMAIL = os.getenv("NREL_API_EMAIL") if NREL_API_KEY is not None: set_developer_nrel_gov_key(NREL_API_KEY) + if NREL_API_EMAIL is not None: + set_developer_nrel_gov_email(NREL_API_EMAIL) From 0ad85a72ea69e6dc16be62504ae958f47dc1e0d5 Mon Sep 17 00:00:00 2001 From: bayc Date: Fri, 5 Apr 2024 10:23:17 -0600 Subject: [PATCH 12/27] formatting --- .../technologies/dispatch/grid_dispatch.py | 45 ++- .../technologies/dispatch/hybrid_dispatch.py | 363 ++++++++++++------ .../dispatch/power_sources/pv_dispatch.py | 26 +- .../dispatch/power_sources/tower_dispatch.py | 47 ++- .../dispatch/power_sources/trough_dispatch.py | 29 +- .../dispatch/power_sources/wave_dispatch.py | 27 +- .../dispatch/power_sources/wind_dispatch.py | 27 +- .../power_storage/power_storage_dispatch.py | 58 ++- 8 files changed, 431 insertions(+), 191 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index c3c9b270a..3a6ec4e6d 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -12,18 +12,22 @@ class GridDispatch(Dispatch): """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model, - financial_model, - block_set_name: str = 'grid'): - - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name) + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model, + financial_model, + block_set_name: str = 'grid', + ): + + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) def dispatch_block_rule(self, grid): # Parameters @@ -112,7 +116,12 @@ def _create_grid_constraints(grid): ################################## grid.balance = pyomo.Constraint( doc="Transmission energy balance", - expr=grid.electricity_sold - grid.electricity_purchased == grid.system_generation - grid.system_load + expr=( + grid.electricity_sold + - grid.electricity_purchased + == grid.system_generation + - grid.system_load + ) ) grid.sales_transmission_limit = pyomo.Constraint( doc="Transmission limit on electricity sales", @@ -120,7 +129,11 @@ def _create_grid_constraints(grid): ) grid.purchases_transmission_limit = pyomo.Constraint( doc="Transmission limit on electricity purchases", - expr=grid.electricity_purchased <= grid.load_transmission_limit * (1 - grid.is_generating) + expr=( + grid.electricity_purchased + <= grid.load_transmission_limit + * (1 - grid.is_generating) + ) ) @staticmethod @@ -184,7 +197,9 @@ def generation_transmission_limit(self) -> list: def generation_transmission_limit(self, limit_mw: list): if len(limit_mw) == len(self.blocks): for t, limit in zip(self.blocks, limit_mw): - self.blocks[t].generation_transmission_limit.set_value(round(limit, self.round_digits)) + self.blocks[t].generation_transmission_limit.set_value( + round(limit, self.round_digits) + ) else: raise ValueError("'limit_mw' list must be the same length as time horizon") diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index b0412d266..3a8688cf0 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -10,12 +10,14 @@ class HybridDispatch(Dispatch): """ """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - power_sources: dict, - dispatch_options: HybridDispatchOptions = None, - block_set_name: str = 'hybrid'): + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + power_sources: dict, + dispatch_options: HybridDispatchOptions = None, + block_set_name: str = 'hybrid', + ): """ Parameters @@ -32,11 +34,13 @@ def __init__(self, self.ports = {key: [] for key in index_set} self.arcs = [] - super().__init__(pyomo_model, - index_set, - None, - None, - block_set_name=block_set_name) + super().__init__( + pyomo_model, + index_set, + None, + None, + block_set_name=block_set_name, + ) def dispatch_block_rule(self, hybrid, t): ################################## @@ -71,14 +75,16 @@ def _create_parameters(hybrid): initialize=1.0, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) def _create_pv_variables(self, hybrid, t): hybrid.pv_generation = pyomo.Var( doc="Power generation of photovoltaics [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) self.power_source_gen_vars[t].append(hybrid.pv_generation) def _create_pv_port(self, hybrid, t): @@ -90,7 +96,8 @@ def _create_wind_variables(self, hybrid, t): doc="Power generation of wind turbines [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) self.power_source_gen_vars[t].append(hybrid.wind_generation) def _create_wind_port(self, hybrid, t): @@ -102,7 +109,8 @@ def _create_wave_variables(self, hybrid, t): doc="Power generation of wave devices [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) self.power_source_gen_vars[t].append(hybrid.wave_generation) def _create_wave_port(self, hybrid, t): @@ -114,18 +122,24 @@ def _create_tower_variables(self, hybrid, t): doc="Power generation of CSP tower [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) hybrid.tower_load = pyomo.Var( doc="Load of CSP tower [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) self.power_source_gen_vars[t].append(hybrid.tower_generation) self.load_vars[t].append(hybrid.tower_load) def _create_tower_port(self, hybrid, t): - hybrid.tower_port = Port(initialize={'cycle_generation': hybrid.tower_generation, - 'system_load': hybrid.tower_load}) + hybrid.tower_port = Port( + initialize={ + 'cycle_generation': hybrid.tower_generation, + 'system_load': hybrid.tower_load, + } + ) self.ports[t].append(hybrid.tower_port) def _create_trough_variables(self, hybrid, t): @@ -133,18 +147,24 @@ def _create_trough_variables(self, hybrid, t): doc="Power generation of CSP trough [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) hybrid.trough_load = pyomo.Var( doc="Load of CSP trough [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) self.power_source_gen_vars[t].append(hybrid.trough_generation) self.load_vars[t].append(hybrid.trough_load) def _create_trough_port(self, hybrid, t): - hybrid.trough_port = Port(initialize={'cycle_generation': hybrid.trough_generation, - 'system_load': hybrid.trough_load}) + hybrid.trough_port = Port( + initialize={ + 'cycle_generation': hybrid.trough_generation, + 'system_load': hybrid.trough_load, + } + ) self.ports[t].append(hybrid.trough_port) def _create_battery_variables(self, hybrid, t): @@ -152,18 +172,24 @@ def _create_battery_variables(self, hybrid, t): doc="Power charging the electric battery [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) hybrid.battery_discharge = pyomo.Var( doc="Power discharging the electric battery [MW]", domain=pyomo.NonNegativeReals, units=u.MW, - initialize=0.0) + initialize=0.0, + ) self.power_source_gen_vars[t].append(hybrid.battery_discharge) self.load_vars[t].append(hybrid.battery_charge) def _create_battery_port(self, hybrid, t): - hybrid.battery_port = Port(initialize={'charge_power': hybrid.battery_charge, - 'discharge_power': hybrid.battery_discharge}) + hybrid.battery_port = Port( + initialize={ + 'charge_power': hybrid.battery_charge, + 'discharge_power': hybrid.battery_discharge, + } + ) self.ports[t].append(hybrid.battery_port) @staticmethod @@ -171,47 +197,59 @@ def _create_grid_variables(hybrid, _): hybrid.system_generation = pyomo.Var( doc="System generation [MW]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) hybrid.system_load = pyomo.Var( doc="System load [MW]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) hybrid.electricity_sold = pyomo.Var( doc="Electricity sold [MW]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) hybrid.electricity_purchased = pyomo.Var( doc="Electricity purchased [MW]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) def _create_grid_port(self, hybrid, t): - hybrid.grid_port = Port(initialize={'system_generation': hybrid.system_generation, - 'system_load': hybrid.system_load, - 'electricity_sold': hybrid.electricity_sold, - 'electricity_purchased': hybrid.electricity_purchased}) + hybrid.grid_port = Port( + initialize={ + 'system_generation': hybrid.system_generation, + 'system_load': hybrid.system_load, + 'electricity_sold': hybrid.electricity_sold, + 'electricity_purchased': hybrid.electricity_purchased, + } + ) self.ports[t].append(hybrid.grid_port) def _create_grid_constraints(self, hybrid, t): hybrid.generation_total = pyomo.Constraint( doc="hybrid system generation total", - rule=hybrid.system_generation == sum(self.power_source_gen_vars[t])) + rule=hybrid.system_generation == sum(self.power_source_gen_vars[t]), + ) hybrid.load_total = pyomo.Constraint( doc="hybrid system load total", - rule=hybrid.system_load == sum(self.load_vars[t])) + rule=hybrid.system_load == sum(self.load_vars[t]), + ) @staticmethod def _create_grid_battery_limitation(hybrid): hybrid.no_grid_battery_charge = pyomo.Constraint( doc="Battery storage cannot charge via the grid", - expr=hybrid.system_generation >= hybrid.battery_charge) + expr=hybrid.system_generation >= hybrid.battery_charge + ) @staticmethod def _create_pv_battery_limitation(hybrid): hybrid.only_pv_battery_charge = pyomo.Constraint( doc="Battery storage can only charge from pv", - expr=hybrid.pv_generation >= hybrid.battery_charge) + expr=hybrid.pv_generation >= hybrid.battery_charge + ) def create_arcs(self): ################################## @@ -246,60 +284,108 @@ def create_max_gross_profit_objective(self): if 'grid' in self.power_sources.keys(): tb = self.power_sources['grid'].dispatch.blocks - self.model.grid_obj = pyomo.Expression(expr= - sum(self.blocks[t].time_weighting_factor * tb[t].time_duration - * tb[t].electricity_sell_price * self.blocks[t].electricity_sold - - (1/self.blocks[t].time_weighting_factor) * tb[t].time_duration - * tb[t].electricity_purchase_price * self.blocks[t].electricity_purchased - - tb[t].epsilon * tb[t].is_generating - for t in self.blocks.index_set())) - + self.model.grid_obj = pyomo.Expression( + expr=sum( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * tb[t].electricity_sell_price + * self.blocks[t].electricity_sold + - (1/self.blocks[t].time_weighting_factor) + * tb[t].time_duration + * tb[t].electricity_purchase_price + * self.blocks[t].electricity_purchased + - tb[t].epsilon + * tb[t].is_generating + for t in self.blocks.index_set() + ) + ) + if 'pv' in self.power_sources.keys(): tb = self.power_sources['pv'].dispatch.blocks - self.model.pv_obj = pyomo.Expression(expr= - sum(- (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration * tb[t].cost_per_generation * self.blocks[t].pv_generation - for t in self.blocks.index_set())) + self.model.pv_obj = pyomo.Expression( + expr=sum( + - (1/self.blocks[t].time_weighting_factor) + * tb[t].time_duration + * tb[t].cost_per_generation + * self.blocks[t].pv_generation + for t in self.blocks.index_set() + ) + ) if 'wind' in self.power_sources.keys(): tb = self.power_sources['wind'].dispatch.blocks - self.model.wind_obj = pyomo.Expression(expr= - sum(- (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration * tb[t].cost_per_generation * self.blocks[t].wind_generation - for t in self.blocks.index_set())) + self.model.wind_obj = pyomo.Expression( + expr=sum( + - (1/self.blocks[t].time_weighting_factor) + * tb[t].time_duration + * tb[t].cost_per_generation + * self.blocks[t].wind_generation + for t in self.blocks.index_set() + ) + ) if 'wave' in self.power_sources.keys(): tb = self.power_sources['wave'].dispatch.blocks - self.model.wave_obj = pyomo.Expression(expr= - sum(- (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration * tb[t].cost_per_generation * self.blocks[t].wave_generation - for t in self.blocks.index_set())) + self.model.wave_obj = pyomo.Expression( + expr=sum( + - (1/self.blocks[t].time_weighting_factor) + * tb[t].time_duration + * tb[t].cost_per_generation + * self.blocks[t].wave_generation + for t in self.blocks.index_set() + ) + ) csp_techs = [i for i in ['tower', 'trough'] if i in self.power_sources.keys()] for tech in csp_techs: tb = self.power_sources[tech].dispatch.blocks - objective = pyomo.Expression(expr= - sum(- (1/self.blocks[t].time_weighting_factor) - * ((tb[t].cost_per_field_generation - * tb[t].receiver_thermal_power - * tb[t].time_duration) - + tb[t].cost_per_field_start * tb[t].incur_field_start - + (tb[t].cost_per_cycle_generation - * tb[t].cycle_generation - * tb[t].time_duration) - + tb[t].cost_per_cycle_start * tb[t].incur_cycle_start - + tb[t].cost_per_change_thermal_input * tb[t].cycle_thermal_ramp) - for t in self.blocks.index_set())) + objective = pyomo.Expression( + expr=sum( + - (1/self.blocks[t].time_weighting_factor) + * ( + ( + tb[t].cost_per_field_generation + * tb[t].receiver_thermal_power + * tb[t].time_duration + ) + + ( + tb[t].cost_per_field_start + * tb[t].incur_field_start + ) + + ( + tb[t].cost_per_cycle_generation + * tb[t].cycle_generation + * tb[t].time_duration + ) + + ( + tb[t].cost_per_cycle_start + * tb[t].incur_cycle_start + ) + + ( + tb[t].cost_per_change_thermal_input + * tb[t].cycle_thermal_ramp + ) + ) + for t in self.blocks.index_set() + ) + ) setattr(self.model, tech + "_obj", objective) - + if 'battery' in self.power_sources.keys(): def battery_profit_objective_rule(m): objective = 0 tb = self.power_sources['battery'].dispatch.blocks - objective += sum(- (1/self.blocks[t].time_weighting_factor) * tb[t].time_duration - * (tb[t].cost_per_charge * self.blocks[t].battery_charge - + tb[t].cost_per_discharge * self.blocks[t].battery_discharge) - for t in self.blocks.index_set()) + objective += sum( + - (1/self.blocks[t].time_weighting_factor) + * tb[t].time_duration + * ( + tb[t].cost_per_charge + * self.blocks[t].battery_charge + + tb[t].cost_per_discharge + * self.blocks[t].battery_discharge + ) + for t in self.blocks.index_set() + ) tb = self.power_sources['battery'].dispatch if tb.options.include_lifecycle_count: objective -= tb.model.lifecycle_cost * sum(tb.model.lifecycles) @@ -324,48 +410,90 @@ def operating_cost_objective_rule(m): for tech in self.power_sources.keys(): if tech == 'grid': tb = self.power_sources[tech].dispatch.blocks - objective += sum(self.blocks[t].time_weighting_factor * tb[t].time_duration - * tb[t].electricity_sell_price * (tb[t].generation_transmission_limit - - self.blocks[t].electricity_sold) - + self.blocks[t].time_weighting_factor * tb[t].time_duration - * tb[t].electricity_purchase_price * self.blocks[t].electricity_purchased - + tb[t].epsilon * tb[t].is_generating - for t in self.blocks.index_set()) + objective += sum( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * tb[t].electricity_sell_price + * ( + tb[t].generation_transmission_limit + - self.blocks[t].electricity_sold + ) + + ( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * tb[t].electricity_purchase_price + * self.blocks[t].electricity_purchased + ) + + ( + tb[t].epsilon + * tb[t].is_generating + ) + for t in self.blocks.index_set() + ) elif tech == 'pv': tb = self.power_sources[tech].dispatch.blocks - objective += sum(self.blocks[t].time_weighting_factor * tb[t].time_duration - * tb[t].cost_per_generation * self.blocks[t].pv_generation - for t in self.blocks.index_set()) + objective += sum( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * tb[t].cost_per_generation + * self.blocks[t].pv_generation + for t in self.blocks.index_set() + ) elif tech == 'wind': tb = self.power_sources[tech].dispatch.blocks - objective += sum(self.blocks[t].time_weighting_factor * tb[t].time_duration - * tb[t].cost_per_generation * self.blocks[t].wind_generation - for t in self.blocks.index_set()) + objective += sum( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * tb[t].cost_per_generation + * self.blocks[t].wind_generation + for t in self.blocks.index_set() + ) elif tech == 'wave': tb = self.power_sources[tech].dispatch.blocks - objective += sum(self.blocks[t].time_weighting_factor * tb[t].time_duration - * tb[t].cost_per_generation * self.blocks[t].wave_generation - for t in self.blocks.index_set()) + objective += sum( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * tb[t].cost_per_generation + * self.blocks[t].wave_generation + for t in self.blocks.index_set() + ) elif tech == 'tower' or tech == 'trough': tb = self.power_sources[tech].dispatch.blocks - objective += sum(self.blocks[t].time_weighting_factor - * (tb[t].cost_per_field_start * tb[t].incur_field_start - - (tb[t].cost_per_field_generation - * tb[t].receiver_thermal_power - * tb[t].time_duration) # Trying to incentivize TES generation - + (tb[t].cost_per_cycle_generation - * tb[t].cycle_generation - * tb[t].time_duration) - + tb[t].cost_per_cycle_start * tb[t].incur_cycle_start - + tb[t].cost_per_change_thermal_input * tb[t].cycle_thermal_ramp) - for t in self.blocks.index_set()) + objective += sum( + self.blocks[t].time_weighting_factor + * ( + tb[t].cost_per_field_start + * tb[t].incur_field_start + - ( + tb[t].cost_per_field_generation + * tb[t].receiver_thermal_power + * tb[t].time_duration + ) # Trying to incentivize TES generation + + ( + tb[t].cost_per_cycle_generation + * tb[t].cycle_generation + * tb[t].time_duration + ) + + tb[t].cost_per_cycle_start + * tb[t].incur_cycle_start + + tb[t].cost_per_change_thermal_input + * tb[t].cycle_thermal_ramp + ) + for t in self.blocks.index_set() + ) elif tech == 'battery': tb = self.power_sources[tech].dispatch.blocks - objective += sum(self.blocks[t].time_weighting_factor * tb[t].time_duration - * (tb[t].cost_per_discharge * self.blocks[t].battery_discharge - - tb[t].cost_per_charge * self.blocks[t].battery_charge) - # Try to incentivize battery charging - for t in self.blocks.index_set()) + objective += sum( + self.blocks[t].time_weighting_factor + * tb[t].time_duration + * ( + tb[t].cost_per_discharge + * self.blocks[t].battery_discharge + - tb[t].cost_per_charge + * self.blocks[t].battery_charge + ) # Try to incentivize battery charging + for t in self.blocks.index_set() + ) tb = self.power_sources['battery'].dispatch if tb.options.include_lifecycle_count: objective += tb.model.lifecycle_cost * tb.model.lifecycles @@ -373,7 +501,8 @@ def operating_cost_objective_rule(m): self.model.objective = pyomo.Objective( rule=operating_cost_objective_rule, - sense=pyomo.minimize) + sense=pyomo.minimize + ) @property def time_weighting_factor(self) -> float: @@ -442,12 +571,20 @@ def system_load(self) -> list: def electricity_sales(self) -> list: if 'grid' in self.power_sources: tb = self.power_sources['grid'].dispatch.blocks - return [tb[t].time_duration.value * tb[t].electricity_sell_price.value - * self.blocks[t].electricity_sold.value for t in self.blocks.index_set()] + return [ + tb[t].time_duration.value + * tb[t].electricity_sell_price.value + * self.blocks[t].electricity_sold.value + for t in self.blocks.index_set() + ] @property def electricity_purchases(self) -> list: if 'grid' in self.power_sources: tb = self.power_sources['grid'].dispatch.blocks - return [tb[t].time_duration.value * tb[t].electricity_purchase_price.value - * self.blocks[t].electricity_purchased.value for t in self.blocks.index_set()] \ No newline at end of file + return [ + tb[t].time_duration.value + * tb[t].electricity_purchase_price.value + * self.blocks[t].electricity_purchased.value + for t in self.blocks.index_set() + ] diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 5e3b737ed..322098a65 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -14,14 +14,24 @@ class PvDispatch(PowerSourceDispatch): """ """ - def __init__(self, - pyomo_model: ConcreteModel, - indexed_set: Set, - system_model: Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8], - financial_model: FinancialModelType, - block_set_name: str = 'pv'): - super().__init__(pyomo_model, indexed_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: ConcreteModel, + indexed_set: Set, + system_model: Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8], + financial_model: FinancialModelType, + block_set_name: str = 'pv', + ): + super().__init__( + pyomo_model, + indexed_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) def update_time_series_parameters(self, start_time: int): super().update_time_series_parameters(start_time) - self.available_generation = [max(0, i) for i in self.available_generation] # zero out any negative load + + # zero out any negative load + self.available_generation = [max(0, i) for i in self.available_generation] diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index fd596a2fb..cec09afd5 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -10,24 +10,45 @@ class TowerDispatch(CspDispatch): """ """ - def __init__(self, - pyomo_model: ConcreteModel, - indexed_set: Set, - system_model: None, - financial_model: FinancialModelType, - block_set_name: str = 'tower'): - super().__init__(pyomo_model, indexed_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: ConcreteModel, + indexed_set: Set, + system_model: None, + financial_model: FinancialModelType, + block_set_name: str = 'tower', + ): + super().__init__( + pyomo_model, + indexed_set, + system_model, + financial_model, + block_set_name=block_set_name + ) def update_initial_conditions(self): super().update_initial_conditions() csp = self._system_model # Note, SS receiver model in ssc assumes full available power is used for startup # (even if, time requirement is binding) - rec_accumulate_time = max(0.0, csp.value('rec_su_delay') - csp.plant_state['rec_startup_time_remain_init']) - rec_accumulate_energy = max(0.0, self.receiver_required_startup_energy - - csp.plant_state['rec_startup_energy_remain_init'] / 1e6) - self.initial_receiver_startup_inventory = min(rec_accumulate_energy, - rec_accumulate_time * self.allowable_receiver_startup_power) - if self.initial_receiver_startup_inventory > (1.0 - 1.e-6) * self.receiver_required_startup_energy: + rec_accumulate_time = max( + 0.0, + csp.value('rec_su_delay') - csp.plant_state['rec_startup_time_remain_init'] + ) + rec_accumulate_energy = max( + 0.0, + ( + self.receiver_required_startup_energy + - csp.plant_state['rec_startup_energy_remain_init'] / 1e6 + ) + ) + self.initial_receiver_startup_inventory = min( + rec_accumulate_energy, + rec_accumulate_time * self.allowable_receiver_startup_power + ) + if ( + self.initial_receiver_startup_inventory > (1.0 - 1.e-6) + * self.receiver_required_startup_energy + ): self.initial_receiver_startup_inventory = self.receiver_required_startup_energy diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index 6694cc0a7..c3df3f377 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -10,18 +10,29 @@ class TroughDispatch(CspDispatch): """ """ - def __init__(self, - pyomo_model: ConcreteModel, - indexed_set: Set, - system_model: None, - financial_model: FinancialModelType, - block_set_name: str = 'trough'): - super().__init__(pyomo_model, indexed_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: ConcreteModel, + indexed_set: Set, + system_model: None, + financial_model: FinancialModelType, + block_set_name: str = 'trough', + ): + super().__init__( + pyomo_model, + indexed_set, + system_model, + financial_model, + block_set_name=block_set_name + ) def update_initial_conditions(self): super().update_initial_conditions() self.initial_receiver_startup_inventory = 0.0 # FIXME: if self.is_field_starting_initial: - print('Warning: Solar field is starting at the initial time step of the dispatch horizon, but initial ' - 'startup energy inventory is assumed to be zero. This may result in persistent receiver start-up') + print( + "Warning: Solar field is starting at the initial time step of the dispatch " + "horizon, but initial startup energy inventory is assumed to be zero. This may " + "result in persistent receiver start-up." + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index 55f1d617a..aa20072da 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -3,8 +3,9 @@ import PySAM.MhkWave as MhkWave from hopp.simulation.technologies.financial import FinancialModelType -from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import PowerSourceDispatch - +from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import ( + PowerSourceDispatch +) class WaveDispatch(PowerSourceDispatch): @@ -13,11 +14,19 @@ class WaveDispatch(PowerSourceDispatch): """ """ - def __init__(self, - pyomo_model: ConcreteModel, - indexed_set: Set, - system_model: MhkWave.MhkWave, - financial_model: FinancialModelType, - block_set_name: str = 'wave'): - super().__init__(pyomo_model, indexed_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: ConcreteModel, + indexed_set: Set, + system_model: MhkWave.MhkWave, + financial_model: FinancialModelType, + block_set_name: str = 'wave', + ): + super().__init__( + pyomo_model, + indexed_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 1fe99861a..79b92bcd6 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -4,22 +4,33 @@ import PySAM.Windpower as Windpower from hopp.simulation.technologies.financial import FinancialModelType -from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import PowerSourceDispatch +from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import ( + PowerSourceDispatch +) if TYPE_CHECKING: from hopp.simulation.technologies.wind.floris import Floris + class WindDispatch(PowerSourceDispatch): _system_model: Union[Windpower.Windpower,"Floris"] _financial_model: FinancialModelType """ """ - def __init__(self, - pyomo_model: ConcreteModel, - indexed_set: Set, - system_model: Union[Windpower.Windpower,"Floris"], - financial_model: FinancialModelType, - block_set_name: str = 'wind'): - super().__init__(pyomo_model, indexed_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: ConcreteModel, + indexed_set: Set, + system_model: Union[Windpower.Windpower,"Floris"], + financial_model: FinancialModelType, + block_set_name: str = 'wind', + ): + super().__init__( + pyomo_model, + indexed_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index f88986482..69b43a7fe 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -11,15 +11,23 @@ class PowerStorageDispatch(Dispatch): """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model, - financial_model, - block_set_name: str, - dispatch_options): - - super().__init__(pyomo_model, index_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model, + financial_model, + block_set_name: str, + dispatch_options, + ): + + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) self._create_soc_linking_constraint() # TODO: we could remove this option and just have lifecycle count default @@ -168,8 +176,16 @@ def _create_storage_constraints(self, storage): def _create_soc_inventory_constraint(self, storage): def soc_inventory_rule(m): - return m.soc == (m.soc0 + m.time_duration * (m.charge_efficiency * m.charge_power - - (1 / m.discharge_efficiency) * m.discharge_power) / m.capacity) + return m.soc == ( + m.soc0 + + m.time_duration + * ( + m.charge_efficiency + * m.charge_power + - (1 / m.discharge_efficiency) + * m.discharge_power + ) / m.capacity + ) # Storage State-of-charge balance storage.soc_inventory = pyomo.Constraint( doc=self.block_set_name + " state-of-charge inventory balance", @@ -270,12 +286,17 @@ def _check_initial_soc(self, initial_soc): initial_soc /= 100. initial_soc = round(initial_soc, self.round_digits) if initial_soc > self.maximum_soc/100: - print("Warning: Storage dispatch was initialized with a state-of-charge greater than maximum value!") + print( + "Warning: Storage dispatch was initialized with a state-of-charge greater than " + "maximum value!") print("Initial SOC = {}".format(initial_soc)) print("Initial SOC was set to maximum value.") initial_soc = self.maximum_soc / 100 elif initial_soc < self.minimum_soc/100: - print("Warning: Storage dispatch was initialized with a state-of-charge less than minimum value!") + print( + "Warning: Storage dispatch was initialized with a state-of-charge less than " + "minimum value!" + ) print("Initial SOC = {}".format(initial_soc)) print("Initial SOC was set to minimum value.") initial_soc = self.minimum_soc / 100 @@ -295,7 +316,9 @@ def time_duration(self, time_duration: list): for t, delta in zip(self.blocks, time_duration): self.blocks[t].time_duration = round(delta, self.round_digits) else: - raise ValueError(self.time_duration.__name__ + " list must be the same length as time horizon") + raise ValueError( + self.time_duration.__name__ + " list must be the same length as time horizon" + ) @property def cost_per_charge(self) -> float: @@ -390,7 +413,8 @@ def round_trip_efficiency(self) -> float: @round_trip_efficiency.setter def round_trip_efficiency(self, round_trip_efficiency: float): round_trip_efficiency = self._check_efficiency_value(round_trip_efficiency) - efficiency = round_trip_efficiency ** (1 / 2) # Assumes equal charge and discharge efficiencies + # Assumes equal charge and discharge efficiencies + efficiency = round_trip_efficiency ** (1 / 2) self.charge_efficiency = efficiency self.discharge_efficiency = efficiency @@ -428,7 +452,9 @@ def lifecycle_cost_per_kWh_cycle(self) -> float: @lifecycle_cost_per_kWh_cycle.setter def lifecycle_cost_per_kWh_cycle(self, lifecycle_cost_per_kWh_cycle: float): self.options.lifecycle_cost_per_kWh_cycle = lifecycle_cost_per_kWh_cycle - self.model.lifecycle_cost = lifecycle_cost_per_kWh_cycle * self._system_model.value('nominal_energy') + self.model.lifecycle_cost = ( + lifecycle_cost_per_kWh_cycle * self._system_model.value('nominal_energy') + ) # Outputs @property From 1bfcf531063da0053bb76329e5b9194b1fb8374e Mon Sep 17 00:00:00 2001 From: bayc Date: Fri, 5 Apr 2024 16:42:20 -0600 Subject: [PATCH 13/27] move the max_gross_profit_objective from hybrid_dispatch into respective technology dispatches --- .../technologies/dispatch/grid_dispatch.py | 19 +++ .../technologies/dispatch/hybrid_dispatch.py | 128 +++--------------- .../dispatch/power_sources/pv_dispatch.py | 14 +- .../dispatch/power_sources/tower_dispatch.py | 35 ++++- .../dispatch/power_sources/trough_dispatch.py | 35 ++++- .../dispatch/power_sources/wave_dispatch.py | 14 +- .../dispatch/power_sources/wind_dispatch.py | 13 +- .../power_storage/power_storage_dispatch.py | 18 +++ 8 files changed, 160 insertions(+), 116 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index 3a6ec4e6d..f41043b29 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -1,3 +1,5 @@ +from typing import Union + import pyomo.environ as pyomo from pyomo.network import Port, Arc from pyomo.environ import units as u @@ -6,6 +8,7 @@ class GridDispatch(Dispatch): + grid_obj: Union[pyomo.Expression, float] _model: pyomo.ConcreteModel _blocks: pyomo.Block """ @@ -39,6 +42,22 @@ def dispatch_block_rule(self, grid): # Ports self._create_grid_ports(grid) + def max_gross_profit_objective(self, blocks): + self.grid_obj = pyomo.Expression( + expr=sum( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * self.blocks[t].electricity_sell_price + * blocks[t].electricity_sold + - (1/blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].electricity_purchase_price + * blocks[t].electricity_purchased + - self.blocks[t].epsilon + * self.blocks[t].is_generating + for t in blocks.index_set() + ) + ) @staticmethod def _create_grid_parameters(grid): ################################## diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index 3a8688cf0..6d7dedf39 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -282,119 +282,25 @@ def _delete_objective(self): def create_max_gross_profit_objective(self): self._delete_objective() - if 'grid' in self.power_sources.keys(): - tb = self.power_sources['grid'].dispatch.blocks - self.model.grid_obj = pyomo.Expression( - expr=sum( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * tb[t].electricity_sell_price - * self.blocks[t].electricity_sold - - (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration - * tb[t].electricity_purchase_price - * self.blocks[t].electricity_purchased - - tb[t].epsilon - * tb[t].is_generating - for t in self.blocks.index_set() - ) - ) - - if 'pv' in self.power_sources.keys(): - tb = self.power_sources['pv'].dispatch.blocks - self.model.pv_obj = pyomo.Expression( - expr=sum( - - (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration - * tb[t].cost_per_generation - * self.blocks[t].pv_generation - for t in self.blocks.index_set() - ) - ) - - if 'wind' in self.power_sources.keys(): - tb = self.power_sources['wind'].dispatch.blocks - self.model.wind_obj = pyomo.Expression( - expr=sum( - - (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration - * tb[t].cost_per_generation - * self.blocks[t].wind_generation - for t in self.blocks.index_set() - ) - ) - - if 'wave' in self.power_sources.keys(): - tb = self.power_sources['wave'].dispatch.blocks - self.model.wave_obj = pyomo.Expression( - expr=sum( - - (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration - * tb[t].cost_per_generation - * self.blocks[t].wave_generation - for t in self.blocks.index_set() - ) - ) - - csp_techs = [i for i in ['tower', 'trough'] if i in self.power_sources.keys()] - for tech in csp_techs: - tb = self.power_sources[tech].dispatch.blocks - objective = pyomo.Expression( - expr=sum( - - (1/self.blocks[t].time_weighting_factor) - * ( - ( - tb[t].cost_per_field_generation - * tb[t].receiver_thermal_power - * tb[t].time_duration - ) - + ( - tb[t].cost_per_field_start - * tb[t].incur_field_start - ) - + ( - tb[t].cost_per_cycle_generation - * tb[t].cycle_generation - * tb[t].time_duration - ) - + ( - tb[t].cost_per_cycle_start - * tb[t].incur_cycle_start - ) - + ( - tb[t].cost_per_change_thermal_input - * tb[t].cycle_thermal_ramp - ) - ) - for t in self.blocks.index_set() + def gross_profit_objective_rule(m) -> float: + obj = 0. + for tech in self.power_sources.keys(): + # Create the max_gross_profit_objective within each of the technology + # dispatch classes. + self.power_sources[tech]._dispatch.max_gross_profit_objective(self.blocks) + # Copy the technology objective to the pyomo model. + setattr( + m, + tech + "_obj", + getattr(self.power_sources[tech]._dispatch, tech + "_obj") ) - ) - setattr(self.model, tech + "_obj", objective) + # TODO: Does the objective really need to be stored on the self.model object? + # Trying to grab the attribute 'tech + "_obj"' from the dispatch classes + # themselves doesn't seem to work within pyomo, e.g.: + # `getattr(self.power_sources[tech]._dispatch, tech + "_obj")`. If we could avoid + # this, then the above `setattr` would not be needed. - if 'battery' in self.power_sources.keys(): - def battery_profit_objective_rule(m): - objective = 0 - tb = self.power_sources['battery'].dispatch.blocks - objective += sum( - - (1/self.blocks[t].time_weighting_factor) - * tb[t].time_duration - * ( - tb[t].cost_per_charge - * self.blocks[t].battery_charge - + tb[t].cost_per_discharge - * self.blocks[t].battery_discharge - ) - for t in self.blocks.index_set() - ) - tb = self.power_sources['battery'].dispatch - if tb.options.include_lifecycle_count: - objective -= tb.model.lifecycle_cost * sum(tb.model.lifecycles) - return objective - self.model.battery_obj = pyomo.Expression(rule=battery_profit_objective_rule) - - def gross_profit_objective_rule(m): - obj = 0 - for tech in self.power_sources.keys(): + # Assemble the objective as a linear summation. obj += getattr(m, tech + "_obj") return obj diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 322098a65..dfcdab718 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -1,5 +1,5 @@ from typing import Union -from pyomo.environ import ConcreteModel, Set +from pyomo.environ import ConcreteModel, Expression, Set import PySAM.Pvsamv1 as Pvsam import PySAM.Pvwattsv8 as Pvwatts @@ -9,6 +9,7 @@ class PvDispatch(PowerSourceDispatch): + pv_obj: Union[Expression, float] _system_model: Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8] _financial_model: FinancialModelType """ @@ -35,3 +36,14 @@ def update_time_series_parameters(self, start_time: int): # zero out any negative load self.available_generation = [max(0, i) for i in self.available_generation] + + def max_gross_profit_objective(self, blocks): + self.pv_obj = Expression( + expr=sum( + - (1/blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].pv_generation + for t in blocks.index_set() + ) + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index cec09afd5..773e0d043 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -1,10 +1,12 @@ -from pyomo.environ import ConcreteModel, Set +from typing import Union +from pyomo.environ import ConcreteModel, Expression, Set from hopp.simulation.technologies.financial import FinancialModelType from hopp.simulation.technologies.dispatch.power_sources.csp_dispatch import CspDispatch class TowerDispatch(CspDispatch): + tower_obj: Union[Expression, float] _system_model: None _financial_model: FinancialModelType """ @@ -52,3 +54,34 @@ def update_initial_conditions(self): ): self.initial_receiver_startup_inventory = self.receiver_required_startup_energy + def max_gross_profit_objective(self, blocks): + self.tower_obj = Expression( + expr=sum( + - (1/blocks[t].time_weighting_factor) + * ( + ( + self.blocks[t].cost_per_field_generation + * self.blocks[t].receiver_thermal_power + * self.blocks[t].time_duration + ) + + ( + self.blocks[t].cost_per_field_start + * self.blocks[t].incur_field_start + ) + + ( + self.blocks[t].cost_per_cycle_generation + * self.blocks[t].cycle_generation + * self.blocks[t].time_duration + ) + + ( + self.blocks[t].cost_per_cycle_start + * self.blocks[t].incur_cycle_start + ) + + ( + self.blocks[t].cost_per_change_thermal_input + * self.blocks[t].cycle_thermal_ramp + ) + ) + for t in blocks.index_set() + ) + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index c3df3f377..27ae9637f 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -1,10 +1,12 @@ -from pyomo.environ import ConcreteModel, Set +from typing import Union +from pyomo.environ import ConcreteModel, Expression, Set from hopp.simulation.technologies.financial import FinancialModelType from hopp.simulation.technologies.dispatch.power_sources.csp_dispatch import CspDispatch class TroughDispatch(CspDispatch): + trough_obj: Union[Expression, float] _system_model: None _financial_model: FinancialModelType """ @@ -36,3 +38,34 @@ def update_initial_conditions(self): "result in persistent receiver start-up." ) + def max_gross_profit_objective(self, blocks): + self.trough_obj = Expression( + expr=sum( + - (1/blocks[t].time_weighting_factor) + * ( + ( + self.blocks[t].cost_per_field_generation + * self.blocks[t].receiver_thermal_power + * self.blocks[t].time_duration + ) + + ( + self.blocks[t].cost_per_field_start + * self.blocks[t].incur_field_start + ) + + ( + self.blocks[t].cost_per_cycle_generation + * self.blocks[t].cycle_generation + * self.blocks[t].time_duration + ) + + ( + self.blocks[t].cost_per_cycle_start + * self.blocks[t].incur_cycle_start + ) + + ( + self.blocks[t].cost_per_change_thermal_input + * self.blocks[t].cycle_thermal_ramp + ) + ) + for t in blocks.index_set() + ) + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index aa20072da..6f3e82772 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -1,4 +1,5 @@ -from pyomo.environ import ConcreteModel, Set +from typing import Union +from pyomo.environ import ConcreteModel, Expression, Set import PySAM.MhkWave as MhkWave @@ -9,6 +10,7 @@ class WaveDispatch(PowerSourceDispatch): + wave_obj: Union[Expression, float] _system_model: MhkWave.MhkWave _financial_model: FinancialModelType """ @@ -30,3 +32,13 @@ def __init__( block_set_name=block_set_name, ) + def max_gross_profit_objective(self, blocks): + self.wave_obj = Expression( + expr=sum( + - (1/blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].wave_generation + for t in blocks.index_set() + ) + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 79b92bcd6..c09bb65c9 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -1,5 +1,5 @@ from typing import Union, TYPE_CHECKING -from pyomo.environ import ConcreteModel, Set +from pyomo.environ import ConcreteModel, Expression, Set import PySAM.Windpower as Windpower @@ -13,6 +13,7 @@ class WindDispatch(PowerSourceDispatch): + wind_obj: Union[Expression, float] _system_model: Union[Windpower.Windpower,"Floris"] _financial_model: FinancialModelType """ @@ -34,3 +35,13 @@ def __init__( block_set_name=block_set_name, ) + def max_gross_profit_objective(self, blocks): + self.wind_obj = Expression( + expr=sum( + - (1/blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].wind_generation + for t in blocks.index_set() + ) + ) diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 69b43a7fe..9659ef824 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -53,6 +53,24 @@ def dispatch_block_rule(self, storage): # Ports self._create_storage_port(storage) + def max_gross_profit_objective(self, blocks): + def battery_profit_objective_rule(m): + objective = 0 + objective += sum( + - (1/blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * ( + self.blocks[t].cost_per_charge + * blocks[t].battery_charge + + self.blocks[t].cost_per_discharge + * blocks[t].battery_discharge + ) + for t in blocks.index_set() + ) + if self.options.include_lifecycle_count: + objective -= self.model.lifecycle_cost * sum(self.model.lifecycles) + return objective + self.battery_obj = pyomo.Expression(rule=battery_profit_objective_rule) def _create_storage_parameters(self, storage): ################################## # Parameters # From 8482bc13211ad03a862275b84ada965d289cf5bd Mon Sep 17 00:00:00 2001 From: bayc Date: Fri, 5 Apr 2024 16:44:42 -0600 Subject: [PATCH 14/27] move the min_operating_cost_objective from hybrid_dispatch into respective technology dispatches --- .../technologies/dispatch/grid_dispatch.py | 23 ++++ .../technologies/dispatch/hybrid_dispatch.py | 102 ++---------------- .../dispatch/power_sources/pv_dispatch.py | 9 ++ .../dispatch/power_sources/tower_dispatch.py | 24 +++++ .../dispatch/power_sources/trough_dispatch.py | 24 +++++ .../dispatch/power_sources/wave_dispatch.py | 9 ++ .../dispatch/power_sources/wind_dispatch.py | 9 ++ .../power_storage/power_storage_dispatch.py | 18 ++++ 8 files changed, 126 insertions(+), 92 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index f41043b29..7efa069f7 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -58,6 +58,29 @@ def max_gross_profit_objective(self, blocks): for t in blocks.index_set() ) ) + + def min_operating_cost_objective(self, blocks): + self.grid_obj = sum( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * self.blocks[t].electricity_sell_price + * ( + self.blocks[t].generation_transmission_limit + - blocks[t].electricity_sold + ) + + ( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * self.blocks[t].electricity_purchase_price + * blocks[t].electricity_purchased + ) + + ( + self.blocks[t].epsilon + * self.blocks[t].is_generating + ) + for t in blocks.index_set() + ) + @staticmethod def _create_grid_parameters(grid): ################################## diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index 6d7dedf39..7b86a57ef 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -311,99 +311,17 @@ def gross_profit_objective_rule(m) -> float: def create_min_operating_cost_objective(self): self._delete_objective() - def operating_cost_objective_rule(m): - objective = 0.0 + def operating_cost_objective_rule(m) -> float: + obj = 0. for tech in self.power_sources.keys(): - if tech == 'grid': - tb = self.power_sources[tech].dispatch.blocks - objective += sum( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * tb[t].electricity_sell_price - * ( - tb[t].generation_transmission_limit - - self.blocks[t].electricity_sold - ) - + ( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * tb[t].electricity_purchase_price - * self.blocks[t].electricity_purchased - ) - + ( - tb[t].epsilon - * tb[t].is_generating - ) - for t in self.blocks.index_set() - ) - elif tech == 'pv': - tb = self.power_sources[tech].dispatch.blocks - objective += sum( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * tb[t].cost_per_generation - * self.blocks[t].pv_generation - for t in self.blocks.index_set() - ) - elif tech == 'wind': - tb = self.power_sources[tech].dispatch.blocks - objective += sum( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * tb[t].cost_per_generation - * self.blocks[t].wind_generation - for t in self.blocks.index_set() - ) - elif tech == 'wave': - tb = self.power_sources[tech].dispatch.blocks - objective += sum( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * tb[t].cost_per_generation - * self.blocks[t].wave_generation - for t in self.blocks.index_set() - ) - elif tech == 'tower' or tech == 'trough': - tb = self.power_sources[tech].dispatch.blocks - objective += sum( - self.blocks[t].time_weighting_factor - * ( - tb[t].cost_per_field_start - * tb[t].incur_field_start - - ( - tb[t].cost_per_field_generation - * tb[t].receiver_thermal_power - * tb[t].time_duration - ) # Trying to incentivize TES generation - + ( - tb[t].cost_per_cycle_generation - * tb[t].cycle_generation - * tb[t].time_duration - ) - + tb[t].cost_per_cycle_start - * tb[t].incur_cycle_start - + tb[t].cost_per_change_thermal_input - * tb[t].cycle_thermal_ramp - ) - for t in self.blocks.index_set() - ) - elif tech == 'battery': - tb = self.power_sources[tech].dispatch.blocks - objective += sum( - self.blocks[t].time_weighting_factor - * tb[t].time_duration - * ( - tb[t].cost_per_discharge - * self.blocks[t].battery_discharge - - tb[t].cost_per_charge - * self.blocks[t].battery_charge - ) # Try to incentivize battery charging - for t in self.blocks.index_set() - ) - tb = self.power_sources['battery'].dispatch - if tb.options.include_lifecycle_count: - objective += tb.model.lifecycle_cost * tb.model.lifecycles - return objective + # Create the min_operating_cost_objective within each of the technology + # dispatch classes. + self.power_sources[tech]._dispatch.min_operating_cost_objective(self.blocks) + + # Assemble the objective as a linear summation. + obj += getattr(self.power_sources[tech]._dispatch, tech + "_obj") + + return obj self.model.objective = pyomo.Objective( rule=operating_cost_objective_rule, diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index dfcdab718..dbea70c71 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -47,3 +47,12 @@ def max_gross_profit_objective(self, blocks): for t in blocks.index_set() ) ) + + def min_operating_cost_objective(self, blocks): + self.pv_obj = sum( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].pv_generation + for t in blocks.index_set() + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index 773e0d043..c20c7c9db 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -85,3 +85,27 @@ def max_gross_profit_objective(self, blocks): for t in blocks.index_set() ) ) + + def min_operating_cost_objective(self, blocks): + self.tower_obj = sum( + blocks[t].time_weighting_factor + * ( + self.blocks[t].cost_per_field_start + * self.blocks[t].incur_field_start + - ( + self.blocks[t].cost_per_field_generation + * self.blocks[t].receiver_thermal_power + * self.blocks[t].time_duration + ) # Trying to incentivize TES generation + + ( + self.blocks[t].cost_per_cycle_generation + * self.blocks[t].cycle_generation + * self.blocks[t].time_duration + ) + + self.blocks[t].cost_per_cycle_start + * self.blocks[t].incur_cycle_start + + self.blocks[t].cost_per_change_thermal_input + * self.blocks[t].cycle_thermal_ramp + ) + for t in blocks.index_set() + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index 27ae9637f..b5494d485 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -69,3 +69,27 @@ def max_gross_profit_objective(self, blocks): for t in blocks.index_set() ) ) + + def min_operating_cost_objective(self, blocks): + self.trough_obj = sum( + blocks[t].time_weighting_factor + * ( + self.blocks[t].cost_per_field_start + * self.blocks[t].incur_field_start + - ( + self.blocks[t].cost_per_field_generation + * self.blocks[t].receiver_thermal_power + * self.blocks[t].time_duration + ) # Trying to incentivize TES generation + + ( + self.blocks[t].cost_per_cycle_generation + * self.blocks[t].cycle_generation + * self.blocks[t].time_duration + ) + + self.blocks[t].cost_per_cycle_start + * self.blocks[t].incur_cycle_start + + self.blocks[t].cost_per_change_thermal_input + * self.blocks[t].cycle_thermal_ramp + ) + for t in blocks.index_set() + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index 6f3e82772..c03e94404 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -42,3 +42,12 @@ def max_gross_profit_objective(self, blocks): for t in blocks.index_set() ) ) + + def min_operating_cost_objective(self, blocks): + self.wave_obj = sum( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].wave_generation + for t in blocks.index_set() + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index c09bb65c9..75379c2f6 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -45,3 +45,12 @@ def max_gross_profit_objective(self, blocks): for t in blocks.index_set() ) ) + + def min_operating_cost_objective(self, blocks): + self.wind_obj = sum( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].wind_generation + for t in blocks.index_set() + ) diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 9659ef824..0b4ca222d 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -71,6 +71,24 @@ def battery_profit_objective_rule(m): objective -= self.model.lifecycle_cost * sum(self.model.lifecycles) return objective self.battery_obj = pyomo.Expression(rule=battery_profit_objective_rule) + + def min_operating_cost_objective(self, blocks): + objective = sum( + blocks[t].time_weighting_factor + * self.blocks[t].time_duration + * ( + self.blocks[t].cost_per_discharge + * blocks[t].battery_discharge + - self.blocks[t].cost_per_charge + * blocks[t].battery_charge + ) # Try to incentivize battery charging + for t in self.blocks.index_set() + ) + if self.options.include_lifecycle_count: + objective += self.model.lifecycle_cost * self.model.lifecycles + + self.battery_obj = objective + def _create_storage_parameters(self, storage): ################################## # Parameters # From ae0fcb6a51314a58a848de45d5ab90cd2e857c61 Mon Sep 17 00:00:00 2001 From: bayc Date: Fri, 5 Apr 2024 23:06:34 -0600 Subject: [PATCH 15/27] move the variable and port generation from hybrid_dispatch into respective technology dispatches --- .../technologies/dispatch/grid_dispatch.py | 33 ++++ .../technologies/dispatch/hybrid_dispatch.py | 178 +++--------------- .../dispatch/power_sources/pv_dispatch.py | 16 +- .../dispatch/power_sources/tower_dispatch.py | 27 ++- .../dispatch/power_sources/trough_dispatch.py | 27 ++- .../dispatch/power_sources/wave_dispatch.py | 16 +- .../dispatch/power_sources/wind_dispatch.py | 16 +- .../power_storage/power_storage_dispatch.py | 24 +++ 8 files changed, 177 insertions(+), 160 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index 7efa069f7..b96db7f8b 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -81,6 +81,39 @@ def min_operating_cost_objective(self, blocks): for t in blocks.index_set() ) + def _create_variables(self, hybrid): + hybrid.system_generation = pyomo.Var( + doc="System generation [MW]", + domain=pyomo.NonNegativeReals, + units=u.MW, + ) + hybrid.system_load = pyomo.Var( + doc="System load [MW]", + domain=pyomo.NonNegativeReals, + units=u.MW, + ) + hybrid.electricity_sold = pyomo.Var( + doc="Electricity sold [MW]", + domain=pyomo.NonNegativeReals, + units=u.MW, + ) + hybrid.electricity_purchased = pyomo.Var( + doc="Electricity purchased [MW]", + domain=pyomo.NonNegativeReals, + units=u.MW, + ) + + def _create_port(self, hybrid): + hybrid.grid_port = Port( + initialize={ + 'system_generation': hybrid.system_generation, + 'system_load': hybrid.system_load, + 'electricity_sold': hybrid.electricity_sold, + 'electricity_purchased': hybrid.electricity_purchased, + } + ) + return hybrid.grid_port + @staticmethod def _create_grid_parameters(grid): ################################## diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index 7b86a57ef..45831966b 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -50,14 +50,7 @@ def dispatch_block_rule(self, hybrid, t): ################################## # Variables / Ports # ################################## - for tech in self.power_sources.keys(): - try: - getattr(self, "_create_" + tech + "_variables")(hybrid, t) - getattr(self, "_create_" + tech + "_port")(hybrid, t) - except AttributeError: - raise ValueError("'{}' is not supported in the hybrid dispatch model.".format(tech)) - except Exception as e: - raise RuntimeError("Error in setting up dispatch for {}: {}".format(tech, e)) + self._create_variables_and_ports(hybrid, t) ################################## # Constraints # ################################## @@ -78,153 +71,28 @@ def _create_parameters(hybrid): units=u.dimensionless, ) - def _create_pv_variables(self, hybrid, t): - hybrid.pv_generation = pyomo.Var( - doc="Power generation of photovoltaics [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - self.power_source_gen_vars[t].append(hybrid.pv_generation) - - def _create_pv_port(self, hybrid, t): - hybrid.pv_port = Port(initialize={'generation': hybrid.pv_generation}) - self.ports[t].append(hybrid.pv_port) - - def _create_wind_variables(self, hybrid, t): - hybrid.wind_generation = pyomo.Var( - doc="Power generation of wind turbines [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - self.power_source_gen_vars[t].append(hybrid.wind_generation) - - def _create_wind_port(self, hybrid, t): - hybrid.wind_port = Port(initialize={'generation': hybrid.wind_generation}) - self.ports[t].append(hybrid.wind_port) - - def _create_wave_variables(self, hybrid, t): - hybrid.wave_generation = pyomo.Var( - doc="Power generation of wave devices [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - self.power_source_gen_vars[t].append(hybrid.wave_generation) - - def _create_wave_port(self, hybrid, t): - hybrid.wave_port = Port(initialize={'generation': hybrid.wave_generation}) - self.ports[t].append(hybrid.wave_port) - - def _create_tower_variables(self, hybrid, t): - hybrid.tower_generation = pyomo.Var( - doc="Power generation of CSP tower [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - hybrid.tower_load = pyomo.Var( - doc="Load of CSP tower [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - self.power_source_gen_vars[t].append(hybrid.tower_generation) - self.load_vars[t].append(hybrid.tower_load) - - def _create_tower_port(self, hybrid, t): - hybrid.tower_port = Port( - initialize={ - 'cycle_generation': hybrid.tower_generation, - 'system_load': hybrid.tower_load, - } - ) - self.ports[t].append(hybrid.tower_port) - - def _create_trough_variables(self, hybrid, t): - hybrid.trough_generation = pyomo.Var( - doc="Power generation of CSP trough [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - hybrid.trough_load = pyomo.Var( - doc="Load of CSP trough [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - self.power_source_gen_vars[t].append(hybrid.trough_generation) - self.load_vars[t].append(hybrid.trough_load) - - def _create_trough_port(self, hybrid, t): - hybrid.trough_port = Port( - initialize={ - 'cycle_generation': hybrid.trough_generation, - 'system_load': hybrid.trough_load, - } - ) - self.ports[t].append(hybrid.trough_port) - - def _create_battery_variables(self, hybrid, t): - hybrid.battery_charge = pyomo.Var( - doc="Power charging the electric battery [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - hybrid.battery_discharge = pyomo.Var( - doc="Power discharging the electric battery [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - initialize=0.0, - ) - self.power_source_gen_vars[t].append(hybrid.battery_discharge) - self.load_vars[t].append(hybrid.battery_charge) - - def _create_battery_port(self, hybrid, t): - hybrid.battery_port = Port( - initialize={ - 'charge_power': hybrid.battery_charge, - 'discharge_power': hybrid.battery_discharge, - } - ) - self.ports[t].append(hybrid.battery_port) - - @staticmethod - def _create_grid_variables(hybrid, _): - hybrid.system_generation = pyomo.Var( - doc="System generation [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - ) - hybrid.system_load = pyomo.Var( - doc="System load [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - ) - hybrid.electricity_sold = pyomo.Var( - doc="Electricity sold [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - ) - hybrid.electricity_purchased = pyomo.Var( - doc="Electricity purchased [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW, - ) - - def _create_grid_port(self, hybrid, t): - hybrid.grid_port = Port( - initialize={ - 'system_generation': hybrid.system_generation, - 'system_load': hybrid.system_load, - 'electricity_sold': hybrid.electricity_sold, - 'electricity_purchased': hybrid.electricity_purchased, - } - ) - self.ports[t].append(hybrid.grid_port) + def _create_variables_and_ports(self, hybrid, t): + for tech in self.power_sources.keys(): + try: + # getattr(self, "_create_" + tech + "_variables")(hybrid, t) + # getattr(self, "_create_" + tech + "_port")(hybrid, t) + + if tech in ["battery", "tower", "trough"]: + gen_var, load_var = self.power_sources[tech]._dispatch._create_variables(hybrid) + self.power_source_gen_vars[t].append(gen_var) + self.load_vars[t].append(load_var) + elif tech in ["grid"]: + self.power_sources[tech]._dispatch._create_variables(hybrid) + else: + self.power_source_gen_vars[t].append( + self.power_sources[tech]._dispatch._create_variables(hybrid) + ) + + self.ports[t].append(self.power_sources[tech]._dispatch._create_port(hybrid)) + except AttributeError: + raise ValueError("'{}' is not supported in the hybrid dispatch model.".format(tech)) + except Exception as e: + raise RuntimeError("Error in setting up dispatch for {}: {}".format(tech, e)) def _create_grid_constraints(self, hybrid, t): hybrid.generation_total = pyomo.Constraint( diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index dbea70c71..d667804c6 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -1,5 +1,6 @@ from typing import Union -from pyomo.environ import ConcreteModel, Expression, Set +from pyomo.environ import ConcreteModel, Expression, NonNegativeReals, Set, units, Var +from pyomo.network import Port import PySAM.Pvsamv1 as Pvsam import PySAM.Pvwattsv8 as Pvwatts @@ -56,3 +57,16 @@ def min_operating_cost_objective(self, blocks): * blocks[t].pv_generation for t in blocks.index_set() ) + + def _create_variables(self, hybrid) -> Var: + hybrid.pv_generation = Var( + doc="Power generation of photovoltaics [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + return hybrid.pv_generation + + def _create_port(self, hybrid) -> Port: + hybrid.pv_port = Port(initialize={'generation': hybrid.pv_generation}) + return hybrid.pv_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index c20c7c9db..b9e5b0291 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -1,5 +1,6 @@ from typing import Union -from pyomo.environ import ConcreteModel, Expression, Set +from pyomo.environ import ConcreteModel, Expression, NonNegativeReals, Set, units, Var +from pyomo.network import Port from hopp.simulation.technologies.financial import FinancialModelType from hopp.simulation.technologies.dispatch.power_sources.csp_dispatch import CspDispatch @@ -109,3 +110,27 @@ def min_operating_cost_objective(self, blocks): ) for t in blocks.index_set() ) + + def _create_variables(self, hybrid): + hybrid.tower_generation = Var( + doc="Power generation of CSP tower [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + hybrid.tower_load = Var( + doc="Load of CSP tower [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + return hybrid.tower_generation, hybrid.tower_load + + def _create_port(self, hybrid): + hybrid.tower_port = Port( + initialize={ + 'cycle_generation': hybrid.tower_generation, + 'system_load': hybrid.tower_load, + } + ) + return hybrid.tower_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index b5494d485..9df2be7ad 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -1,5 +1,6 @@ from typing import Union -from pyomo.environ import ConcreteModel, Expression, Set +from pyomo.environ import ConcreteModel, Expression, NonNegativeReals, Set, units, Var +from pyomo.network import Port from hopp.simulation.technologies.financial import FinancialModelType from hopp.simulation.technologies.dispatch.power_sources.csp_dispatch import CspDispatch @@ -93,3 +94,27 @@ def min_operating_cost_objective(self, blocks): ) for t in blocks.index_set() ) + + def _create_variables(self, hybrid): + hybrid.trough_generation = Var( + doc="Power generation of CSP trough [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + hybrid.trough_load = Var( + doc="Load of CSP trough [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + return hybrid.trough_generation, hybrid.trough_load + + def _create_port(self, hybrid): + hybrid.trough_port = Port( + initialize={ + 'cycle_generation': hybrid.trough_generation, + 'system_load': hybrid.trough_load, + } + ) + return hybrid.trough_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index c03e94404..62ea8be99 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -1,5 +1,6 @@ from typing import Union -from pyomo.environ import ConcreteModel, Expression, Set +from pyomo.environ import ConcreteModel, Expression, NonNegativeReals, Set, units, Var +from pyomo.network import Port import PySAM.MhkWave as MhkWave @@ -51,3 +52,16 @@ def min_operating_cost_objective(self, blocks): * blocks[t].wave_generation for t in blocks.index_set() ) + + def _create_variables(self, hybrid) -> Var: + hybrid.wave_generation = Var( + doc="Power generation of wind turbines [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + return hybrid.wave_generation + + def _create_port(self, hybrid) -> Port: + hybrid.wave_port = Port(initialize={'generation': hybrid.wave_generation}) + return hybrid.wave_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 75379c2f6..2452459af 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -1,5 +1,6 @@ from typing import Union, TYPE_CHECKING -from pyomo.environ import ConcreteModel, Expression, Set +from pyomo.environ import ConcreteModel, Expression, NonNegativeReals, Set, units, Var +from pyomo.network import Port import PySAM.Windpower as Windpower @@ -54,3 +55,16 @@ def min_operating_cost_objective(self, blocks): * blocks[t].wind_generation for t in blocks.index_set() ) + + def _create_variables(self, hybrid) -> Var: + hybrid.wind_generation = Var( + doc="Power generation of wind turbines [MW]", + domain=NonNegativeReals, + units=units.MW, + initialize=0.0, + ) + return hybrid.wind_generation + + def _create_port(self, hybrid) -> Port: + hybrid.wind_port = Port(initialize={'generation': hybrid.wind_generation}) + return hybrid.wind_port diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 0b4ca222d..9868ce6c6 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -89,6 +89,30 @@ def min_operating_cost_objective(self, blocks): self.battery_obj = objective + def _create_variables(self, hybrid): + hybrid.battery_charge = pyomo.Var( + doc="Power charging the electric battery [MW]", + domain=pyomo.NonNegativeReals, + units=u.MW, + initialize=0.0, + ) + hybrid.battery_discharge = pyomo.Var( + doc="Power discharging the electric battery [MW]", + domain=pyomo.NonNegativeReals, + units=u.MW, + initialize=0.0, + ) + return hybrid.battery_discharge, hybrid.battery_charge + + def _create_port(self, hybrid): + hybrid.battery_port = Port( + initialize={ + 'charge_power': hybrid.battery_charge, + 'discharge_power': hybrid.battery_discharge, + } + ) + return hybrid.battery_port + def _create_storage_parameters(self, storage): ################################## # Parameters # From e615145d3f3cb06485bc904760f7b5a7484a8b9b Mon Sep 17 00:00:00 2001 From: bayc Date: Thu, 11 Apr 2024 21:52:23 -0600 Subject: [PATCH 16/27] generalize the technology objective attributes to just be obj --- hopp/simulation/technologies/dispatch/grid_dispatch.py | 4 ++-- hopp/simulation/technologies/dispatch/hybrid_dispatch.py | 8 ++++---- .../technologies/dispatch/power_sources/pv_dispatch.py | 4 ++-- .../technologies/dispatch/power_sources/tower_dispatch.py | 4 ++-- .../dispatch/power_sources/trough_dispatch.py | 4 ++-- .../technologies/dispatch/power_sources/wave_dispatch.py | 4 ++-- .../technologies/dispatch/power_sources/wind_dispatch.py | 4 ++-- .../dispatch/power_storage/power_storage_dispatch.py | 4 ++-- 8 files changed, 18 insertions(+), 18 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index b96db7f8b..3fcab6048 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -43,7 +43,7 @@ def dispatch_block_rule(self, grid): self._create_grid_ports(grid) def max_gross_profit_objective(self, blocks): - self.grid_obj = pyomo.Expression( + self.obj = pyomo.Expression( expr=sum( blocks[t].time_weighting_factor * self.blocks[t].time_duration @@ -60,7 +60,7 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): - self.grid_obj = sum( + self.obj = sum( blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].electricity_sell_price diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index 45831966b..86cb1eba8 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -160,12 +160,12 @@ def gross_profit_objective_rule(m) -> float: setattr( m, tech + "_obj", - getattr(self.power_sources[tech]._dispatch, tech + "_obj") + self.power_sources[tech]._dispatch.obj ) # TODO: Does the objective really need to be stored on the self.model object? - # Trying to grab the attribute 'tech + "_obj"' from the dispatch classes + # Trying to grab the attribute 'obj' from the dispatch classes # themselves doesn't seem to work within pyomo, e.g.: - # `getattr(self.power_sources[tech]._dispatch, tech + "_obj")`. If we could avoid + # `getattr(self.power_sources[tech]._dispatch, "obj")`. If we could avoid # this, then the above `setattr` would not be needed. # Assemble the objective as a linear summation. @@ -187,7 +187,7 @@ def operating_cost_objective_rule(m) -> float: self.power_sources[tech]._dispatch.min_operating_cost_objective(self.blocks) # Assemble the objective as a linear summation. - obj += getattr(self.power_sources[tech]._dispatch, tech + "_obj") + obj += self.power_sources[tech]._dispatch.obj return obj diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index d667804c6..8e504365f 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -39,7 +39,7 @@ def update_time_series_parameters(self, start_time: int): self.available_generation = [max(0, i) for i in self.available_generation] def max_gross_profit_objective(self, blocks): - self.pv_obj = Expression( + self.obj = Expression( expr=sum( - (1/blocks[t].time_weighting_factor) * self.blocks[t].time_duration @@ -50,7 +50,7 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): - self.pv_obj = sum( + self.obj = sum( blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index b9e5b0291..19b3885f8 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -56,7 +56,7 @@ def update_initial_conditions(self): self.initial_receiver_startup_inventory = self.receiver_required_startup_energy def max_gross_profit_objective(self, blocks): - self.tower_obj = Expression( + self.obj = Expression( expr=sum( - (1/blocks[t].time_weighting_factor) * ( @@ -88,7 +88,7 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): - self.tower_obj = sum( + self.obj = sum( blocks[t].time_weighting_factor * ( self.blocks[t].cost_per_field_start diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index 9df2be7ad..5ac9224d6 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -40,7 +40,7 @@ def update_initial_conditions(self): ) def max_gross_profit_objective(self, blocks): - self.trough_obj = Expression( + self.obj = Expression( expr=sum( - (1/blocks[t].time_weighting_factor) * ( @@ -72,7 +72,7 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): - self.trough_obj = sum( + self.obj = sum( blocks[t].time_weighting_factor * ( self.blocks[t].cost_per_field_start diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index 62ea8be99..0a5bd137f 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -34,7 +34,7 @@ def __init__( ) def max_gross_profit_objective(self, blocks): - self.wave_obj = Expression( + self.obj = Expression( expr=sum( - (1/blocks[t].time_weighting_factor) * self.blocks[t].time_duration @@ -45,7 +45,7 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): - self.wave_obj = sum( + self.obj = sum( blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 2452459af..7bff407c2 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -37,7 +37,7 @@ def __init__( ) def max_gross_profit_objective(self, blocks): - self.wind_obj = Expression( + self.obj = Expression( expr=sum( - (1/blocks[t].time_weighting_factor) * self.blocks[t].time_duration @@ -48,7 +48,7 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): - self.wind_obj = sum( + self.obj = sum( blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 9868ce6c6..bd3bfb8a0 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -70,7 +70,7 @@ def battery_profit_objective_rule(m): if self.options.include_lifecycle_count: objective -= self.model.lifecycle_cost * sum(self.model.lifecycles) return objective - self.battery_obj = pyomo.Expression(rule=battery_profit_objective_rule) + self.obj = pyomo.Expression(rule=battery_profit_objective_rule) def min_operating_cost_objective(self, blocks): objective = sum( @@ -87,7 +87,7 @@ def min_operating_cost_objective(self, blocks): if self.options.include_lifecycle_count: objective += self.model.lifecycle_cost * self.model.lifecycles - self.battery_obj = objective + self.obj = objective def _create_variables(self, hybrid): hybrid.battery_charge = pyomo.Var( From 2d98df76926196db8361ccb2e79ae8268946bdd0 Mon Sep 17 00:00:00 2001 From: bayc Date: Thu, 11 Apr 2024 21:55:35 -0600 Subject: [PATCH 17/27] move grid constraint to grid_dispatch and update hybrid constraints --- .../technologies/dispatch/grid_dispatch.py | 6 ++++++ .../technologies/dispatch/hybrid_dispatch.py | 15 ++++++++------- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index 3fcab6048..559d3ed1b 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -114,6 +114,12 @@ def _create_port(self, hybrid): ) return hybrid.grid_port + def _create_constraints(self, hybrid, t): + hybrid.generation_total = pyomo.Constraint( + doc="hybrid system generation total", + rule=hybrid.system_generation == sum(self.power_source_gen_vars[t]), + ) + @staticmethod def _create_grid_parameters(grid): ################################## diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index 86cb1eba8..a01992502 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -54,12 +54,7 @@ def dispatch_block_rule(self, hybrid, t): ################################## # Constraints # ################################## - self._create_grid_constraints(hybrid, t) - if 'battery' in self.power_sources.keys(): - if self.options.pv_charging_only: - self._create_pv_battery_limitation(hybrid) - elif not self.options.grid_charging: - self._create_grid_battery_limitation(hybrid) + self._create_hybrid_constraints(hybrid, t) @staticmethod def _create_parameters(hybrid): @@ -94,7 +89,7 @@ def _create_variables_and_ports(self, hybrid, t): except Exception as e: raise RuntimeError("Error in setting up dispatch for {}: {}".format(tech, e)) - def _create_grid_constraints(self, hybrid, t): + def _create_hybrid_constraints(self, hybrid, t): hybrid.generation_total = pyomo.Constraint( doc="hybrid system generation total", rule=hybrid.system_generation == sum(self.power_source_gen_vars[t]), @@ -105,6 +100,12 @@ def _create_grid_constraints(self, hybrid, t): rule=hybrid.system_load == sum(self.load_vars[t]), ) + if 'battery' in self.power_sources.keys(): + if self.options.pv_charging_only: + self._create_pv_battery_limitation(hybrid) + elif not self.options.grid_charging: + self._create_grid_battery_limitation(hybrid) + @staticmethod def _create_grid_battery_limitation(hybrid): hybrid.no_grid_battery_charge = pyomo.Constraint( From 702a7ddcaabf40525c3f8b41d05b51dff32964f0 Mon Sep 17 00:00:00 2001 From: bayc Date: Thu, 11 Apr 2024 21:58:00 -0600 Subject: [PATCH 18/27] generalize variable creation to eliminate if/else in hybrid_dispatch --- .../technologies/dispatch/grid_dispatch.py | 2 ++ .../technologies/dispatch/hybrid_dispatch.py | 17 +++-------------- .../dispatch/power_sources/pv_dispatch.py | 6 +++--- .../dispatch/power_sources/wave_dispatch.py | 6 +++--- .../dispatch/power_sources/wind_dispatch.py | 6 +++--- 5 files changed, 14 insertions(+), 23 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index 559d3ed1b..492dbdde3 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -103,6 +103,8 @@ def _create_variables(self, hybrid): units=u.MW, ) + return 0, 0 + def _create_port(self, hybrid): hybrid.grid_port = Port( initialize={ diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index a01992502..f95933189 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -69,20 +69,9 @@ def _create_parameters(hybrid): def _create_variables_and_ports(self, hybrid, t): for tech in self.power_sources.keys(): try: - # getattr(self, "_create_" + tech + "_variables")(hybrid, t) - # getattr(self, "_create_" + tech + "_port")(hybrid, t) - - if tech in ["battery", "tower", "trough"]: - gen_var, load_var = self.power_sources[tech]._dispatch._create_variables(hybrid) - self.power_source_gen_vars[t].append(gen_var) - self.load_vars[t].append(load_var) - elif tech in ["grid"]: - self.power_sources[tech]._dispatch._create_variables(hybrid) - else: - self.power_source_gen_vars[t].append( - self.power_sources[tech]._dispatch._create_variables(hybrid) - ) - + gen_var, load_var = self.power_sources[tech]._dispatch._create_variables(hybrid) + self.power_source_gen_vars[t].append(gen_var) + self.load_vars[t].append(load_var) self.ports[t].append(self.power_sources[tech]._dispatch._create_port(hybrid)) except AttributeError: raise ValueError("'{}' is not supported in the hybrid dispatch model.".format(tech)) diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 8e504365f..49ffa9f00 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -58,15 +58,15 @@ def min_operating_cost_objective(self, blocks): for t in blocks.index_set() ) - def _create_variables(self, hybrid) -> Var: + def _create_variables(self, hybrid): hybrid.pv_generation = Var( doc="Power generation of photovoltaics [MW]", domain=NonNegativeReals, units=units.MW, initialize=0.0, ) - return hybrid.pv_generation + return hybrid.pv_generation, 0 - def _create_port(self, hybrid) -> Port: + def _create_port(self, hybrid): hybrid.pv_port = Port(initialize={'generation': hybrid.pv_generation}) return hybrid.pv_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index 0a5bd137f..44b9c7282 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -53,15 +53,15 @@ def min_operating_cost_objective(self, blocks): for t in blocks.index_set() ) - def _create_variables(self, hybrid) -> Var: + def _create_variables(self, hybrid): hybrid.wave_generation = Var( doc="Power generation of wind turbines [MW]", domain=NonNegativeReals, units=units.MW, initialize=0.0, ) - return hybrid.wave_generation + return hybrid.wave_generation, 0 - def _create_port(self, hybrid) -> Port: + def _create_port(self, hybrid): hybrid.wave_port = Port(initialize={'generation': hybrid.wave_generation}) return hybrid.wave_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 7bff407c2..1f9c7bd02 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -56,15 +56,15 @@ def min_operating_cost_objective(self, blocks): for t in blocks.index_set() ) - def _create_variables(self, hybrid) -> Var: + def _create_variables(self, hybrid): hybrid.wind_generation = Var( doc="Power generation of wind turbines [MW]", domain=NonNegativeReals, units=units.MW, initialize=0.0, ) - return hybrid.wind_generation + return hybrid.wind_generation, 0 - def _create_port(self, hybrid) -> Port: + def _create_port(self, hybrid): hybrid.wind_port = Port(initialize={'generation': hybrid.wind_generation}) return hybrid.wind_port From 6bf65815116d5ab38ad9ee45652f9daea18a8938 Mon Sep 17 00:00:00 2001 From: bayc Date: Thu, 11 Apr 2024 21:58:41 -0600 Subject: [PATCH 19/27] add variable and port creation methods to base power generation dispatch class --- .../dispatch/power_sources/power_source_dispatch.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py index baf1edfc8..aa248e05c 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py @@ -79,6 +79,12 @@ def update_time_series_parameters(self, start_time: int): f"length but has only {len(generation)}") self.available_generation = [gen_kw / 1e3 for gen_kw in horizon_gen] + def _create_variables(self, hyrbid): + raise NotImplemented("This function must be overridden for specific dispatch model") + + def _create_port(self, hyrbid): + raise NotImplemented("This function must be overridden for specific dispatch model") + @property def cost_per_generation(self) -> float: for t in self.blocks.index_set(): From 2bab8c930a8f7fdc5bb337e5a7ae3c8fa411dc49 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Tue, 16 Apr 2024 08:42:34 -0700 Subject: [PATCH 20/27] remove hardcoded values. sync financial and system models. --- .../power_storage/simple_battery_dispatch.py | 111 ++- tests/hopp/test_battery_dispatch.py | 2 +- tests/hopp/test_custom_financial.py | 2 + tests/hopp/test_dispatch.py | 12 +- tests/hopp/test_hybrid.py | 819 +++++++++++------- 5 files changed, 616 insertions(+), 330 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py index 23f96b12c..21fead6a9 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py @@ -2,66 +2,121 @@ from pyomo.environ import units as u import PySAM.BatteryStateful as BatteryModel -import PySAM.Singleowner as Singleowner -from hopp.simulation.technologies.dispatch.power_storage.power_storage_dispatch import PowerStorageDispatch +from hopp.simulation.technologies.dispatch.power_storage.power_storage_dispatch import ( + PowerStorageDispatch, +) from hopp.simulation.technologies.financial import FinancialModelType class SimpleBatteryDispatch(PowerStorageDispatch): - _system_model: BatteryModel.BatteryStateful - _financial_model: Singleowner.Singleowner """ + A dispatch class for simple battery operations. """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model: BatteryModel.BatteryStateful, - financial_model: FinancialModelType, - block_set_name: str, - dispatch_options): - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name, - dispatch_options=dispatch_options) + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: FinancialModelType, + block_set_name: str, + dispatch_options, + ): + """ + Initializes SimpleBatteryDispatch. + + Args: + pyomo_model (pyomo.ConcreteModel): The Pyomo model instance. + index_set (pyomo.Set): The Pyomo index set. + system_model (BatteryModel.BatteryStateful): The battery stateful model. + financial_model (FinancialModelType): The financial model type. + block_set_name (str): Name of the block set. + dispatch_options: Dispatch options. + + """ + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + dispatch_options=dispatch_options, + ) def initialize_parameters(self): + """ + Initializes parameters. + + """ if self.options.include_lifecycle_count: - self.lifecycle_cost = self.options.lifecycle_cost_per_kWh_cycle * self._system_model.value('nominal_energy') + self.lifecycle_cost = ( + self.options.lifecycle_cost_per_kWh_cycle + * self._system_model.value("nominal_energy") + ) - self.cost_per_charge = 0.75 # [$/MWh] - self.cost_per_discharge = 0.75 # [$/MWh] + self.cost_per_charge = self._financial_model.value("om_batt_variable_cost")[ + 0 + ] # [$/MWh] + self.cost_per_discharge = self._financial_model.value("om_batt_variable_cost")[ + 0 + ] # [$/MWh] self.minimum_power = 0.0 # FIXME: Change C_rate call to user set system_capacity_kw # self.maximum_power = self._system_model.value('nominal_energy') * self._system_model.value('C_rate') / 1e3 self.maximum_power = self._financial_model.value("system_capacity") / 1e3 - self.minimum_soc = self._system_model.value('minimum_SOC') - self.maximum_soc = self._system_model.value('maximum_SOC') - self.initial_soc = self._system_model.value('initial_SOC') + self.minimum_soc = self._system_model.value("minimum_SOC") + self.maximum_soc = self._system_model.value("maximum_SOC") + self.initial_soc = self._system_model.value("initial_SOC") self._set_control_mode() self._set_model_specific_parameters() def _set_control_mode(self): + """ + Sets control mode. + + """ if isinstance(self._system_model, BatteryModel.BatteryStateful): self._system_model.value("control_mode", 1.0) # Power control - self._system_model.value("input_power", 0.) + self._system_model.value("input_power", 0.0) self.control_variable = "input_power" - def _set_model_specific_parameters(self): - self.round_trip_efficiency = 88.0 # Including converter efficiency - self.capacity = self._system_model.value('nominal_energy') / 1e3 # [MWh] + def _set_model_specific_parameters(self, round_trip_efficiency=88.0): + """ + Sets model-specific parameters. + + Args: + round_trip_efficiency (float, optional): The round-trip efficiency including converter efficiency. + Defaults to 88.0, which includes converter efficiency. + + """ + self.round_trip_efficiency = ( + round_trip_efficiency # Including converter efficiency + ) + self.capacity = self._system_model.value("nominal_energy") / 1e3 # [MWh] def update_time_series_parameters(self, start_time: int): + """ + Updates time series parameters. + + Args: + start_time (int): The start time. + + """ # TODO: provide more control self.time_duration = [1.0] * len(self.blocks.index_set()) def update_dispatch_initial_soc(self, initial_soc: float = None): + """ + Updates dispatch initial state of charge (SOC). + + Args: + initial_soc (float, optional): Initial state of charge. Defaults to None. + + """ if initial_soc is not None: self._system_model.value("initial_SOC", initial_soc) self._system_model.setup() # TODO: Do I need to re-setup stateful battery? - self.initial_soc = self._system_model.value('SOC') + self.initial_soc = self._system_model.value("SOC") diff --git a/tests/hopp/test_battery_dispatch.py b/tests/hopp/test_battery_dispatch.py index f21251f64..72edc3317 100644 --- a/tests/hopp/test_battery_dispatch.py +++ b/tests/hopp/test_battery_dispatch.py @@ -28,7 +28,7 @@ 'om_production': [2], 'om_capacity': (0,), 'om_batt_fixed_cost': 0, - 'om_batt_variable_cost': [0], + 'om_batt_variable_cost': [0.75], 'om_batt_capacity_cost': 0, 'om_batt_replacement_cost': [0], 'om_replacement_cost_escal': 0, diff --git a/tests/hopp/test_custom_financial.py b/tests/hopp/test_custom_financial.py index 44818b946..6fc87b2a8 100644 --- a/tests/hopp/test_custom_financial.py +++ b/tests/hopp/test_custom_financial.py @@ -302,6 +302,7 @@ def test_hybrid_simple_pv_with_wind_storage_dispatch(site): hybrid_plant = hi.system hybrid_plant.layout.plot() hybrid_plant.battery.dispatch.lifecycle_cost_per_kWh_cycle = 0.01 + hybrid_plant.battery._financial_model.om_batt_variable_cost = [0.75] hybrid_plant.simulate() @@ -391,6 +392,7 @@ def test_hybrid_detailed_pv_with_wind_storage_dispatch(site): hybrid_plant = hi.system hybrid_plant.layout.plot() hybrid_plant.battery.dispatch.lifecycle_cost_per_kWh_cycle = 0.01 + hybrid_plant.battery._financial_model.om_batt_variable_cost = [0.75] hybrid_plant.simulate() diff --git a/tests/hopp/test_dispatch.py b/tests/hopp/test_dispatch.py index b42c0c413..dc55f47b6 100644 --- a/tests/hopp/test_dispatch.py +++ b/tests/hopp/test_dispatch.py @@ -483,7 +483,7 @@ def create_test_objective_rule(m): def test_simple_battery_dispatch(site): - expected_objective = 28957.15 + expected_objective = 29678.62 dispatch_n_look_ahead = 48 config = BatteryConfig.from_dict(technologies['battery']) @@ -547,7 +547,7 @@ def create_test_objective_rule(m): def test_simple_battery_dispatch_lifecycle_count(site): - expected_objective = 23657 + expected_objective = 24378.6 expected_lifecycles = [0.75048, 1.50096] dispatch_n_look_ahead = 48 @@ -612,7 +612,7 @@ def create_test_objective_rule(m): def test_detailed_battery_dispatch(site): - expected_objective = 33508 + expected_objective = 34505.9 expected_lifecycles = [0.14300, 0.22169] # TODO: McCormick error is large enough to make objective 50% higher than # the value of simple battery dispatch objective @@ -682,7 +682,7 @@ def create_test_objective_rule(m): def test_pv_wind_battery_hybrid_dispatch(site): - expected_objective = 38777.757 + expected_objective = 39005 wind_solar_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery', 'grid')} hopp_config = { @@ -784,7 +784,7 @@ def test_hybrid_dispatch_one_cycle_heuristic(site): def test_hybrid_solar_battery_dispatch(site): - expected_objective = 23474 + expected_objective = 24029 solar_battery_technologies = {k: technologies[k] for k in ('pv', 'battery', 'grid')} hopp_config = { @@ -929,7 +929,7 @@ def test_desired_schedule_dispatch(site): def test_simple_battery_dispatch_lifecycle_limit(site): - expected_objective = 7561 + expected_objective = 7882 max_lifecycle_per_day = 0.5 dispatch_n_look_ahead = 48 diff --git a/tests/hopp/test_hybrid.py b/tests/hopp/test_hybrid.py index 2933d4da3..3ec205332 100644 --- a/tests/hopp/test_hybrid.py +++ b/tests/hopp/test_hybrid.py @@ -8,8 +8,13 @@ from hopp.simulation import HoppInterface from hopp.simulation.technologies.sites import SiteInfo -from hopp.simulation.technologies.pv.detailed_pv_plant import DetailedPVPlant, DetailedPVConfig -from hopp.simulation.technologies.layout.pv_design_utils import size_electrical_parameters +from hopp.simulation.technologies.pv.detailed_pv_plant import ( + DetailedPVPlant, + DetailedPVConfig, +) +from hopp.simulation.technologies.layout.pv_design_utils import ( + size_electrical_parameters, +) from hopp.simulation.technologies.financial.mhk_cost_model import MHKCostModelInputs from tests.hopp.utils import create_default_site_info, DEFAULT_FIN_CONFIG from hopp import ROOT_DIR @@ -19,7 +24,9 @@ @fixture def hybrid_config(): """Loads the config YAML and updates site info to use resource files.""" - hybrid_config_path = ROOT_DIR.parent / "tests" / "hopp" / "inputs" / "hybrid_run.yaml" + hybrid_config_path = ( + ROOT_DIR.parent / "tests" / "hopp" / "inputs" / "hybrid_run.yaml" + ) hybrid_config = load_yaml(hybrid_config_path) return hybrid_config @@ -29,25 +36,23 @@ def hybrid_config(): def site(): return create_default_site_info() -wave_resource_file = ROOT_DIR.parent / "resource_files" / "wave" / "Wave_resource_timeseries.csv" + +wave_resource_file = ( + ROOT_DIR.parent / "resource_files" / "wave" / "Wave_resource_timeseries.csv" +) + @fixture def wavesite(): - data = { - "lat": 44.6899, - "lon": 124.1346, - "year": 2010, - "tz": -7 - } + data = {"lat": 44.6899, "lon": 124.1346, "year": 2010, "tz": -7} return SiteInfo( - data, - wave_resource_file=wave_resource_file, - solar=False, - wind=False, - wave=True + data, wave_resource_file=wave_resource_file, solar=False, wind=False, wave=True ) -mhk_yaml_path = ROOT_DIR.parent / "tests" / "hopp" / "inputs" / "wave" / "wave_device.yaml" + +mhk_yaml_path = ( + ROOT_DIR.parent / "tests" / "hopp" / "inputs" / "wave" / "wave_device.yaml" +) mhk_config = load_yaml(mhk_yaml_path) interconnection_size_kw = 15000 @@ -56,63 +61,158 @@ def wavesite(): batt_kw = 5000 detailed_pv = { - 'tech_config': { - 'system_capacity_kw': pv_kw - }, - 'layout_params': { + "tech_config": {"system_capacity_kw": pv_kw}, + "layout_params": { "x_position": 0.5, "y_position": 0.5, "aspect_power": 0, "gcr": 0.5, "s_buffer": 2, - "x_buffer": 2 - } + "x_buffer": 2, + }, } # From a Cambium midcase BA10 2030 analysis (Jan 1 = 1): -capacity_credit_hours_of_year = [4604,4605,4606,4628,4629,4630,4652,4821,5157,5253, - 5254,5277,5278,5299,5300,5301,5302,5321,5323,5324, - 5325,5326,5327,5347,5348,5349,5350,5369,5370,5371, - 5372,5374,5395,5396,5397,5398,5419,5420,5421,5422, - 5443,5444,5445,5446,5467,5468,5469,5493,5494,5517, - 5539,5587,5589,5590,5661,5757,5781,5803,5804,5805, - 5806,5826,5827,5830,5947,5948,5949,5995,5996,5997, - 6019,6090,6091,6092,6093,6139,6140,6141,6163,6164, - 6165,6166,6187,6188,6211,6212,6331,6354,6355,6356, - 6572,6594,6595,6596,6597,6598,6618,6619,6620,6621] +capacity_credit_hours_of_year = [ + 4604, + 4605, + 4606, + 4628, + 4629, + 4630, + 4652, + 4821, + 5157, + 5253, + 5254, + 5277, + 5278, + 5299, + 5300, + 5301, + 5302, + 5321, + 5323, + 5324, + 5325, + 5326, + 5327, + 5347, + 5348, + 5349, + 5350, + 5369, + 5370, + 5371, + 5372, + 5374, + 5395, + 5396, + 5397, + 5398, + 5419, + 5420, + 5421, + 5422, + 5443, + 5444, + 5445, + 5446, + 5467, + 5468, + 5469, + 5493, + 5494, + 5517, + 5539, + 5587, + 5589, + 5590, + 5661, + 5757, + 5781, + 5803, + 5804, + 5805, + 5806, + 5826, + 5827, + 5830, + 5947, + 5948, + 5949, + 5995, + 5996, + 5997, + 6019, + 6090, + 6091, + 6092, + 6093, + 6139, + 6140, + 6141, + 6163, + 6164, + 6165, + 6166, + 6187, + 6188, + 6211, + 6212, + 6331, + 6354, + 6355, + 6356, + 6572, + 6594, + 6595, + 6596, + 6597, + 6598, + 6618, + 6619, + 6620, + 6621, +] # List length 8760, True if the hour counts for capacity payments, False otherwise -capacity_credit_hours = [hour in capacity_credit_hours_of_year for hour in range(1,8760+1)] +capacity_credit_hours = [ + hour in capacity_credit_hours_of_year for hour in range(1, 8760 + 1) +] + def test_hybrid_wave_only(hybrid_config, wavesite, subtests): hybrid_config["site"]["wave"] = True hybrid_config["site"]["wave_resource_file"] = wave_resource_file wave_only_technologies = { - 'wave': { - 'device_rating_kw': mhk_config['device_rating_kw'], - 'num_devices': 10, - 'wave_power_matrix': mhk_config['wave_power_matrix'], - 'fin_model': DEFAULT_FIN_CONFIG + "wave": { + "device_rating_kw": mhk_config["device_rating_kw"], + "num_devices": 10, + "wave_power_matrix": mhk_config["wave_power_matrix"], + "fin_model": DEFAULT_FIN_CONFIG, + }, + "grid": { + "interconnect_kw": interconnection_size_kw, + "fin_model": DEFAULT_FIN_CONFIG, }, - 'grid': { - 'interconnect_kw': interconnection_size_kw, - 'fin_model': DEFAULT_FIN_CONFIG, - } } hybrid_config["technologies"] = wave_only_technologies - - # TODO once the financial model is implemented, romove the line immediately following this comment and un-indent the rest of the test + + # TODO once the financial model is implemented, romove the line immediately following this comment and un-indent the rest of the test hi = HoppInterface(hybrid_config) hybrid_plant = hi.system # hybrid_plant = HybridSimulation(wave_only_technologies, wavesite) - cost_model_inputs = MHKCostModelInputs.from_dict({ - 'reference_model_num':3, - 'water_depth': 100, - 'distance_to_shore': 80, - 'number_rows': 10, - 'device_spacing':600, - 'row_spacing': 600, - 'cable_system_overbuild': 20 - }) + cost_model_inputs = MHKCostModelInputs.from_dict( + { + "reference_model_num": 3, + "water_depth": 100, + "distance_to_shore": 80, + "number_rows": 10, + "device_spacing": 600, + "row_spacing": 600, + "cable_system_overbuild": 20, + } + ) assert hybrid_plant.wave is not None hybrid_plant.wave.create_mhk_cost_calculator(cost_model_inputs) @@ -123,99 +223,142 @@ def test_hybrid_wave_only(hybrid_config, wavesite, subtests): # check that wave and grid match when only wave is in the hybrid system with subtests.test("financial parameters"): - assert hybrid_plant.wave._financial_model.FinancialParameters == approx(hybrid_plant.grid._financial_model.FinancialParameters) + assert hybrid_plant.wave._financial_model.FinancialParameters == approx( + hybrid_plant.grid._financial_model.FinancialParameters + ) with subtests.test("Revenue"): - assert hybrid_plant.wave._financial_model.Revenue == approx(hybrid_plant.grid._financial_model.Revenue) + assert hybrid_plant.wave._financial_model.Revenue == approx( + hybrid_plant.grid._financial_model.Revenue + ) with subtests.test("SystemCosts"): - assert hybrid_plant.wave._financial_model.SystemCosts == approx(hybrid_plant.grid._financial_model.SystemCosts) + assert hybrid_plant.wave._financial_model.SystemCosts == approx( + hybrid_plant.grid._financial_model.SystemCosts + ) # with subtests.test("SystemOutput.__dict__"): # skip(reason="this test will not be consistent until the code is more type stable. Outputs may be tuple or list") # assert hybrid_plant.wave._financial_model.SystemOutput.__dict__ == hybrid_plant.grid._financial_model.SystemOutput.__dict__ with subtests.test("SystemOutput.gen"): - assert hybrid_plant.wave._financial_model.SystemOutput.gen == approx(hybrid_plant.grid._financial_model.SystemOutput.gen) + assert hybrid_plant.wave._financial_model.SystemOutput.gen == approx( + hybrid_plant.grid._financial_model.SystemOutput.gen + ) with subtests.test("SystemOutput.system_capacity"): - assert hybrid_plant.wave._financial_model.SystemOutput.system_capacity == approx(hybrid_plant.grid._financial_model.SystemOutput.system_capacity) + assert ( + hybrid_plant.wave._financial_model.SystemOutput.system_capacity + == approx(hybrid_plant.grid._financial_model.SystemOutput.system_capacity) + ) with subtests.test("SystemOutput.degradation"): - assert hybrid_plant.wave._financial_model.SystemOutput.degradation == approx(hybrid_plant.grid._financial_model.SystemOutput.degradation) + assert hybrid_plant.wave._financial_model.SystemOutput.degradation == approx( + hybrid_plant.grid._financial_model.SystemOutput.degradation + ) with subtests.test("SystemOutput.system_pre_curtailment_kwac"): - assert hybrid_plant.wave._financial_model.SystemOutput.system_pre_curtailment_kwac == approx(hybrid_plant.grid._financial_model.SystemOutput.system_pre_curtailment_kwac) + assert ( + hybrid_plant.wave._financial_model.SystemOutput.system_pre_curtailment_kwac + == approx( + hybrid_plant.grid._financial_model.SystemOutput.system_pre_curtailment_kwac + ) + ) with subtests.test("SystemOutput.annual_energy_pre_curtailment_ac"): - assert hybrid_plant.wave._financial_model.SystemOutput.annual_energy_pre_curtailment_ac == approx(hybrid_plant.grid._financial_model.SystemOutput.annual_energy_pre_curtailment_ac) + assert ( + hybrid_plant.wave._financial_model.SystemOutput.annual_energy_pre_curtailment_ac + == approx( + hybrid_plant.grid._financial_model.SystemOutput.annual_energy_pre_curtailment_ac + ) + ) with subtests.test("Outputs"): - assert hybrid_plant.wave._financial_model.Outputs == approx(hybrid_plant.grid._financial_model.Outputs) + assert hybrid_plant.wave._financial_model.Outputs == approx( + hybrid_plant.grid._financial_model.Outputs + ) with subtests.test("net cash flow"): - wave_period = hybrid_plant.wave._financial_model.value('analysis_period') - grid_period = hybrid_plant.grid._financial_model.value('analysis_period') - assert hybrid_plant.wave._financial_model.net_cash_flow(wave_period) == approx(hybrid_plant.grid._financial_model.net_cash_flow(grid_period)) - + wave_period = hybrid_plant.wave._financial_model.value("analysis_period") + grid_period = hybrid_plant.grid._financial_model.value("analysis_period") + assert hybrid_plant.wave._financial_model.net_cash_flow(wave_period) == approx( + hybrid_plant.grid._financial_model.net_cash_flow(grid_period) + ) + with subtests.test("degradation"): - assert hybrid_plant.wave._financial_model.value("degradation") == approx(hybrid_plant.grid._financial_model.value("degradation")) + assert hybrid_plant.wave._financial_model.value("degradation") == approx( + hybrid_plant.grid._financial_model.value("degradation") + ) with subtests.test("total_installed_cost"): - assert hybrid_plant.wave._financial_model.value("total_installed_cost") == approx(hybrid_plant.grid._financial_model.value("total_installed_cost")) + assert hybrid_plant.wave._financial_model.value( + "total_installed_cost" + ) == approx(hybrid_plant.grid._financial_model.value("total_installed_cost")) with subtests.test("inflation_rate"): - assert hybrid_plant.wave._financial_model.value("inflation_rate") == approx(hybrid_plant.grid._financial_model.value("inflation_rate")) + assert hybrid_plant.wave._financial_model.value("inflation_rate") == approx( + hybrid_plant.grid._financial_model.value("inflation_rate") + ) with subtests.test("annual_energy"): - assert hybrid_plant.wave._financial_model.value("annual_energy") == approx(hybrid_plant.grid._financial_model.value("annual_energy")) + assert hybrid_plant.wave._financial_model.value("annual_energy") == approx( + hybrid_plant.grid._financial_model.value("annual_energy") + ) with subtests.test("ppa_price_input"): - assert hybrid_plant.wave._financial_model.value("ppa_price_input") == approx(hybrid_plant.grid._financial_model.value("ppa_price_input")) + assert hybrid_plant.wave._financial_model.value("ppa_price_input") == approx( + hybrid_plant.grid._financial_model.value("ppa_price_input") + ) with subtests.test("ppa_escalation"): - assert hybrid_plant.wave._financial_model.value("ppa_escalation") == approx(hybrid_plant.grid._financial_model.value("ppa_escalation")) + assert hybrid_plant.wave._financial_model.value("ppa_escalation") == approx( + hybrid_plant.grid._financial_model.value("ppa_escalation") + ) # test hybrid outputs with subtests.test("wave aep"): - assert aeps.wave == approx(12132526.0,1e-2) + assert aeps.wave == approx(12132526.0, 1e-2) with subtests.test("hybrid wave only aep"): assert aeps.hybrid == approx(aeps.wave) with subtests.test("wave cf"): - assert cf.wave == approx(48.42,1e-2) + assert cf.wave == approx(48.42, 1e-2) with subtests.test("hybrid wave only cf"): assert cf.hybrid == approx(cf.wave) with subtests.test("wave npv"): - #TODO check/verify this test value somehow, not sure how to do it right now + # TODO check/verify this test value somehow, not sure how to do it right now assert npvs.wave == approx(-53731805.52113224) with subtests.test("hybrid wave only npv"): assert npvs.hybrid == approx(npvs.wave) + def test_hybrid_wave_battery(hybrid_config, wavesite, subtests): hybrid_config["site"]["wave"] = True hybrid_config["site"]["wave_resource_file"] = wave_resource_file wave_only_technologies = { - 'wave': { - 'device_rating_kw': mhk_config['device_rating_kw'], - 'num_devices': 10, - 'wave_power_matrix': mhk_config['wave_power_matrix'], - 'fin_model': DEFAULT_FIN_CONFIG + "wave": { + "device_rating_kw": mhk_config["device_rating_kw"], + "num_devices": 10, + "wave_power_matrix": mhk_config["wave_power_matrix"], + "fin_model": DEFAULT_FIN_CONFIG, }, - 'battery': { - 'system_capacity_kwh': 20000, - 'system_capacity_kw': 80000, - 'fin_model': DEFAULT_FIN_CONFIG + "battery": { + "system_capacity_kwh": 20000, + "system_capacity_kw": 80000, + "fin_model": DEFAULT_FIN_CONFIG, + }, + "grid": { + "interconnect_kw": interconnection_size_kw, + "fin_model": DEFAULT_FIN_CONFIG, }, - 'grid': { - 'interconnect_kw': interconnection_size_kw, - 'fin_model': DEFAULT_FIN_CONFIG, - } } hybrid_config["technologies"] = wave_only_technologies - - # TODO once the financial model is implemented, romove the line immediately following this comment and un-indent the rest of the test + + # TODO once the financial model is implemented, romove the line immediately following this comment and un-indent the rest of the test hi = HoppInterface(hybrid_config) hybrid_plant = hi.system # hybrid_plant = HybridSimulation(wave_only_technologies, wavesite) - cost_model_inputs = MHKCostModelInputs.from_dict({ - 'reference_model_num':3, - 'water_depth': 100, - 'distance_to_shore': 80, - 'number_rows': 10, - 'device_spacing':600, - 'row_spacing': 600, - 'cable_system_overbuild': 20 - }) + cost_model_inputs = MHKCostModelInputs.from_dict( + { + "reference_model_num": 3, + "water_depth": 100, + "distance_to_shore": 80, + "number_rows": 10, + "device_spacing": 600, + "row_spacing": 600, + "cable_system_overbuild": 20, + } + ) assert hybrid_plant.wave is not None hybrid_plant.wave.create_mhk_cost_calculator(cost_model_inputs) + hybrid_plant.battery._financial_model.om_batt_variable_cost = [0.75] hi.simulate() aeps = hybrid_plant.annual_energies @@ -225,9 +368,10 @@ def test_hybrid_wave_battery(hybrid_config, wavesite, subtests): with subtests.test("battery aep"): assert aeps.battery == approx(87.84, 1e3) + def test_hybrid_wind_only(hybrid_config): technologies = hybrid_config["technologies"] - wind_only = {key: technologies[key] for key in ('wind', 'grid')} + wind_only = {key: technologies[key] for key in ("wind", "grid")} hybrid_config["technologies"] = wind_only hi = HoppInterface(hybrid_config) hybrid_plant = hi.system @@ -247,7 +391,7 @@ def test_hybrid_wind_only(hybrid_config): def test_hybrid_pv_only(hybrid_config): technologies = hybrid_config["technologies"] - solar_only = {key: technologies[key] for key in ('pv', 'grid')} + solar_only = {key: technologies[key] for key in ("pv", "grid")} hybrid_config["technologies"] = solar_only hi = HoppInterface(hybrid_config) @@ -269,17 +413,23 @@ def test_hybrid_pv_only(hybrid_config): def test_detailed_pv_system_capacity(hybrid_config, subtests): - with subtests.test("Detailed PV model (pvsamv1) using defaults except the top level system_capacity_kw parameter"): + with subtests.test( + "Detailed PV model (pvsamv1) using defaults except the top level system_capacity_kw parameter" + ): annual_energy_expected = 11128604 npv_expected = -2436229 technologies = hybrid_config["technologies"] - solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) # includes system_capacity_kw parameter - solar_only['pv']['use_pvwatts'] = False # specify detailed PV model but don't change any defaults - solar_only['grid']['interconnect_kw'] = 150e3 + solar_only = deepcopy( + {key: technologies[key] for key in ("pv", "grid")} + ) # includes system_capacity_kw parameter + solar_only["pv"][ + "use_pvwatts" + ] = False # specify detailed PV model but don't change any defaults + solar_only["grid"]["interconnect_kw"] = 150e3 hybrid_config["technologies"] = solar_only hi = HoppInterface(hybrid_config) hybrid_plant = hi.system - assert hybrid_plant.pv.value('subarray1_nstrings') == 1343 + assert hybrid_plant.pv.value("subarray1_nstrings") == 1343 hybrid_plant.layout.plot() hi.simulate() @@ -291,35 +441,47 @@ def test_detailed_pv_system_capacity(hybrid_config, subtests): assert npvs.pv == approx(npv_expected, 1e-3) assert npvs.hybrid == approx(npv_expected, 1e-3) - - with subtests.test("Detailed PV model (pvsamv1) using parameters from file except the top level system_capacity_kw parameter"): - pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" - with open(pvsamv1_defaults_file, 'r') as f: + with subtests.test( + "Detailed PV model (pvsamv1) using parameters from file except the top level system_capacity_kw parameter" + ): + pvsamv1_defaults_file = ( + Path(__file__).absolute().parent / "pvsamv1_basic_params.json" + ) + with open(pvsamv1_defaults_file, "r") as f: tech_config = json.load(f) - solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) # includes system_capacity_kw parameter - solar_only['pv']['use_pvwatts'] = False # specify detailed PV model - solar_only['pv']['tech_config'] = tech_config # specify parameters - solar_only['grid']['interconnect_kw'] = 150e3 + solar_only = deepcopy( + {key: technologies[key] for key in ("pv", "grid")} + ) # includes system_capacity_kw parameter + solar_only["pv"]["use_pvwatts"] = False # specify detailed PV model + solar_only["pv"]["tech_config"] = tech_config # specify parameters + solar_only["grid"]["interconnect_kw"] = 150e3 hybrid_config["technologies"] = solar_only with raises(Exception) as context: hi = HoppInterface(hybrid_config) - assert "The specified system capacity of 5000 kW is more than 5% from the value calculated" in str(context.value) + assert ( + "The specified system capacity of 5000 kW is more than 5% from the value calculated" + in str(context.value) + ) # Run detailed PV model (pvsamv1) using file parameters, minus the number of strings, and the top level system_capacity_kw parameter annual_energy_expected = 8955045 npv_expected = -2622684 - pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" - with open(pvsamv1_defaults_file, 'r') as f: + pvsamv1_defaults_file = ( + Path(__file__).absolute().parent / "pvsamv1_basic_params.json" + ) + with open(pvsamv1_defaults_file, "r") as f: tech_config = json.load(f) - tech_config.pop('subarray1_nstrings') - solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) # includes system_capacity_kw parameter - solar_only['pv']['use_pvwatts'] = False # specify detailed PV model - solar_only['pv']['tech_config'] = tech_config # specify parameters - solar_only['grid']['interconnect_kw'] = 150e3 + tech_config.pop("subarray1_nstrings") + solar_only = deepcopy( + {key: technologies[key] for key in ("pv", "grid")} + ) # includes system_capacity_kw parameter + solar_only["pv"]["use_pvwatts"] = False # specify detailed PV model + solar_only["pv"]["tech_config"] = tech_config # specify parameters + solar_only["grid"]["interconnect_kw"] = 150e3 hybrid_config["technologies"] = solar_only hi = HoppInterface(hybrid_config) hybrid_plant = hi.system - assert hybrid_plant.pv.value('subarray1_nstrings') == 1343 + assert hybrid_plant.pv.value("subarray1_nstrings") == 1343 hybrid_plant.layout.plot() hi.simulate() @@ -339,18 +501,19 @@ def test_hybrid_detailed_pv_only(site, hybrid_config, subtests): assert pv_plant.system_capacity_kw == approx(pv_kw, 1e-2) pv_plant.simulate_power(1, False) assert pv_plant.system_capacity_kw == approx(pv_kw, 1e-2) - assert pv_plant._system_model.Outputs.annual_energy == approx(annual_energy_expected, 1e-2) + assert pv_plant._system_model.Outputs.annual_energy == approx( + annual_energy_expected, 1e-2 + ) assert pv_plant._system_model.Outputs.capacity_factor == approx(25.66, 1e-2) with subtests.test("detailed PV model (pvsamv1) using defaults"): technologies = hybrid_config["technologies"] npv_expected = -2436229 - solar_only = { - 'pv': detailed_pv, - 'grid': technologies['grid'] - } - solar_only['pv']['use_pvwatts'] = False # specify detailed PV model but don't change any defaults - solar_only['grid']['interconnect_kw'] = 150e3 + solar_only = {"pv": detailed_pv, "grid": technologies["grid"]} + solar_only["pv"][ + "use_pvwatts" + ] = False # specify detailed PV model but don't change any defaults + solar_only["grid"]["interconnect_kw"] = 150e3 hybrid_config["technologies"] = solar_only hi = HoppInterface(hybrid_config) hybrid_plant = hi.system @@ -368,14 +531,16 @@ def test_hybrid_detailed_pv_only(site, hybrid_config, subtests): with subtests.test("Detailed PV model (pvsamv1) using parameters from file"): annual_energy_expected = 102997528 npv_expected = -25049424 - pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" - with open(pvsamv1_defaults_file, 'r') as f: + pvsamv1_defaults_file = ( + Path(__file__).absolute().parent / "pvsamv1_basic_params.json" + ) + with open(pvsamv1_defaults_file, "r") as f: tech_config = json.load(f) - solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) - solar_only['pv']['use_pvwatts'] = False # specify detailed PV model - solar_only['pv']['tech_config'] = tech_config # specify parameters - solar_only['grid']['interconnect_kw'] = 150e3 - solar_only['pv']['system_capacity_kw'] = 50000 # use another system capacity + solar_only = deepcopy({key: technologies[key] for key in ("pv", "grid")}) + solar_only["pv"]["use_pvwatts"] = False # specify detailed PV model + solar_only["pv"]["tech_config"] = tech_config # specify parameters + solar_only["grid"]["interconnect_kw"] = 150e3 + solar_only["pv"]["system_capacity_kw"] = 50000 # use another system capacity hybrid_config["technologies"] = solar_only hi = HoppInterface(hybrid_config) hybrid_plant = hi.system @@ -410,38 +575,49 @@ def test_hybrid_detailed_pv_only(site, hybrid_config, subtests): # assert npvs.pv == approx(npv_expected, 1e-3) # assert npvs.hybrid == approx(npv_expected, 1e-3) - with subtests.test("Detailed PV model using parameters from file and autosizing electrical parameters"): + with subtests.test( + "Detailed PV model using parameters from file and autosizing electrical parameters" + ): annual_energy_expected = 102319358 npv_expected = -25110524 - pvsamv1_defaults_file = Path(__file__).absolute().parent / "pvsamv1_basic_params.json" - with open(pvsamv1_defaults_file, 'r') as f: + pvsamv1_defaults_file = ( + Path(__file__).absolute().parent / "pvsamv1_basic_params.json" + ) + with open(pvsamv1_defaults_file, "r") as f: tech_config = json.load(f) - solar_only = deepcopy({key: technologies[key] for key in ('pv', 'grid')}) - solar_only['pv']['use_pvwatts'] = False # specify detailed PV model - solar_only['pv']['tech_config'] = tech_config # specify parameters - solar_only['grid']['interconnect_kw'] = 150e3 - solar_only['pv'].pop('system_capacity_kw') # use default system capacity instead + solar_only = deepcopy({key: technologies[key] for key in ("pv", "grid")}) + solar_only["pv"]["use_pvwatts"] = False # specify detailed PV model + solar_only["pv"]["tech_config"] = tech_config # specify parameters + solar_only["grid"]["interconnect_kw"] = 150e3 + solar_only["pv"].pop( + "system_capacity_kw" + ) # use default system capacity instead # autosize number of strings, number of inverters and adjust system capacity - n_strings, n_combiners, n_inverters, calculated_system_capacity = size_electrical_parameters( - target_system_capacity=solar_only['pv']['tech_config']['system_capacity'], - target_dc_ac_ratio=1.34, - modules_per_string=solar_only['pv']['tech_config']['subarray1_modules_per_string'], - module_power= \ - solar_only['pv']['tech_config']['cec_i_mp_ref'] \ - * solar_only['pv']['tech_config']['cec_v_mp_ref'] \ + n_strings, n_combiners, n_inverters, calculated_system_capacity = ( + size_electrical_parameters( + target_system_capacity=solar_only["pv"]["tech_config"][ + "system_capacity" + ], + target_dc_ac_ratio=1.34, + modules_per_string=solar_only["pv"]["tech_config"][ + "subarray1_modules_per_string" + ], + module_power=solar_only["pv"]["tech_config"]["cec_i_mp_ref"] + * solar_only["pv"]["tech_config"]["cec_v_mp_ref"] * 1e-3, - inverter_power=solar_only['pv']['tech_config']['inv_snl_paco'] * 1e-3, - n_inputs_inverter=50, - n_inputs_combiner=32 + inverter_power=solar_only["pv"]["tech_config"]["inv_snl_paco"] * 1e-3, + n_inputs_inverter=50, + n_inputs_combiner=32, + ) ) assert n_strings == 13435 assert n_combiners == 420 assert n_inverters == 50 assert calculated_system_capacity == approx(50002.2, 1e-3) - solar_only['pv']['tech_config']['subarray1_nstrings'] = n_strings - solar_only['pv']['tech_config']['inverter_count'] = n_inverters - solar_only['pv']['tech_config']['system_capacity'] = calculated_system_capacity + solar_only["pv"]["tech_config"]["subarray1_nstrings"] = n_strings + solar_only["pv"]["tech_config"]["inverter_count"] = n_inverters + solar_only["pv"]["tech_config"]["system_capacity"] = calculated_system_capacity hybrid_config["technologies"] = solar_only hi = HoppInterface(hybrid_config) @@ -468,32 +644,26 @@ def test_hybrid_user_instantiated(site, subtests): interconnect_kw = 150e3 layout_params = { - "x_position": 0.5, - "y_position": 0.5, - "aspect_power": 0, - "gcr": 0.5, - "s_buffer": 2, - "x_buffer": 2 + "x_position": 0.5, + "y_position": 0.5, + "aspect_power": 0, + "gcr": 0.5, + "s_buffer": 2, + "x_buffer": 2, } # Run non-user-instantiated to compare against with subtests.test("baseline comparison"): solar_only = { - 'pv': { - 'use_pvwatts': False, - 'tech_config': {'system_capacity_kw': system_capacity_kw}, + "pv": { + "use_pvwatts": False, + "tech_config": {"system_capacity_kw": system_capacity_kw}, "layout_params": layout_params, - 'dc_degradation': [0] * 25 + "dc_degradation": [0] * 25, }, - 'grid': { - 'interconnect_kw': interconnect_kw, - 'ppa_price': 0.01 - } - } - hopp_config = { - "site": site, - "technologies": solar_only + "grid": {"interconnect_kw": interconnect_kw, "ppa_price": 0.01}, } + hopp_config = {"site": site, "technologies": solar_only} hi = HoppInterface(hopp_config) hybrid_plant = hi.system hybrid_plant.layout.plot() @@ -506,27 +676,23 @@ def test_hybrid_user_instantiated(site, subtests): assert npvs.pv == approx(npv_expected, 1e-2) assert npvs.hybrid == approx(npv_expected, 1e-2) - with subtests.test("detailed PV plant, grid and respective financial models"): - # Run + # Run power_sources = { - 'pv': { - 'use_pvwatts': False, - 'system_capacity_kw': system_capacity_kw, - 'layout_params': layout_params, - 'fin_model': 'FlatPlatePVSingleOwner', - 'dc_degradation': [0] * 25 + "pv": { + "use_pvwatts": False, + "system_capacity_kw": system_capacity_kw, + "layout_params": layout_params, + "fin_model": "FlatPlatePVSingleOwner", + "dc_degradation": [0] * 25, + }, + "grid": { + "interconnect_kw": interconnect_kw, + "fin_model": "GenericSystemSingleOwner", + "ppa_price": 0.01, }, - 'grid': { - 'interconnect_kw': interconnect_kw, - 'fin_model': 'GenericSystemSingleOwner', - 'ppa_price': 0.01 - } - } - hopp_config = { - "site": site, - "technologies": power_sources } + hopp_config = {"site": site, "technologies": power_sources} hi = HoppInterface(hopp_config) hybrid_plant = hi.system assert hybrid_plant.pv is not None @@ -536,8 +702,12 @@ def test_hybrid_user_instantiated(site, subtests): aeps = hybrid_plant.annual_energies npvs = hybrid_plant.net_present_values - assert hybrid_plant.pv._system_model.value("system_capacity") == approx(system_capacity_kw_expected, 1e-3) - assert hybrid_plant.pv._financial_model.value("system_capacity") == approx(system_capacity_kw_expected, 1e-3) + assert hybrid_plant.pv._system_model.value("system_capacity") == approx( + system_capacity_kw_expected, 1e-3 + ) + assert hybrid_plant.pv._financial_model.value("system_capacity") == approx( + system_capacity_kw_expected, 1e-3 + ) assert aeps.pv == approx(annual_energy_expected, 1e-3) assert aeps.hybrid == approx(annual_energy_expected, 1e-3) assert npvs.pv == approx(npv_expected, 1e-3) @@ -549,7 +719,7 @@ def test_hybrid(hybrid_config): Performance from Wind is slightly different from wind-only case because the solar presence modified the wind layout """ technologies = hybrid_config["technologies"] - solar_wind_hybrid = {key: technologies[key] for key in ('pv', 'wind', 'grid')} + solar_wind_hybrid = {key: technologies[key] for key in ("pv", "wind", "grid")} hybrid_config["technologies"] = solar_wind_hybrid hi = HoppInterface(hybrid_config) hybrid_plant = hi.system @@ -570,11 +740,16 @@ def test_hybrid(hybrid_config): def test_wind_pv_with_storage_dispatch(hybrid_config): technologies = hybrid_config["technologies"] - wind_pv_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery', 'grid')} + wind_pv_battery = { + key: technologies[key] for key in ("pv", "wind", "battery", "grid") + } hybrid_config["technologies"] = wind_pv_battery hybrid_config["technologies"]["grid"]["ppa_price"] = 0.03 hi = HoppInterface(hybrid_config) hybrid_plant = hi.system + hybrid_plant.battery._financial_model.SystemCosts.assign( + {"om_batt_variable_cost": [0.75]} + ) hi.simulate() @@ -649,26 +824,23 @@ def test_wind_pv_with_storage_dispatch(hybrid_config): def test_tower_pv_hybrid(hybrid_config): interconnection_size_kw_test = 50000 technologies_test = { - 'tower': { - 'cycle_capacity_kw': 50 * 1000, - 'solar_multiple': 2.0, - 'tes_hours': 12.0 + "tower": { + "cycle_capacity_kw": 50 * 1000, + "solar_multiple": 2.0, + "tes_hours": 12.0, }, - 'pv': {'system_capacity_kw': 50 * 1000}, - 'grid': { - 'interconnect_kw': interconnection_size_kw_test, - 'ppa_price': 0.12 - } + "pv": {"system_capacity_kw": 50 * 1000}, + "grid": {"interconnect_kw": interconnection_size_kw_test, "ppa_price": 0.12}, } - solar_hybrid = {key: technologies_test[key] for key in ('tower', 'pv', 'grid')} + solar_hybrid = {key: technologies_test[key] for key in ("tower", "pv", "grid")} hybrid_config["technologies"] = solar_hybrid - dispatch_options={'is_test_start_year': True, 'is_test_end_year': True} + dispatch_options = {"is_test_start_year": True, "is_test_end_year": True} hybrid_config["config"]["dispatch_options"] = dispatch_options hi = HoppInterface(hybrid_config) hybrid_plant = hi.system - hybrid_plant.tower.value('helio_width', 8.0) - hybrid_plant.tower.value('helio_height', 8.0) + hybrid_plant.tower.value("helio_width", 8.0) + hybrid_plant.tower.value("helio_height", 8.0) hi.simulate() @@ -681,28 +853,25 @@ def test_tower_pv_hybrid(hybrid_config): # TODO: check npv for csp would require a full simulation assert npvs.pv == approx(45233832.23, 1e3) - #assert npvs.tower == approx(-13909363, 1e3) - #assert npvs.hybrid == approx(-19216589, 1e3) + # assert npvs.tower == approx(-13909363, 1e3) + # assert npvs.hybrid == approx(-19216589, 1e3) def test_trough_pv_hybrid(hybrid_config): interconnection_size_kw_test = 50000 technologies_test = { - 'trough': { - 'cycle_capacity_kw': 50 * 1000, - 'solar_multiple': 2.0, - 'tes_hours': 12.0 - }, - 'pv': {'system_capacity_kw': 50 * 1000}, - 'grid': { - 'interconnect_kw': interconnection_size_kw_test, - 'ppa_price': 0.12 + "trough": { + "cycle_capacity_kw": 50 * 1000, + "solar_multiple": 2.0, + "tes_hours": 12.0, }, + "pv": {"system_capacity_kw": 50 * 1000}, + "grid": {"interconnect_kw": interconnection_size_kw_test, "ppa_price": 0.12}, } - solar_hybrid = {key: technologies_test[key] for key in ('trough', 'pv', 'grid')} + solar_hybrid = {key: technologies_test[key] for key in ("trough", "pv", "grid")} hybrid_config["technologies"] = solar_hybrid - dispatch_options={'is_test_start_year': True, 'is_test_end_year': True} + dispatch_options = {"is_test_start_year": True, "is_test_end_year": True} hybrid_config["config"]["dispatch_options"] = dispatch_options hi = HoppInterface(hybrid_config) hybrid_plant = hi.system @@ -717,37 +886,33 @@ def test_trough_pv_hybrid(hybrid_config): assert aeps.hybrid == approx(106111732.52, 1e-3) assert npvs.pv == approx(80738107, 1e3) - #assert npvs.tower == approx(-13909363, 1e3) - #assert npvs.hybrid == approx(-19216589, 1e3) + # assert npvs.tower == approx(-13909363, 1e3) + # assert npvs.hybrid == approx(-19216589, 1e3) def test_tower_pv_battery_hybrid(hybrid_config): interconnection_size_kw_test = 50000 technologies_test = { - 'tower': { - 'cycle_capacity_kw': 50 * 1000, - 'solar_multiple': 2.0, - 'tes_hours': 12.0 + "tower": { + "cycle_capacity_kw": 50 * 1000, + "solar_multiple": 2.0, + "tes_hours": 12.0, }, - 'pv': {'system_capacity_kw': 50 * 1000}, - 'battery': { - 'system_capacity_kwh': 40 * 1000, - 'system_capacity_kw': 20 * 1000 - }, - 'grid': { - 'interconnect_kw': interconnection_size_kw_test, - 'ppa_price': 0.12 - } + "pv": {"system_capacity_kw": 50 * 1000}, + "battery": {"system_capacity_kwh": 40 * 1000, "system_capacity_kw": 20 * 1000}, + "grid": {"interconnect_kw": interconnection_size_kw_test, "ppa_price": 0.12}, } - solar_hybrid = {key: technologies_test[key] for key in ('tower', 'pv', 'battery', 'grid')} - dispatch_options={'is_test_start_year': True, 'is_test_end_year': True} + solar_hybrid = { + key: technologies_test[key] for key in ("tower", "pv", "battery", "grid") + } + dispatch_options = {"is_test_start_year": True, "is_test_end_year": True} hybrid_config["technologies"] = solar_hybrid hybrid_config["config"]["dispatch_options"] = dispatch_options hi = HoppInterface(hybrid_config) hybrid_plant = hi.system - hybrid_plant.tower.value('helio_width', 10.0) - hybrid_plant.tower.value('helio_height', 10.0) + hybrid_plant.tower.value("helio_width", 10.0) + hybrid_plant.tower.value("helio_height", 10.0) hi.simulate() @@ -760,29 +925,35 @@ def test_tower_pv_battery_hybrid(hybrid_config): assert aeps.hybrid == approx(107903653, 1e-2) assert npvs.pv == approx(80738107, 1e3) - #assert npvs.tower == approx(-13909363, 1e3) - #assert npvs.hybrid == approx(-19216589, 1e3) + # assert npvs.tower == approx(-13909363, 1e3) + # assert npvs.hybrid == approx(-19216589, 1e3) + def test_hybrid_om_costs_error(hybrid_config): technologies = hybrid_config["technologies"] - wind_pv_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery', 'grid')} - dispatch_options={'battery_dispatch': 'one_cycle_heuristic'} + wind_pv_battery = { + key: technologies[key] for key in ("pv", "wind", "battery", "grid") + } + dispatch_options = {"battery_dispatch": "one_cycle_heuristic"} hybrid_config["technologies"] = wind_pv_battery hybrid_config["technologies"]["grid"]["ppa_price"] = 0.03 hybrid_config["config"]["dispatch_options"] = dispatch_options hi = HoppInterface(hybrid_config) hybrid_plant = hi.system - hybrid_plant.battery._financial_model.value('om_production', (1,)) + hybrid_plant.battery._financial_model.value("om_production", (1,)) try: hi.simulate() except ValueError as e: assert e + def test_hybrid_om_costs(hybrid_config): technologies = hybrid_config["technologies"] - wind_pv_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery', 'grid')} - dispatch_options={'battery_dispatch': 'one_cycle_heuristic'} + wind_pv_battery = { + key: technologies[key] for key in ("pv", "wind", "battery", "grid") + } + dispatch_options = {"battery_dispatch": "one_cycle_heuristic"} hybrid_config["technologies"] = wind_pv_battery hybrid_config["technologies"]["grid"]["ppa_price"] = 0.03 hybrid_config["config"]["dispatch_options"] = dispatch_options @@ -810,7 +981,9 @@ def test_hybrid_om_costs(hybrid_config): var_om_costs = hybrid_plant.om_variable_expenses total_om_costs = hybrid_plant.om_total_expenses for i in range(len(var_om_costs.hybrid)): - assert var_om_costs.pv[i] + var_om_costs.wind[i] + var_om_costs.battery[i] == approx(var_om_costs.hybrid[i], rel=1e-1) + assert var_om_costs.pv[i] + var_om_costs.wind[i] + var_om_costs.battery[ + i + ] == approx(var_om_costs.hybrid[i], rel=1e-1) assert total_om_costs.pv[i] == approx(var_om_costs.pv[i]) assert total_om_costs.wind[i] == approx(var_om_costs.wind[i]) assert total_om_costs.battery[i] == approx(var_om_costs.battery[i]) @@ -827,8 +1000,9 @@ def test_hybrid_om_costs(hybrid_config): fixed_om_costs = hybrid_plant.om_fixed_expenses total_om_costs = hybrid_plant.om_total_expenses for i in range(len(fixed_om_costs.hybrid)): - assert fixed_om_costs.pv[i] + fixed_om_costs.wind[i] + fixed_om_costs.battery[i] \ - == approx(fixed_om_costs.hybrid[i]) + assert fixed_om_costs.pv[i] + fixed_om_costs.wind[i] + fixed_om_costs.battery[ + i + ] == approx(fixed_om_costs.hybrid[i]) assert total_om_costs.pv[i] == approx(fixed_om_costs.pv[i]) assert total_om_costs.wind[i] == approx(fixed_om_costs.wind[i]) assert total_om_costs.battery[i] == approx(fixed_om_costs.battery[i]) @@ -845,8 +1019,9 @@ def test_hybrid_om_costs(hybrid_config): cap_om_costs = hybrid_plant.om_capacity_expenses total_om_costs = hybrid_plant.om_total_expenses for i in range(len(cap_om_costs.hybrid)): - assert cap_om_costs.pv[i] + cap_om_costs.wind[i] + cap_om_costs.battery[i] \ - == approx(cap_om_costs.hybrid[i]) + assert cap_om_costs.pv[i] + cap_om_costs.wind[i] + cap_om_costs.battery[ + i + ] == approx(cap_om_costs.hybrid[i]) assert total_om_costs.pv[i] == approx(cap_om_costs.pv[i]) assert total_om_costs.wind[i] == approx(cap_om_costs.wind[i]) assert total_om_costs.battery[i] == approx(cap_om_costs.battery[i]) @@ -855,54 +1030,76 @@ def test_hybrid_om_costs(hybrid_config): hybrid_plant.pv.om_capacity = 0 hybrid_plant.battery.om_capacity = 0 + def test_hybrid_tax_incentives(hybrid_config): technologies = hybrid_config["technologies"] - wind_pv_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery', 'grid')} - dispatch_options={'battery_dispatch': 'one_cycle_heuristic'} + wind_pv_battery = { + key: technologies[key] for key in ("pv", "wind", "battery", "grid") + } + dispatch_options = {"battery_dispatch": "one_cycle_heuristic"} hybrid_config["technologies"] = wind_pv_battery hybrid_config["technologies"]["grid"]["ppa_price"] = 0.03 hybrid_config["config"]["dispatch_options"] = dispatch_options hi = HoppInterface(hybrid_config) hybrid_plant = hi.system - hybrid_plant.pv._financial_model.value('itc_fed_percent', [0.0]) - hybrid_plant.wind._financial_model.value('ptc_fed_amount', (1,)) - hybrid_plant.pv._financial_model.value('ptc_fed_amount', (2,)) - hybrid_plant.battery._financial_model.value('ptc_fed_amount', (3,)) - hybrid_plant.wind._financial_model.value('ptc_fed_escal', 0) - hybrid_plant.pv._financial_model.value('ptc_fed_escal', 0) - hybrid_plant.battery._financial_model.value('ptc_fed_escal', 0) + hybrid_plant.pv._financial_model.value("itc_fed_percent", [0.0]) + hybrid_plant.wind._financial_model.value("ptc_fed_amount", (1,)) + hybrid_plant.pv._financial_model.value("ptc_fed_amount", (2,)) + hybrid_plant.battery._financial_model.value("ptc_fed_amount", (3,)) + hybrid_plant.wind._financial_model.value("ptc_fed_escal", 0) + hybrid_plant.pv._financial_model.value("ptc_fed_escal", 0) + hybrid_plant.battery._financial_model.value("ptc_fed_escal", 0) hi.simulate() ptc_wind = hybrid_plant.wind._financial_model.value("cf_ptc_fed")[1] - assert ptc_wind == approx(hybrid_plant.wind._financial_model.value("ptc_fed_amount")[0]*hybrid_plant.wind.annual_energy_kwh, rel=1e-3) + assert ptc_wind == approx( + hybrid_plant.wind._financial_model.value("ptc_fed_amount")[0] + * hybrid_plant.wind.annual_energy_kwh, + rel=1e-3, + ) ptc_pv = hybrid_plant.pv._financial_model.value("cf_ptc_fed")[1] - assert ptc_pv == approx(hybrid_plant.pv._financial_model.value("ptc_fed_amount")[0]*hybrid_plant.pv.annual_energy_kwh, rel=1e-3) + assert ptc_pv == approx( + hybrid_plant.pv._financial_model.value("ptc_fed_amount")[0] + * hybrid_plant.pv.annual_energy_kwh, + rel=1e-3, + ) ptc_batt = hybrid_plant.battery._financial_model.value("cf_ptc_fed")[1] - assert ptc_batt == approx(hybrid_plant.battery._financial_model.value("ptc_fed_amount")[0] - * hybrid_plant.battery._financial_model.value('batt_annual_discharge_energy')[1], rel=1e-3) + assert ptc_batt == approx( + hybrid_plant.battery._financial_model.value("ptc_fed_amount")[0] + * hybrid_plant.battery._financial_model.value("batt_annual_discharge_energy")[ + 1 + ], + rel=1e-3, + ) ptc_hybrid = hybrid_plant.grid._financial_model.value("cf_ptc_fed")[1] ptc_fed_amount = hybrid_plant.grid._financial_model.value("ptc_fed_amount")[0] assert ptc_fed_amount == approx(1.229, rel=1e-2) - assert ptc_hybrid == approx(ptc_fed_amount * hybrid_plant.grid._financial_model.value('cf_energy_net')[1], rel=1e-3) + assert ptc_hybrid == approx( + ptc_fed_amount * hybrid_plant.grid._financial_model.value("cf_energy_net")[1], + rel=1e-3, + ) def test_capacity_credit(hybrid_config): technologies = hybrid_config["technologies"] site = create_default_site_info(capacity_hours=capacity_credit_hours) - wind_pv_battery = {key: technologies[key] for key in ('pv', 'wind', 'battery')} - wind_pv_battery['grid'] = { - 'interconnect_kw': interconnection_size_kw, - 'ppa_price': 0.03 + wind_pv_battery = {key: technologies[key] for key in ("pv", "wind", "battery")} + wind_pv_battery["grid"] = { + "interconnect_kw": interconnection_size_kw, + "ppa_price": 0.03, } hybrid_config["technologies"] = wind_pv_battery hybrid_config["site"] = site hi = HoppInterface(hybrid_config) hybrid_plant = hi.system + hybrid_plant.battery._financial_model.SystemCosts.assign( + {"om_batt_variable_cost": [0.75]} + ) assert hybrid_plant.interconnect_kw == 15e3 @@ -910,6 +1107,7 @@ def test_capacity_credit(hybrid_config): gen_max_feasible_orig = hybrid_plant.battery.gen_max_feasible capacity_hours_orig = hybrid_plant.site.capacity_hours interconnect_kw_orig = hybrid_plant.interconnect_kw + def reinstate_orig_values(): hybrid_plant.battery.gen_max_feasible = gen_max_feasible_orig hybrid_plant.site.capacity_hours = capacity_hours_orig @@ -918,24 +1116,32 @@ def reinstate_orig_values(): # Test when 0 gen_max_feasible reinstate_orig_values() hybrid_plant.battery.gen_max_feasible = [0] * 8760 - capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent(hybrid_plant.interconnect_kw) + capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent( + hybrid_plant.interconnect_kw + ) assert capacity_credit_battery == approx(0, rel=0.05) # Test when representative gen_max_feasible reinstate_orig_values() hybrid_plant.battery.gen_max_feasible = [2500] * 8760 - capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent(hybrid_plant.interconnect_kw) + capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent( + hybrid_plant.interconnect_kw + ) assert capacity_credit_battery == approx(50, rel=0.05) # Test when no capacity hours reinstate_orig_values() hybrid_plant.battery.gen_max_feasible = [2500] * 8760 hybrid_plant.site.capacity_hours = [False] * 8760 - capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent(hybrid_plant.interconnect_kw) + capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent( + hybrid_plant.interconnect_kw + ) assert capacity_credit_battery == approx(0, rel=0.05) # Test when no interconnect capacity reinstate_orig_values() hybrid_plant.battery.gen_max_feasible = [2500] * 8760 hybrid_plant.interconnect_kw = 0 - capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent(hybrid_plant.interconnect_kw) + capacity_credit_battery = hybrid_plant.battery.calc_capacity_credit_percent( + hybrid_plant.interconnect_kw + ) assert capacity_credit_battery == approx(0, rel=0.05) # Test integration with system simulation @@ -947,30 +1153,53 @@ def reinstate_orig_values(): hi.simulate() - total_gen_max_feasible = np.array(hybrid_plant.pv.gen_max_feasible) \ - + np.array(hybrid_plant.wind.gen_max_feasible) \ - + np.array(hybrid_plant.battery.gen_max_feasible) - assert sum(hybrid_plant.grid.gen_max_feasible) == approx(sum(np.minimum(hybrid_plant.grid.interconnect_kw * hybrid_plant.site.interval / 60, \ - total_gen_max_feasible)), rel=0.01) + total_gen_max_feasible = ( + np.array(hybrid_plant.pv.gen_max_feasible) + + np.array(hybrid_plant.wind.gen_max_feasible) + + np.array(hybrid_plant.battery.gen_max_feasible) + ) + assert sum(hybrid_plant.grid.gen_max_feasible) == approx( + sum( + np.minimum( + hybrid_plant.grid.interconnect_kw * hybrid_plant.site.interval / 60, + total_gen_max_feasible, + ) + ), + rel=0.01, + ) - total_nominal_capacity = hybrid_plant.pv.calc_nominal_capacity(hybrid_plant.interconnect_kw) \ - + hybrid_plant.wind.calc_nominal_capacity(hybrid_plant.interconnect_kw) \ - + hybrid_plant.battery.calc_nominal_capacity(hybrid_plant.interconnect_kw) + total_nominal_capacity = ( + hybrid_plant.pv.calc_nominal_capacity(hybrid_plant.interconnect_kw) + + hybrid_plant.wind.calc_nominal_capacity(hybrid_plant.interconnect_kw) + + hybrid_plant.battery.calc_nominal_capacity(hybrid_plant.interconnect_kw) + ) assert total_nominal_capacity == approx(18845.8, rel=0.01) - assert total_nominal_capacity == approx(hybrid_plant.grid.hybrid_nominal_capacity, rel=0.01) - + assert total_nominal_capacity == approx( + hybrid_plant.grid.hybrid_nominal_capacity, rel=0.01 + ) + capcred = hybrid_plant.capacity_credit_percent - assert capcred['pv'][0] == approx(8.03, rel=0.05) - assert capcred['wind'][0] == approx(33.25, rel=0.10) - assert capcred['battery'][0] == approx(58.95, rel=0.05) - assert capcred['hybrid'][0] == approx(43.88, rel=0.05) + assert capcred["pv"][0] == approx(8.03, rel=0.05) + assert capcred["wind"][0] == approx(33.25, rel=0.10) + assert capcred["battery"][0] == approx(58.95, rel=0.05) + assert capcred["hybrid"][0] == approx(43.88, rel=0.05) cp_pay = hybrid_plant.capacity_payments - np_cap = hybrid_plant.system_nameplate_mw # This is not the same as nominal capacity... - assert cp_pay['pv'][1]/(np_cap['pv'])/(capcred['pv'][0]/100) == approx(cap_payment_mw, 0.05) - assert cp_pay['wind'][1]/(np_cap['wind'])/(capcred['wind'][0]/100) == approx(cap_payment_mw, 0.05) - assert cp_pay['battery'][1]/(np_cap['battery'])/(capcred['battery'][0]/100) == approx(cap_payment_mw, 0.05) - assert cp_pay['hybrid'][1]/(np_cap['hybrid'])/(capcred['hybrid'][0]/100) == approx(cap_payment_mw, 0.05) + np_cap = ( + hybrid_plant.system_nameplate_mw + ) # This is not the same as nominal capacity... + assert cp_pay["pv"][1] / (np_cap["pv"]) / (capcred["pv"][0] / 100) == approx( + cap_payment_mw, 0.05 + ) + assert cp_pay["wind"][1] / (np_cap["wind"]) / (capcred["wind"][0] / 100) == approx( + cap_payment_mw, 0.05 + ) + assert cp_pay["battery"][1] / (np_cap["battery"]) / ( + capcred["battery"][0] / 100 + ) == approx(cap_payment_mw, 0.05) + assert cp_pay["hybrid"][1] / (np_cap["hybrid"]) / ( + capcred["hybrid"][0] / 100 + ) == approx(cap_payment_mw, 0.05) aeps = hybrid_plant.annual_energies npvs = hybrid_plant.net_present_values From bf529be37e043deee1a8fb77b4bd295e3e89920b Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 17 Apr 2024 11:58:51 -0700 Subject: [PATCH 21/27] add docstring, reformat power sources dispatch. --- .../dispatch/power_source_dispatch.rst | 15 - .../dispatch/power_sources/csp_dispatch.py | 1274 ++++++++++++----- .../power_sources/power_source_dispatch.py | 177 ++- .../dispatch/power_sources/pv_dispatch.py | 83 +- .../dispatch/power_sources/tower_dispatch.py | 91 +- .../dispatch/power_sources/trough_dispatch.py | 83 +- .../dispatch/power_sources/wave_dispatch.py | 79 +- .../dispatch/power_sources/wind_dispatch.py | 82 +- 8 files changed, 1428 insertions(+), 456 deletions(-) delete mode 100644 docs/hopp/simulation/technologies/dispatch/power_source_dispatch.rst diff --git a/docs/hopp/simulation/technologies/dispatch/power_source_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_source_dispatch.rst deleted file mode 100644 index c13f3be80..000000000 --- a/docs/hopp/simulation/technologies/dispatch/power_source_dispatch.rst +++ /dev/null @@ -1,15 +0,0 @@ -:orphan: - -.. _PowerSourceDispatch: - - -PowerSourceDispatch: Abstract Class -=================================== - -Base dispatch class for power source models - -.. toctree:: - - -.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch.PowerSourceDispatch - :members: diff --git a/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py index 26e8a1000..fa323594d 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py @@ -13,25 +13,47 @@ class CspDispatch(Dispatch): Dispatch model for Concentrating Solar Power (CSP) with thermal energy storage. """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model, - financial_model, - block_set_name: str = 'csp'): - - super().__init__(pyomo_model, index_set, system_model, financial_model, block_set_name=block_set_name) + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model, + financial_model, + block_set_name: str = "csp", + ): + """ + Initialize a CSP dispatch model. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo model instance. + index_set (pyomo.Set): Index set for the model. + system_model: System model. + financial_model: Financial model. + block_set_name (str, optional): Name of the block. Defaults to 'csp'. + """ + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) self._create_linking_constraints() - self.objective_cost_terms = {'cost_per_field_generation': 0.5, - 'cost_per_field_start_rel': 1.5, - 'cost_per_cycle_generation': 2.0, - 'cost_per_cycle_start_rel': 40.0, - 'cost_per_change_thermal_input': 0.5} + self.objective_cost_terms = { + "cost_per_field_generation": 0.5, + "cost_per_field_start_rel": 1.5, + "cost_per_cycle_generation": 2.0, + "cost_per_cycle_start_rel": 40.0, + "cost_per_change_thermal_input": 0.5, + } def dispatch_block_rule(self, csp): """ - Called during Dispatch's __init__ + Called during Dispatch's __init__. Define dispatch block rules. + + Args: + csp: CSP instance. """ # Parameters self._create_storage_parameters(csp) @@ -55,89 +77,115 @@ def dispatch_block_rule(self, csp): @staticmethod def _create_storage_parameters(csp): + """ + Create parameters related to thermal energy storage. + + Args: + csp: CSP instance. + """ + csp.time_duration = pyomo.Param( doc="Time step [hour]", default=1.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.hr) + units=u.hr, + ) csp.storage_capacity = pyomo.Param( doc="Thermal energy storage capacity [MWht]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) @staticmethod def _create_receiver_parameters(csp): + """ + Create parameters related to CSP receiver. + + Args: + csp: CSP instance. + """ # Cost Parameters csp.cost_per_field_generation = pyomo.Param( doc="Generation cost for the CSP field and receiver [$/MWht]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.MWh) + units=u.USD / u.MWh, + ) csp.cost_per_field_start = pyomo.Param( doc="Fixed cost for receiver start-up [$/start]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD) # $/start + units=u.USD, + ) # $/start # Performance Parameters csp.available_thermal_generation = pyomo.Param( doc="Available thermal power generated by the CSP heliostat field [MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) csp.receiver_startup_fraction = pyomo.Param( doc="Estimated fraction of time period required for receiver start-up [-]", default=1.0, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.min_receiver_start_time = pyomo.Param( doc="Minimum time to start the receiver [hr]", default=0.5, within=pyomo.NonNegativeReals, mutable=True, - units=u.hr) + units=u.hr, + ) csp.field_startup_losses = pyomo.Param( doc="Heliostat field startup or shut down parasitic loss [MWhe]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) csp.receiver_required_startup_energy = pyomo.Param( doc="Required energy expended to start receiver [MWht]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) csp.receiver_pumping_losses = pyomo.Param( doc="Solar field and/or receiver pumping power per unit power produced [MWe/MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.minimum_receiver_power = pyomo.Param( doc="Minimum operational thermal power delivered by receiver [MWt]", default=1.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) csp.allowable_receiver_startup_power = pyomo.Param( doc="Allowable power per period for receiver start-up [MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) csp.field_track_losses = pyomo.Param( doc="Solar field tracking parasitic loss [MWe]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) # csp.heat_trace_losses = pyomo.Param( # doc="Piping heat trace parasitic loss [MWe]", # default=0.0, @@ -147,79 +195,97 @@ def _create_receiver_parameters(csp): @staticmethod def _create_cycle_parameters(csp): + """ + Create parameters related to the power cycle. + + Args: + csp: CSP instance. + """ # Cost parameters csp.cost_per_cycle_generation = pyomo.Param( doc="Generation cost for power cycle [$/MWh]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.MWh) # Electric + units=u.USD / u.MWh, + ) # Electric csp.cost_per_cycle_start = pyomo.Param( doc="Fixed cost for power cycle start [$/start]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD) # $/start + units=u.USD, + ) # $/start csp.cost_per_change_thermal_input = pyomo.Param( doc="Penalty for change in power cycle thermal input [$/MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.MW) # $/(Delta)MW (thermal) + units=u.USD / u.MW, + ) # $/(Delta)MW (thermal) # Performance parameters csp.cycle_ambient_efficiency_correction = pyomo.Param( doc="Cycle efficiency ambient temperature adjustment [-]", within=pyomo.NonNegativeReals, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.condenser_losses = pyomo.Param( doc="Normalized condenser parasitic losses [-]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.cycle_required_startup_energy = pyomo.Param( doc="Required energy expended to start cycle [MWht]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) csp.cycle_nominal_efficiency = pyomo.Param( doc="Power cycle nominal efficiency [-]", default=0.0, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.cycle_performance_slope = pyomo.Param( doc="Slope of linear approximation of power cycle performance curve [MWe/MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.cycle_pumping_losses = pyomo.Param( doc="Cycle heat transfer fluid pumping power per unit energy expended [MWe/MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) csp.allowable_cycle_startup_power = pyomo.Param( doc="Allowable power per period for cycle start-up [MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) csp.minimum_cycle_thermal_power = pyomo.Param( doc="Minimum operational thermal power delivered to the power cycle [MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) csp.maximum_cycle_thermal_power = pyomo.Param( doc="Maximum operational thermal power delivered to the power cycle [MWt]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) # csp.minimum_cycle_power = pyomo.Param( # doc="Minimum cycle electric power output [MWe]", # default=0.0, @@ -231,7 +297,8 @@ def _create_cycle_parameters(csp): default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) ################################## # Variables # @@ -239,112 +306,151 @@ def _create_cycle_parameters(csp): @staticmethod def _create_storage_variables(csp): + """ + Create variables related to thermal energy storage. + + Args: + csp: CSP instance. + """ csp.thermal_energy_storage = pyomo.Var( doc="Thermal energy storage reserve quantity [MWht]", domain=pyomo.NonNegativeReals, bounds=(0, csp.storage_capacity), - units=u.MWh) + units=u.MWh, + ) # initial variables csp.previous_thermal_energy_storage = pyomo.Var( doc="Thermal energy storage reserve quantity at the beginning of the period [MWht]", domain=pyomo.NonNegativeReals, bounds=(0, csp.storage_capacity), - units=u.MWh) + units=u.MWh, + ) @staticmethod def _create_receiver_variables(csp): + """ + Create variables related to the receiver. + + Args: + csp: CSP instance. + """ csp.receiver_startup_inventory = pyomo.Var( doc="Receiver start-up energy inventory [MWht]", domain=pyomo.NonNegativeReals, - units=u.MWh) + units=u.MWh, + ) csp.receiver_thermal_power = pyomo.Var( doc="Thermal power delivered by the receiver [MWt]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) csp.receiver_startup_consumption = pyomo.Var( doc="Receiver start-up power consumption [MWt]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) csp.is_field_generating = pyomo.Var( doc="1 if solar field is generating 'usable' thermal power; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) csp.is_field_starting = pyomo.Var( doc="1 if solar field is starting up; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) csp.incur_field_start = pyomo.Var( doc="1 if solar field start-up penalty is incurred; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) # initial variables csp.previous_receiver_startup_inventory = pyomo.Var( doc="Previous receiver start-up energy inventory [MWht]", domain=pyomo.NonNegativeReals, - units=u.MWh) + units=u.MWh, + ) csp.was_field_generating = pyomo.Var( doc="1 if solar field was generating 'usable' thermal power in the previous time period; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) csp.was_field_starting = pyomo.Var( doc="1 if solar field was starting up in the previous time period; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) @staticmethod def _create_cycle_variables(csp): + """ + Create variables related to the power cycle. + + Args: + csp: CSP instance. + """ csp.system_load = pyomo.Var( - doc="Load of csp system [MWe]", - domain=pyomo.NonNegativeReals, - units=u.MW) + doc="Load of csp system [MWe]", domain=pyomo.NonNegativeReals, units=u.MW + ) csp.cycle_startup_inventory = pyomo.Var( doc="Cycle start-up energy inventory [MWht]", domain=pyomo.NonNegativeReals, - units=u.MWh) + units=u.MWh, + ) csp.cycle_generation = pyomo.Var( doc="Power cycle electricity generation [MWe]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) csp.cycle_thermal_ramp = pyomo.Var( doc="Power cycle positive change in thermal energy input [MWt]", domain=pyomo.NonNegativeReals, bounds=(0, csp.maximum_cycle_thermal_power), - units=u.MW) + units=u.MW, + ) csp.cycle_thermal_power = pyomo.Var( doc="Cycle thermal power utilization [MWt]", domain=pyomo.NonNegativeReals, bounds=(0, csp.maximum_cycle_thermal_power), - units=u.MW) + units=u.MW, + ) csp.is_cycle_generating = pyomo.Var( doc="1 if cycle is generating electric power; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) csp.is_cycle_starting = pyomo.Var( doc="1 if cycle is starting up; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) csp.incur_cycle_start = pyomo.Var( doc="1 if cycle start-up penalty is incurred; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) # Initial variables csp.previous_cycle_startup_inventory = pyomo.Var( doc="Previous cycle start-up energy inventory [MWht]", domain=pyomo.NonNegativeReals, - units=u.MWh) + units=u.MWh, + ) csp.previous_cycle_thermal_power = pyomo.Var( doc="Cycle thermal power in the previous period [MWt]", domain=pyomo.NonNegativeReals, bounds=(0, csp.maximum_cycle_thermal_power), - units=u.MW) + units=u.MW, + ) csp.was_cycle_generating = pyomo.Var( doc="1 if cycle was generating electric power in previous time period; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) csp.was_cycle_starting = pyomo.Var( doc="1 if cycle was starting up in previous time period; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) ################################## # Constraints # @@ -352,124 +458,221 @@ def _create_cycle_variables(csp): @staticmethod def _create_storage_constraints(csp): + """ + Create constraints related to thermal energy storage. + + Args: + csp: CSP instance. + """ csp.storage_inventory = pyomo.Constraint( doc="Thermal energy storage energy balance", - expr=(csp.thermal_energy_storage - csp.previous_thermal_energy_storage == - csp.time_duration * (csp.receiver_thermal_power - - (csp.allowable_cycle_startup_power * csp.is_cycle_starting - + csp.cycle_thermal_power) - ) - )) + expr=( + csp.thermal_energy_storage - csp.previous_thermal_energy_storage + == csp.time_duration + * ( + csp.receiver_thermal_power + - ( + csp.allowable_cycle_startup_power * csp.is_cycle_starting + + csp.cycle_thermal_power + ) + ) + ), + ) csp.receiver_startup = pyomo.Constraint( doc="If receiver is starting up, then there must be a sufficient charge level " - "in the TES in the previous time period", - expr=(csp.previous_thermal_energy_storage >= csp.time_duration * csp.receiver_startup_fraction - * (csp.maximum_cycle_thermal_power * (-3 + csp.is_field_starting - + csp.was_cycle_generating + csp.is_cycle_generating) - + csp.cycle_thermal_power)) + "in the TES in the previous time period", + expr=( + csp.previous_thermal_energy_storage + >= csp.time_duration + * csp.receiver_startup_fraction + * ( + csp.maximum_cycle_thermal_power + * ( + -3 + + csp.is_field_starting + + csp.was_cycle_generating + + csp.is_cycle_generating + ) + + csp.cycle_thermal_power + ) + ), ) @staticmethod def _create_receiver_constraints(csp): + """ + Create constraints related to the receiver. + + Args: + csp: CSP instance. + """ # Start-up csp.receiver_startup_inventory_balance = pyomo.Constraint( doc="Receiver startup energy inventory balance", - expr=csp.receiver_startup_inventory <= (csp.previous_receiver_startup_inventory - + csp.time_duration * csp.receiver_startup_consumption)) + expr=csp.receiver_startup_inventory + <= ( + csp.previous_receiver_startup_inventory + + csp.time_duration * csp.receiver_startup_consumption + ), + ) csp.receiver_startup_inventory_reset = pyomo.Constraint( doc="Resets receiver and/or field startup inventory when startup is completed", - expr=csp.receiver_startup_inventory <= csp.receiver_required_startup_energy * csp.is_field_starting) + expr=csp.receiver_startup_inventory + <= csp.receiver_required_startup_energy * csp.is_field_starting, + ) csp.receiver_operation_startup = pyomo.Constraint( doc="Thermal production is allowed only upon completion of start-up or operating in previous time period", - expr=csp.is_field_generating <= (csp.receiver_startup_inventory - / csp.receiver_required_startup_energy) + csp.was_field_generating) + expr=csp.is_field_generating + <= (csp.receiver_startup_inventory / csp.receiver_required_startup_energy) + + csp.was_field_generating, + ) csp.receiver_startup_delay = pyomo.Constraint( doc="If field previously was producing, it cannot startup this period", - expr=csp.is_field_starting + csp.was_field_generating <= 1) + expr=csp.is_field_starting + csp.was_field_generating <= 1, + ) csp.receiver_startup_limit = pyomo.Constraint( doc="Receiver and/or field startup energy consumption limit", - expr=csp.receiver_startup_consumption <= (csp.allowable_receiver_startup_power - * csp.is_field_starting)) + expr=csp.receiver_startup_consumption + <= (csp.allowable_receiver_startup_power * csp.is_field_starting), + ) csp.receiver_startup_cut = pyomo.Constraint( doc="Receiver and/or field trivial resource startup cut", - expr=csp.is_field_starting <= csp.available_thermal_generation / csp.minimum_receiver_power) + expr=csp.is_field_starting + <= csp.available_thermal_generation / csp.minimum_receiver_power, + ) # Supply and demand csp.receiver_energy_balance = pyomo.Constraint( doc="Receiver generation and startup usage must be below available", - expr=csp.available_thermal_generation >= csp.receiver_thermal_power + csp.receiver_startup_consumption) + expr=csp.available_thermal_generation + >= csp.receiver_thermal_power + csp.receiver_startup_consumption, + ) csp.maximum_field_generation = pyomo.Constraint( doc="Receiver maximum generation limit", - expr=csp.receiver_thermal_power <= csp.available_thermal_generation * csp.is_field_generating) + expr=csp.receiver_thermal_power + <= csp.available_thermal_generation * csp.is_field_generating, + ) csp.minimum_field_generation = pyomo.Constraint( doc="Receiver minimum generation limit", - expr=csp.receiver_thermal_power >= csp.minimum_receiver_power * csp.is_field_generating) + expr=csp.receiver_thermal_power + >= csp.minimum_receiver_power * csp.is_field_generating, + ) csp.receiver_generation_cut = pyomo.Constraint( doc="Receiver and/or field trivial resource generation cut", - expr=csp.is_field_generating <= csp.available_thermal_generation / csp.minimum_receiver_power) + expr=csp.is_field_generating + <= csp.available_thermal_generation / csp.minimum_receiver_power, + ) # Logic associated with receiver modes csp.field_startup = pyomo.Constraint( doc="Ensures that field start is accounted", - expr=csp.incur_field_start >= csp.is_field_starting - csp.was_field_starting) + expr=csp.incur_field_start + >= csp.is_field_starting - csp.was_field_starting, + ) @staticmethod def _create_cycle_constraints(csp): + """ + Create constraints related to the power cycle. + + Args: + csp: CSP instance. + """ # Start-up csp.cycle_startup_inventory_balance = pyomo.Constraint( doc="Cycle startup energy inventory balance", - expr=csp.cycle_startup_inventory <= (csp.previous_cycle_startup_inventory - + (csp.time_duration - * csp.allowable_cycle_startup_power - * csp.is_cycle_starting))) + expr=csp.cycle_startup_inventory + <= ( + csp.previous_cycle_startup_inventory + + ( + csp.time_duration + * csp.allowable_cycle_startup_power + * csp.is_cycle_starting + ) + ), + ) csp.cycle_startup_inventory_reset = pyomo.Constraint( doc="Resets power cycle startup inventory when startup is completed", - expr=csp.cycle_startup_inventory <= csp.cycle_required_startup_energy * csp.is_cycle_starting) + expr=csp.cycle_startup_inventory + <= csp.cycle_required_startup_energy * csp.is_cycle_starting, + ) csp.cycle_operation_startup = pyomo.Constraint( doc="Electric production is allowed only upon completion of start-up or operating in previous time period", - expr=csp.is_cycle_generating <= (csp.cycle_startup_inventory - / csp.cycle_required_startup_energy) + csp.was_cycle_generating) + expr=csp.is_cycle_generating + <= (csp.cycle_startup_inventory / csp.cycle_required_startup_energy) + + csp.was_cycle_generating, + ) csp.cycle_startup_delay = pyomo.Constraint( doc="If cycle previously was generating, it cannot startup this period", - expr=csp.is_cycle_starting + csp.was_cycle_generating <= 1) + expr=csp.is_cycle_starting + csp.was_cycle_generating <= 1, + ) # Supply and demand # TODO: do we penalize start-up on valuable hours? I don't think I need this... csp.maximum_cycle_thermal_consumption_startup = pyomo.Constraint( doc="Power cycle maximum thermal energy consumption maximum limit including startup", - expr=(csp.cycle_thermal_power - + (csp.cycle_required_startup_energy / csp.time_duration) * csp.is_cycle_starting - <= csp.maximum_cycle_thermal_power)) + expr=( + csp.cycle_thermal_power + + (csp.cycle_required_startup_energy / csp.time_duration) + * csp.is_cycle_starting + <= csp.maximum_cycle_thermal_power + ), + ) csp.maximum_cycle_thermal_consumption = pyomo.Constraint( doc="Power cycle maximum thermal energy consumption maximum limit", - expr=csp.cycle_thermal_power <= csp.maximum_cycle_thermal_power * csp.is_cycle_generating) + expr=csp.cycle_thermal_power + <= csp.maximum_cycle_thermal_power * csp.is_cycle_generating, + ) csp.minimum_cycle_thermal_consumption = pyomo.Constraint( doc="Power cycle minimum thermal energy consumption minimum limit", - expr=csp.cycle_thermal_power >= csp.minimum_cycle_thermal_power * csp.is_cycle_generating) + expr=csp.cycle_thermal_power + >= csp.minimum_cycle_thermal_power * csp.is_cycle_generating, + ) csp.cycle_performance_curve = pyomo.Constraint( doc="Power cycle relationship between electrical power and thermal input with corrections " - "for ambient temperature", - expr=(csp.cycle_generation == - (csp.cycle_ambient_efficiency_correction / csp.cycle_nominal_efficiency) - * (csp.cycle_performance_slope * csp.cycle_thermal_power - + (csp.maximum_cycle_power - csp.cycle_performance_slope - * csp.maximum_cycle_thermal_power) * csp.is_cycle_generating))) + "for ambient temperature", + expr=( + csp.cycle_generation + == ( + csp.cycle_ambient_efficiency_correction + / csp.cycle_nominal_efficiency + ) + * ( + csp.cycle_performance_slope * csp.cycle_thermal_power + + ( + csp.maximum_cycle_power + - csp.cycle_performance_slope * csp.maximum_cycle_thermal_power + ) + * csp.is_cycle_generating + ) + ), + ) csp.cycle_thermal_ramp_constraint = pyomo.Constraint( doc="Positive ramping of power cycle thermal power", - expr=csp.cycle_thermal_ramp >= csp.cycle_thermal_power - csp.previous_cycle_thermal_power) + expr=csp.cycle_thermal_ramp + >= csp.cycle_thermal_power - csp.previous_cycle_thermal_power, + ) # System load csp.generation_balance = pyomo.Constraint( doc="Calculates csp system load for grid model", - expr=csp.system_load == (csp.cycle_generation * csp.condenser_losses - + csp.receiver_pumping_losses * (csp.receiver_thermal_power - + csp.receiver_startup_consumption) - + csp.cycle_pumping_losses * (csp.cycle_thermal_power - + (csp.allowable_cycle_startup_power - * csp.is_cycle_starting)) - + csp.field_track_losses * csp.is_field_generating - # + csp.heat_trace_losses * csp.is_field_starting - + (csp.field_startup_losses/csp.time_duration) * csp.is_field_starting)) + expr=csp.system_load + == ( + csp.cycle_generation * csp.condenser_losses + + csp.receiver_pumping_losses + * (csp.receiver_thermal_power + csp.receiver_startup_consumption) + + csp.cycle_pumping_losses + * ( + csp.cycle_thermal_power + + (csp.allowable_cycle_startup_power * csp.is_cycle_starting) + ) + + csp.field_track_losses * csp.is_field_generating + # + csp.heat_trace_losses * csp.is_field_starting + + (csp.field_startup_losses / csp.time_duration) * csp.is_field_starting + ), + ) # Logic governing cycle modes csp.cycle_startup = pyomo.Constraint( doc="Ensures that cycle start is accounted", - expr=csp.incur_cycle_start >= csp.is_cycle_starting - csp.was_cycle_starting) + expr=csp.incur_cycle_start + >= csp.is_cycle_starting - csp.was_cycle_starting, + ) ################################## # Ports # @@ -477,6 +680,12 @@ def _create_cycle_constraints(csp): @staticmethod def _create_csp_port(csp): + """ + Create pyomo ports related to CSP instance. + + Args: + csp: CSP instance. + """ csp.port = Port() csp.port.add(csp.cycle_generation) csp.port.add(csp.system_load) @@ -486,6 +695,7 @@ def _create_csp_port(csp): ################################## def _create_linking_constraints(self): + """Create linking constraints for storage, receiver and cycle.""" self._create_storage_linking_constraints() self._create_receiver_linking_constraints() self._create_cycle_linking_constraints() @@ -495,171 +705,278 @@ def _create_linking_constraints(self): ################################## def _create_storage_linking_constraints(self): + """Create constraints for linking storage.""" self.model.initial_thermal_energy_storage = pyomo.Param( doc="Initial thermal energy storage reserve quantity at beginning of the horizon [MWht]", default=0.0, within=pyomo.NonNegativeReals, # validate= # TODO: Might be worth looking into mutable=True, - units=u.MWh) + units=u.MWh, + ) def tes_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].previous_thermal_energy_storage == self.model.initial_thermal_energy_storage - return self.blocks[t].previous_thermal_energy_storage == self.blocks[t - 1].thermal_energy_storage + return ( + self.blocks[t].previous_thermal_energy_storage + == self.model.initial_thermal_energy_storage + ) + return ( + self.blocks[t].previous_thermal_energy_storage + == self.blocks[t - 1].thermal_energy_storage + ) + self.model.tes_linking = pyomo.Constraint( self.blocks.index_set(), doc="Thermal energy storage block linking constraint", - rule=tes_linking_rule) + rule=tes_linking_rule, + ) def _create_receiver_linking_constraints(self): + """Create constraints for linking receiver.""" self.model.initial_receiver_startup_inventory = pyomo.Param( doc="Initial receiver start-up energy inventory at beginning of the horizon [MWht]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) self.model.is_field_generating_initial = pyomo.Param( doc="1 if solar field is generating 'usable' thermal power at beginning of the horizon; 0 Otherwise [-]", default=0.0, within=pyomo.Binary, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) self.model.is_field_starting_initial = pyomo.Param( doc="1 if solar field is starting up at beginning of the horizon; 0 Otherwise [-]", default=0.0, within=pyomo.Binary, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) def receiver_startup_inventory_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].previous_receiver_startup_inventory == self.model.initial_receiver_startup_inventory - return self.blocks[t].previous_receiver_startup_inventory == self.blocks[t - 1].receiver_startup_inventory + return ( + self.blocks[t].previous_receiver_startup_inventory + == self.model.initial_receiver_startup_inventory + ) + return ( + self.blocks[t].previous_receiver_startup_inventory + == self.blocks[t - 1].receiver_startup_inventory + ) + self.model.receiver_startup_inventory_linking = pyomo.Constraint( self.blocks.index_set(), doc="Receiver startup inventory block linking constraint", - rule=receiver_startup_inventory_linking_rule) + rule=receiver_startup_inventory_linking_rule, + ) def field_generating_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].was_field_generating == self.model.is_field_generating_initial - return self.blocks[t].was_field_generating == self.blocks[t - 1].is_field_generating + return ( + self.blocks[t].was_field_generating + == self.model.is_field_generating_initial + ) + return ( + self.blocks[t].was_field_generating + == self.blocks[t - 1].is_field_generating + ) + self.model.field_generating_linking = pyomo.Constraint( self.blocks.index_set(), doc="Is field generating binary block linking constraint", - rule=field_generating_linking_rule) + rule=field_generating_linking_rule, + ) def field_starting_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].was_field_starting == self.model.is_field_starting_initial - return self.blocks[t].was_field_starting == self.blocks[t - 1].is_field_starting + return ( + self.blocks[t].was_field_starting + == self.model.is_field_starting_initial + ) + return ( + self.blocks[t].was_field_starting + == self.blocks[t - 1].is_field_starting + ) + self.model.field_starting_linking = pyomo.Constraint( self.blocks.index_set(), doc="Is field starting up binary block linking constraint", - rule=field_starting_linking_rule) + rule=field_starting_linking_rule, + ) def _create_cycle_linking_constraints(self): + """Create constraints for linking cycle.""" self.model.initial_cycle_startup_inventory = pyomo.Param( doc="Initial cycle start-up energy inventory at beginning of the horizon [MWht]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) self.model.initial_cycle_thermal_power = pyomo.Param( doc="Initial cycle thermal power at beginning of the horizon [MWt]", default=0.0, within=pyomo.NonNegativeReals, # validate= # TODO: bounds->(0, csp.maximum_cycle_thermal_power), Sec. 4.7.1 mutable=True, - units=u.MW) + units=u.MW, + ) self.model.is_cycle_generating_initial = pyomo.Param( doc="1 if cycle is generating electric power at beginning of the horizon; 0 Otherwise [-]", default=0.0, within=pyomo.Binary, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) self.model.is_cycle_starting_initial = pyomo.Param( doc="1 if cycle is starting up at beginning of the horizon; 0 Otherwise [-]", default=0.0, within=pyomo.Binary, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) def cycle_startup_inventory_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].previous_cycle_startup_inventory == self.model.initial_cycle_startup_inventory - return self.blocks[t].previous_cycle_startup_inventory == self.blocks[t - 1].cycle_startup_inventory + return ( + self.blocks[t].previous_cycle_startup_inventory + == self.model.initial_cycle_startup_inventory + ) + return ( + self.blocks[t].previous_cycle_startup_inventory + == self.blocks[t - 1].cycle_startup_inventory + ) + self.model.cycle_startup_inventory_linking = pyomo.Constraint( self.blocks.index_set(), doc="Cycle startup inventory block linking constraint", - rule=cycle_startup_inventory_linking_rule) + rule=cycle_startup_inventory_linking_rule, + ) def cycle_thermal_power_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].previous_cycle_thermal_power == self.model.initial_cycle_thermal_power - return self.blocks[t].previous_cycle_thermal_power == self.blocks[t - 1].cycle_thermal_power + return ( + self.blocks[t].previous_cycle_thermal_power + == self.model.initial_cycle_thermal_power + ) + return ( + self.blocks[t].previous_cycle_thermal_power + == self.blocks[t - 1].cycle_thermal_power + ) + self.model.cycle_thermal_power_linking = pyomo.Constraint( self.blocks.index_set(), doc="Cycle thermal power block linking constraint", - rule=cycle_thermal_power_linking_rule) + rule=cycle_thermal_power_linking_rule, + ) def cycle_generating_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].was_cycle_generating == self.model.is_cycle_generating_initial - return self.blocks[t].was_cycle_generating == self.blocks[t - 1].is_cycle_generating + return ( + self.blocks[t].was_cycle_generating + == self.model.is_cycle_generating_initial + ) + return ( + self.blocks[t].was_cycle_generating + == self.blocks[t - 1].is_cycle_generating + ) + self.model.cycle_generating_linking = pyomo.Constraint( self.blocks.index_set(), doc="Is cycle generating binary block linking constraint", - rule=cycle_generating_linking_rule) + rule=cycle_generating_linking_rule, + ) def cycle_starting_linking_rule(m, t): if t == self.blocks.index_set().first(): - return self.blocks[t].was_cycle_starting == self.model.is_cycle_starting_initial - return self.blocks[t].was_cycle_starting == self.blocks[t - 1].is_cycle_starting + return ( + self.blocks[t].was_cycle_starting + == self.model.is_cycle_starting_initial + ) + return ( + self.blocks[t].was_cycle_starting + == self.blocks[t - 1].is_cycle_starting + ) + self.model.cycle_starting_linking = pyomo.Constraint( self.blocks.index_set(), doc="Is cycle starting up binary block linking constraint", - rule=cycle_starting_linking_rule) + rule=cycle_starting_linking_rule, + ) def initialize_parameters(self): + """Initialize parameters for the CSP model.""" csp = self._system_model cycle_rated_thermal = csp.cycle_thermal_rating field_rated_thermal = csp.field_thermal_rating # Cost Parameters - self.cost_per_field_generation = self.objective_cost_terms['cost_per_field_generation'] - self.cost_per_field_start = self.objective_cost_terms['cost_per_field_start_rel'] * field_rated_thermal - self.cost_per_cycle_generation = self.objective_cost_terms['cost_per_cycle_generation'] - self.cost_per_cycle_start = self.objective_cost_terms['cost_per_cycle_start_rel'] * csp.value('P_ref') - self.cost_per_change_thermal_input = self.objective_cost_terms['cost_per_change_thermal_input'] + self.cost_per_field_generation = self.objective_cost_terms[ + "cost_per_field_generation" + ] + self.cost_per_field_start = ( + self.objective_cost_terms["cost_per_field_start_rel"] * field_rated_thermal + ) + self.cost_per_cycle_generation = self.objective_cost_terms[ + "cost_per_cycle_generation" + ] + self.cost_per_cycle_start = self.objective_cost_terms[ + "cost_per_cycle_start_rel" + ] * csp.value("P_ref") + self.cost_per_change_thermal_input = self.objective_cost_terms[ + "cost_per_change_thermal_input" + ] # Solar field and thermal energy storage performance parameters - self.field_startup_losses = csp.value('p_start') * csp.number_of_reflector_units / 1e3 - self.receiver_required_startup_energy = csp.value('rec_qf_delay') * field_rated_thermal + self.field_startup_losses = ( + csp.value("p_start") * csp.number_of_reflector_units / 1e3 + ) + self.receiver_required_startup_energy = ( + csp.value("rec_qf_delay") * field_rated_thermal + ) self.storage_capacity = csp.tes_hours * cycle_rated_thermal - self.minimum_receiver_power = csp.minimum_receiver_power_fraction * field_rated_thermal - self.allowable_receiver_startup_power = self.receiver_required_startup_energy / csp.value('rec_su_delay') + self.minimum_receiver_power = ( + csp.minimum_receiver_power_fraction * field_rated_thermal + ) + self.allowable_receiver_startup_power = ( + self.receiver_required_startup_energy / csp.value("rec_su_delay") + ) self.receiver_pumping_losses = csp.estimate_receiver_pumping_parasitic() self.field_track_losses = csp.field_tracking_power - #self.heat_trace_losses = 0.00163 * field_rated_thermal # TODO: need to update for troughs + # self.heat_trace_losses = 0.00163 * field_rated_thermal # TODO: need to update for troughs # Power cycle performance - self.cycle_required_startup_energy = csp.value('startup_frac') * cycle_rated_thermal + self.cycle_required_startup_energy = ( + csp.value("startup_frac") * cycle_rated_thermal + ) self.cycle_nominal_efficiency = csp.cycle_nominal_efficiency design_mass_flow = csp.get_cycle_design_mass_flow() - self.cycle_pumping_losses = csp.value('pb_pump_coef') * design_mass_flow / (cycle_rated_thermal * 1e3) - self.allowable_cycle_startup_power = self.cycle_required_startup_energy / csp.value('startup_time') - self.minimum_cycle_thermal_power = csp.value('cycle_cutoff_frac') * cycle_rated_thermal - self.maximum_cycle_thermal_power = csp.value('cycle_max_frac') * cycle_rated_thermal + self.cycle_pumping_losses = ( + csp.value("pb_pump_coef") * design_mass_flow / (cycle_rated_thermal * 1e3) + ) + self.allowable_cycle_startup_power = ( + self.cycle_required_startup_energy / csp.value("startup_time") + ) + self.minimum_cycle_thermal_power = ( + csp.value("cycle_cutoff_frac") * cycle_rated_thermal + ) + self.maximum_cycle_thermal_power = ( + csp.value("cycle_max_frac") * cycle_rated_thermal + ) self.set_part_load_cycle_parameters() def update_time_series_parameters(self, start_time: int): """ Sets up SSC simulation to get time series performance parameters after simulation. - : param start_time: hour of the year starting dispatch horizon + + Args: + start_time (int): Hour of the year starting dispatch horizon. + """ n_horizon = len(self.blocks.index_set()) self.time_duration = [1.0] * n_horizon # assume hourly for now @@ -669,13 +986,15 @@ def update_time_series_parameters(self, start_time: int): temperature = list(self._system_model.year_weather_df.Temperature.values) if start_time + n_horizon > len(thermal_resource): field_gen = list(thermal_resource[start_time:]) - field_gen.extend(list(thermal_resource[0:n_horizon - len(field_gen)])) + field_gen.extend(list(thermal_resource[0 : n_horizon - len(field_gen)])) dry_bulb_temperature = list(temperature[start_time:]) - dry_bulb_temperature.extend(list(temperature[0:n_horizon - len(dry_bulb_temperature)])) + dry_bulb_temperature.extend( + list(temperature[0 : n_horizon - len(dry_bulb_temperature)]) + ) else: - field_gen = thermal_resource[start_time:start_time + n_horizon] - dry_bulb_temperature = temperature[start_time:start_time + n_horizon] + field_gen = thermal_resource[start_time : start_time + n_horizon] + dry_bulb_temperature = temperature[start_time : start_time + n_horizon] self.available_thermal_generation = field_gen # Set cycle performance parameters that depend on ambient temperature @@ -688,82 +1007,168 @@ def set_part_load_cycle_parameters(self): """Set parameters in dispatch model for off-design cycle performance.""" # --- Cycle part-load efficiency tables = self._system_model.cycle_efficiency_tables - if 'cycle_eff_load_table' in tables: + if "cycle_eff_load_table" in tables: q_pb_design = self._system_model.cycle_thermal_rating - num_pts = len(tables['cycle_eff_load_table']) - norm_heat_pts = [tables['cycle_eff_load_table'][i][0] / q_pb_design for i in range(num_pts)] # Load fraction - efficiency_pts = [tables['cycle_eff_load_table'][i][1] for i in range(num_pts)] # Efficiency + num_pts = len(tables["cycle_eff_load_table"]) + norm_heat_pts = [ + tables["cycle_eff_load_table"][i][0] / q_pb_design + for i in range(num_pts) + ] # Load fraction + efficiency_pts = [ + tables["cycle_eff_load_table"][i][1] for i in range(num_pts) + ] # Efficiency self.set_linearized_cycle_part_load_params(norm_heat_pts, efficiency_pts) - elif 'ud_ind_od' in tables: + elif "ud_ind_od" in tables: # Tables not returned from ssc, but can be taken from user-defined cycle inputs - D = self.interpret_user_defined_cycle_data(tables['ud_ind_od']) - k = 3 * D['nT'] + D['nm'] - norm_heat_pts = D['mpts'] # Load fraction - efficiency_pts = [self._system_model.cycle_nominal_efficiency * (tables['ud_ind_od'][k + p][3] / tables['ud_ind_od'][k + p][4]) - for p in range(len(norm_heat_pts))] # Efficiency + D = self.interpret_user_defined_cycle_data(tables["ud_ind_od"]) + k = 3 * D["nT"] + D["nm"] + norm_heat_pts = D["mpts"] # Load fraction + efficiency_pts = [ + self._system_model.cycle_nominal_efficiency + * (tables["ud_ind_od"][k + p][3] / tables["ud_ind_od"][k + p][4]) + for p in range(len(norm_heat_pts)) + ] # Efficiency self.set_linearized_cycle_part_load_params(norm_heat_pts, efficiency_pts) else: - print('WARNING: Dispatch optimization cycle part-load efficiency is not set. ' - 'Defaulting to constant efficiency vs load.') + print( + "WARNING: Dispatch optimization cycle part-load efficiency is not set. " + "Defaulting to constant efficiency vs load." + ) self.cycle_performance_slope = self._system_model.cycle_nominal_efficiency # self.minimum_cycle_power = self.minimum_cycle_thermal_power * self._system_model.cycle_nominal_efficiency - self.maximum_cycle_power = self.maximum_cycle_thermal_power * self._system_model.cycle_nominal_efficiency + self.maximum_cycle_power = ( + self.maximum_cycle_thermal_power + * self._system_model.cycle_nominal_efficiency + ) def set_linearized_cycle_part_load_params(self, norm_heat_pts, efficiency_pts): + """Set linearized part-load parameters for the power cycle. + + Args: + norm_heat_pts (list): Normalized heat points for the power cycle. + efficiency_pts (list): Efficiency points for the power cycle. + """ q_pb_design = self._system_model.cycle_thermal_rating - fpts = [self._system_model.value('cycle_cutoff_frac'), self._system_model.value('cycle_max_frac')] + fpts = [ + self._system_model.value("cycle_cutoff_frac"), + self._system_model.value("cycle_max_frac"), + ] step = norm_heat_pts[1] - norm_heat_pts[0] - q, eta = [ [] for v in range(2)] + q, eta = [[] for v in range(2)] for j in range(2): # Find first point in user-defined array of load fractions - p = max(0, min(int((fpts[j] - norm_heat_pts[0]) / step), len(norm_heat_pts) - 2)) - eta.append(efficiency_pts[p] + (efficiency_pts[p + 1] - efficiency_pts[p]) / step * (fpts[j] - norm_heat_pts[p])) - q.append(fpts[j]*q_pb_design) - etap = (q[1]*eta[1]-q[0]*eta[0])/(q[1]-q[0]) - b = q[1]*(eta[1] - etap) + p = max( + 0, min(int((fpts[j] - norm_heat_pts[0]) / step), len(norm_heat_pts) - 2) + ) + eta.append( + efficiency_pts[p] + + (efficiency_pts[p + 1] - efficiency_pts[p]) + / step + * (fpts[j] - norm_heat_pts[p]) + ) + q.append(fpts[j] * q_pb_design) + etap = (q[1] * eta[1] - q[0] * eta[0]) / (q[1] - q[0]) + b = q[1] * (eta[1] - etap) self.cycle_performance_slope = etap # self.minimum_cycle_power = b + self.minimum_cycle_thermal_power * self.cycle_performance_slope - self.maximum_cycle_power = b + self.maximum_cycle_thermal_power * self.cycle_performance_slope + self.maximum_cycle_power = ( + b + self.maximum_cycle_thermal_power * self.cycle_performance_slope + ) return def set_ambient_temperature_cycle_parameters(self, dry_bulb_temperature): - """Set ambient temperature dependent cycle performance parameters.""" + """ + Set ambient temperature dependent cycle performance parameters. + + Args: + dry_bulb_temperature (float or list): Ambient dry bulb temperature(s) [°C]. + + Returns: + None + + Notes: + This method sets up ambient temperature dependent cycle performance parameters + such as cycle efficiency corrections and condenser losses based on the provided + dry bulb temperature(s). + """ # --- Cycle ambient-temperature efficiency corrections tables = self._system_model.cycle_efficiency_tables - if 'cycle_eff_Tdb_table' in tables: - nT = len(tables['cycle_eff_Tdb_table']) - Tpts = [tables['cycle_eff_Tdb_table'][i][0] for i in range(nT)] - efficiency_pts = [tables['cycle_eff_Tdb_table'][i][1] * self._system_model.cycle_nominal_efficiency for i in range(nT)] # Efficiency - wcondfpts = [tables['cycle_wcond_Tdb_table'][i][1] for i in range(nT)] # Fraction of cycle design gross output consumed by cooling - self.set_cycle_ambient_corrections(dry_bulb_temperature, Tpts, efficiency_pts, wcondfpts) - elif 'ud_ind_od' in tables: + if "cycle_eff_Tdb_table" in tables: + nT = len(tables["cycle_eff_Tdb_table"]) + Tpts = [tables["cycle_eff_Tdb_table"][i][0] for i in range(nT)] + efficiency_pts = [ + tables["cycle_eff_Tdb_table"][i][1] + * self._system_model.cycle_nominal_efficiency + for i in range(nT) + ] # Efficiency + wcondfpts = [ + tables["cycle_wcond_Tdb_table"][i][1] for i in range(nT) + ] # Fraction of cycle design gross output consumed by cooling + self.set_cycle_ambient_corrections( + dry_bulb_temperature, Tpts, efficiency_pts, wcondfpts + ) + elif "ud_ind_od" in tables: # Tables not returned from ssc, but can be taken from user-defined cycle inputs - D = self.interpret_user_defined_cycle_data(tables['ud_ind_od']) - k = 3 * D['nT'] + 3 * D['nm'] + D[ - 'nTamb'] # first index in udpc data corresponding to performance at design point HTF T, and design point mass flow - npts = D['nTamb'] - efficiency_pts = [self._system_model.cycle_nominal_efficiency * (tables['ud_ind_od'][j][3] / tables['ud_ind_od'][j][4]) - for j in range(k, k + npts)] # Efficiency - wcondfpts = [(self._system_model.value('ud_f_W_dot_cool_des') / 100.) * tables['ud_ind_od'][j][5] for j in - range(k, k + npts)] # Fraction of cycle design gross output consumed by cooling - self.set_cycle_ambient_corrections(dry_bulb_temperature, D['Tambpts'], efficiency_pts, wcondfpts) + D = self.interpret_user_defined_cycle_data(tables["ud_ind_od"]) + k = ( + 3 * D["nT"] + 3 * D["nm"] + D["nTamb"] + ) # first index in udpc data corresponding to performance at design point HTF T, and design point mass flow + npts = D["nTamb"] + efficiency_pts = [ + self._system_model.cycle_nominal_efficiency + * (tables["ud_ind_od"][j][3] / tables["ud_ind_od"][j][4]) + for j in range(k, k + npts) + ] # Efficiency + wcondfpts = [ + (self._system_model.value("ud_f_W_dot_cool_des") / 100.0) + * tables["ud_ind_od"][j][5] + for j in range(k, k + npts) + ] # Fraction of cycle design gross output consumed by cooling + self.set_cycle_ambient_corrections( + dry_bulb_temperature, D["Tambpts"], efficiency_pts, wcondfpts + ) else: - print('WARNING: Dispatch optimization cycle ambient temperature corrections are not set up.') + print( + "WARNING: Dispatch optimization cycle ambient temperature corrections are not set up." + ) n = len(dry_bulb_temperature) - self.cycle_ambient_efficiency_correction = [self._system_model.cycle_nominal_efficiency] * n + self.cycle_ambient_efficiency_correction = [ + self._system_model.cycle_nominal_efficiency + ] * n self.condenser_losses = [0.0] * n return def set_cycle_ambient_corrections(self, Tdb, Tpts, etapts, wcondfpts): - n = len(Tdb) # Tdb = set of ambient temperature points for each dispatch time step - npts = len(Tpts) # Tpts = ambient temperature points with tabulated values - cycle_ambient_efficiency_correction = [1.0]*n - condenser_losses = [0.0]*n + """ + Set cycle ambient corrections based on ambient temperature. + + Args: + Tdb (float or list): Ambient temperature(s) for each dispatch time step [°C]. + Tpts (list): Ambient temperature points with tabulated values [°C]. + etapts (list): Efficiency values corresponding to each Tpts. + wcondfpts (list): Fraction of cycle design gross output consumed by cooling corresponding to each Tpts. + + Returns: + None + + Notes: + This method calculates cycle ambient efficiency correction and condenser losses based on the provided + ambient temperature(s) and tabulated values. The corrections are set for each dispatch time step. + + """ + n = len( + Tdb + ) # Tdb = set of ambient temperature points for each dispatch time step + npts = len(Tpts) # Tpts = ambient temperature points with tabulated values + cycle_ambient_efficiency_correction = [1.0] * n + condenser_losses = [0.0] * n Tstep = Tpts[1] - Tpts[0] for j in range(n): - i = max(0, min( int((Tdb[j] - Tpts[0]) / Tstep), npts-2) ) + i = max(0, min(int((Tdb[j] - Tpts[0]) / Tstep), npts - 2)) r = (Tdb[j] - Tpts[i]) / Tstep - cycle_ambient_efficiency_correction[j] = etapts[i] + (etapts[i + 1] - etapts[i]) * r + cycle_ambient_efficiency_correction[j] = ( + etapts[i] + (etapts[i + 1] - etapts[i]) * r + ) condenser_losses[j] = wcondfpts[i] + (wcondfpts[i + 1] - wcondfpts[i]) * r self.cycle_ambient_efficiency_correction = cycle_ambient_efficiency_correction self.condenser_losses = condenser_losses @@ -771,79 +1176,163 @@ def set_cycle_ambient_corrections(self, Tdb, Tpts, etapts, wcondfpts): @staticmethod def interpret_user_defined_cycle_data(ud_ind_od): + """ + Interpret user-defined cycle data. + + Args: + ud_ind_od (list): User-defined cycle data. + + Returns: + dict: Dictionary containing interpreted data with keys: + - 'nT': Number of temperature points + - 'Tpts': Ambient temperature points + - 'Tlevels': Levels of temperature + - 'nm': Number of mass flow rate points + - 'mpts': Mass flow rate points + - 'mlevels': Levels of mass flow rate + - 'nTamb': Number of ambient temperature points + - 'Tambpts': Ambient temperature points + - 'Tamblevels': Levels of ambient temperature + + Notes: + This method interprets user-defined cycle data and organizes it into a dictionary + containing relevant information about temperature points, mass flow rate points, and + ambient temperature points. + """ + data = np.array(ud_ind_od) i0 = 0 nT = np.where(np.diff(data[i0::, 0]) < 0)[0][0] + 1 - Tpts = data[i0:i0 + nT, 0] + Tpts = data[i0 : i0 + nT, 0] mlevels = [data[j, 1] for j in [i0, i0 + nT, i0 + 2 * nT]] i0 = 3 * nT nm = np.where(np.diff(data[i0::, 1]) < 0)[0][0] + 1 - mpts = data[i0:i0 + nm, 1] + mpts = data[i0 : i0 + nm, 1] Tamblevels = [data[j, 2] for j in [i0, i0 + nm, i0 + 2 * nm]] i0 = 3 * nT + 3 * nm nTamb = np.where(np.diff(data[i0::, 2]) < 0)[0][0] + 1 - Tambpts = data[i0:i0 + nTamb, 2] + Tambpts = data[i0 : i0 + nTamb, 2] Tlevels = [data[j, 0] for j in [i0, i0 + nm, i0 + 2 * nm]] - return {'nT': nT, 'Tpts': Tpts, 'Tlevels': Tlevels, 'nm': nm, 'mpts': mpts, 'mlevels': mlevels, 'nTamb': nTamb, - 'Tambpts': Tambpts, 'Tamblevels': Tamblevels} + return { + "nT": nT, + "Tpts": Tpts, + "Tlevels": Tlevels, + "nm": nm, + "mpts": mpts, + "mlevels": mlevels, + "nTamb": nTamb, + "Tambpts": Tambpts, + "Tamblevels": Tamblevels, + } def set_receiver_require_startup_time_fraction(self, field_gen: list): - """Estimates the fraction of time period required for receiver start-up.""" - self.min_receiver_start_time = self._system_model.value('rec_su_delay') + """ + Estimates the fraction of time period required for receiver start-up. + + Args: + field_gen (list): Field generation profile. + + Notes: + This method estimates the fraction of time period required for the receiver start-up + based on the field generation profile. + + """ + self.min_receiver_start_time = self._system_model.value("rec_su_delay") su_fraction = [1.0] * len(field_gen) epsilon = 1e-6 time_duration = self.time_duration for i in range(len(field_gen)): - su_fraction[i] = min(1.0, - max(self.min_receiver_start_time / time_duration[i], - self.receiver_required_startup_energy / max(1e-6, - field_gen[i] * time_duration[i]) - ) - ) + su_fraction[i] = min( + 1.0, + max( + self.min_receiver_start_time / time_duration[i], + self.receiver_required_startup_energy + / max(1e-6, field_gen[i] * time_duration[i]), + ), + ) self.receiver_startup_fraction = su_fraction def update_initial_conditions(self): + """This method updates the initial conditions for the dispatch optimization, + including the initial thermal energy storage, initial cycle startup inventory, + and initial cycle thermal power. + """ csp = self._system_model m_des = csp.get_design_storage_mass() - m_hot = csp.initial_tes_hot_mass_fraction * m_des # Available active mass in hot tank - cp = csp.get_cp_htf(0.5 * (csp.plant_state['T_tank_hot_init'] + csp.htf_cold_design_temperature)) # J/kg/K - self.initial_thermal_energy_storage = min(self.storage_capacity, - m_hot * cp * (csp.plant_state['T_tank_hot_init'] - - csp.htf_cold_design_temperature) * 1.e-6 / 3600) + m_hot = ( + csp.initial_tes_hot_mass_fraction * m_des + ) # Available active mass in hot tank + cp = csp.get_cp_htf( + 0.5 * (csp.plant_state["T_tank_hot_init"] + csp.htf_cold_design_temperature) + ) # J/kg/K + self.initial_thermal_energy_storage = min( + self.storage_capacity, + m_hot + * cp + * (csp.plant_state["T_tank_hot_init"] - csp.htf_cold_design_temperature) + * 1.0e-6 + / 3600, + ) - self.is_field_generating_initial = (csp.plant_state['rec_op_mode_initial'] == 2) - self.is_field_starting_initial = (csp.plant_state['rec_op_mode_initial'] == 1) + self.is_field_generating_initial = csp.plant_state["rec_op_mode_initial"] == 2 + self.is_field_starting_initial = csp.plant_state["rec_op_mode_initial"] == 1 # Initial startup energy accumulated # ssc seems to report nan when startup is completed - if csp.plant_state['pc_startup_energy_remain_initial'] != csp.plant_state['pc_startup_energy_remain_initial']: + if ( + csp.plant_state["pc_startup_energy_remain_initial"] + != csp.plant_state["pc_startup_energy_remain_initial"] + ): self.initial_cycle_startup_inventory = self.cycle_required_startup_energy else: - self.initial_cycle_startup_inventory = max(0.0, self.cycle_required_startup_energy - - csp.plant_state['pc_startup_energy_remain_initial'] / 1e3) - if self.initial_cycle_startup_inventory > (1.0 - 1.e-6) * self.cycle_required_startup_energy: - self.initial_cycle_startup_inventory = self.cycle_required_startup_energy - - self.is_cycle_generating_initial = (csp.plant_state['pc_op_mode_initial'] == 1) - self.is_cycle_starting_initial = (csp.plant_state['pc_op_mode_initial'] == 0 - or csp.plant_state['pc_op_mode_initial'] == 4) + self.initial_cycle_startup_inventory = max( + 0.0, + self.cycle_required_startup_energy + - csp.plant_state["pc_startup_energy_remain_initial"] / 1e3, + ) + if ( + self.initial_cycle_startup_inventory + > (1.0 - 1.0e-6) * self.cycle_required_startup_energy + ): + self.initial_cycle_startup_inventory = ( + self.cycle_required_startup_energy + ) + + self.is_cycle_generating_initial = csp.plant_state["pc_op_mode_initial"] == 1 + self.is_cycle_starting_initial = ( + csp.plant_state["pc_op_mode_initial"] == 0 + or csp.plant_state["pc_op_mode_initial"] == 4 + ) # self.ycsb0 = (plant.state['pc_op_mode_initial'] == 2) if self.is_cycle_generating_initial: - self.initial_cycle_thermal_power = csp.plant_state['heat_into_cycle'] + self.initial_cycle_thermal_power = csp.plant_state["heat_into_cycle"] else: self.initial_cycle_thermal_power = 0.0 @staticmethod def get_start_end_datetime(start_time: int, n_horizon: int): + """Get start and end datetimes based on simulation start time and horizon length. + + Args: + start_time (int): Start time of the simulation in hours. + n_horizon (int): Length of the simulation horizon in hours. + + Returns: + tuple: A tuple containing the start and end datetime objects. + + Notes: + This method calculates the start and end datetimes based on the provided start time + and horizon length, assuming hourly data. + """ # Setting simulation times start_datetime = CspDispatch.get_start_datetime_by_hour(start_time) # Handling end of simulation horizon -> assumes hourly data @@ -856,9 +1345,17 @@ def get_start_end_datetime(start_time: int, n_horizon: int): @staticmethod def get_start_datetime_by_hour(start_time: int): """ - Get datetime for start_time hour of the year - : param start_time: hour of year - : return: datetime object + Get the datetime object corresponding to the start time of year in hours. + + Args: + start_time (int): Start time of the simulation in hours. + + Returns: + datetime.datetime: Datetime object corresponding to the start time. + + Notes: + This method calculates the datetime object corresponding to the start time + in hours relative to the beginning of the year. """ # TODO: bring in the correct year from site data - or replace outside of function? beginning_of_year = datetime.datetime(2009, 1, 1, 0) @@ -866,12 +1363,24 @@ def get_start_datetime_by_hour(start_time: int): @staticmethod def seconds_since_newyear(dt): + """ + Get the number of seconds elapsed since the beginning of the year. + + Args: + dt (datetime.datetime): Datetime object. + + Returns: + int: Number of seconds elapsed since the beginning of the year. + + Notes: + This method calculates the number of seconds elapsed since the beginning of the year, + using a non-leap year (2009) for consistency with a multiple of 8760 hours assumption. + """ # Substitute a non-leap year (2009) to keep multiple of 8760 assumption: newyear = datetime.datetime(2009, 1, 1, 0, 0, 0, 0) time_diff = dt - newyear return int(time_diff.total_seconds()) - ################################# # INPUTS # ################################# @@ -883,40 +1392,58 @@ def time_duration(self) -> list: @time_duration.setter def time_duration(self, time_duration: list): - """Dispatch horizon time steps [hour]""" if len(time_duration) == len(self.blocks): for t, delta in zip(self.blocks, time_duration): self.blocks[t].time_duration = round(delta, self.round_digits) else: - raise ValueError(self.time_duration.__name__ + " list must be the same length as time horizon") + raise ValueError( + self.time_duration.__name__ + + " list must be the same length as time horizon" + ) @property def available_thermal_generation(self) -> list: """Available solar thermal generation from the csp field [MWt]""" - return [self.blocks[t].available_thermal_generation.value for t in self.blocks.index_set()] + return [ + self.blocks[t].available_thermal_generation.value + for t in self.blocks.index_set() + ] @available_thermal_generation.setter def available_thermal_generation(self, available_thermal_generation: list): - """Available solar thermal generation from the csp field [MWt]""" if len(available_thermal_generation) == len(self.blocks): for t, value in zip(self.blocks, available_thermal_generation): - self.blocks[t].available_thermal_generation = round(value, self.round_digits) + self.blocks[t].available_thermal_generation = round( + value, self.round_digits + ) else: - raise ValueError(self.available_thermal_generation.__name__ + " list must be the same length as time horizon") + raise ValueError( + self.available_thermal_generation.__name__ + + " list must be the same length as time horizon" + ) @property def cycle_ambient_efficiency_correction(self) -> list: """Cycle efficiency ambient temperature adjustment factor [-]""" - return [self.blocks[t].cycle_ambient_efficiency_correction.value for t in self.blocks.index_set()] + return [ + self.blocks[t].cycle_ambient_efficiency_correction.value + for t in self.blocks.index_set() + ] @cycle_ambient_efficiency_correction.setter - def cycle_ambient_efficiency_correction(self, cycle_ambient_efficiency_correction: list): - """Cycle efficiency ambient temperature adjustment factor [-]""" + def cycle_ambient_efficiency_correction( + self, cycle_ambient_efficiency_correction: list + ): if len(cycle_ambient_efficiency_correction) == len(self.blocks): for t, value in zip(self.blocks, cycle_ambient_efficiency_correction): - self.blocks[t].cycle_ambient_efficiency_correction = round(value, self.round_digits) + self.blocks[t].cycle_ambient_efficiency_correction = round( + value, self.round_digits + ) else: - raise ValueError(self.cycle_ambient_efficiency_correction.__name__ + " list must be the same length as time horizon") + raise ValueError( + self.cycle_ambient_efficiency_correction.__name__ + + " list must be the same length as time horizon" + ) @property def condenser_losses(self) -> list: @@ -925,26 +1452,35 @@ def condenser_losses(self) -> list: @condenser_losses.setter def condenser_losses(self, condenser_losses: list): - """Normalized condenser parasitic losses [-]""" if len(condenser_losses) == len(self.blocks): for t, value in zip(self.blocks, condenser_losses): self.blocks[t].condenser_losses = round(value, self.round_digits) else: - raise ValueError(self.condenser_losses.__name__ + " list must be the same length as time horizon") + raise ValueError( + self.condenser_losses.__name__ + + " list must be the same length as time horizon" + ) @property def receiver_startup_fraction(self) -> list: """Estimated fraction of time period required for receiver start-up [-]""" - return [self.blocks[t].receiver_startup_fraction.value for t in self.blocks.index_set()] + return [ + self.blocks[t].receiver_startup_fraction.value + for t in self.blocks.index_set() + ] @receiver_startup_fraction.setter def receiver_startup_fraction(self, receiver_startup_fraction: list): - """Estimated fraction of time period required for receiver start-up [-]""" if len(receiver_startup_fraction) == len(self.blocks): for t, value in zip(self.blocks, receiver_startup_fraction): - self.blocks[t].receiver_startup_fraction = round(value, self.round_digits) + self.blocks[t].receiver_startup_fraction = round( + value, self.round_digits + ) else: - raise ValueError(self.receiver_startup_fraction.__name__ + " list must be the same length as time horizon") + raise ValueError( + self.receiver_startup_fraction.__name__ + + " list must be the same length as time horizon" + ) @property def min_receiver_start_time(self) -> float: @@ -954,9 +1490,10 @@ def min_receiver_start_time(self) -> float: @min_receiver_start_time.setter def min_receiver_start_time(self, min_receiver_start_time_hr: float): - """Minimum time to start the receiver [hr]""" for t in self.blocks.index_set(): - self.blocks[t].min_receiver_start_time.set_value(round(min_receiver_start_time_hr, self.round_digits)) + self.blocks[t].min_receiver_start_time.set_value( + round(min_receiver_start_time_hr, self.round_digits) + ) @property def cost_per_field_generation(self) -> float: @@ -966,9 +1503,10 @@ def cost_per_field_generation(self) -> float: @cost_per_field_generation.setter def cost_per_field_generation(self, om_dollar_per_mwh_thermal: float): - """Generation cost for the csp field [$/MWht]""" for t in self.blocks.index_set(): - self.blocks[t].cost_per_field_generation.set_value(round(om_dollar_per_mwh_thermal, self.round_digits)) + self.blocks[t].cost_per_field_generation.set_value( + round(om_dollar_per_mwh_thermal, self.round_digits) + ) @property def cost_per_field_start(self) -> float: @@ -978,9 +1516,10 @@ def cost_per_field_start(self) -> float: @cost_per_field_start.setter def cost_per_field_start(self, dollars_per_start: float): - """Penalty for field start-up [$/start]""" for t in self.blocks.index_set(): - self.blocks[t].cost_per_field_start.set_value(round(dollars_per_start, self.round_digits)) + self.blocks[t].cost_per_field_start.set_value( + round(dollars_per_start, self.round_digits) + ) @property def cost_per_cycle_generation(self) -> float: @@ -990,9 +1529,10 @@ def cost_per_cycle_generation(self) -> float: @cost_per_cycle_generation.setter def cost_per_cycle_generation(self, om_dollar_per_mwh_electric: float): - """Generation cost for power cycle [$/MWhe]""" for t in self.blocks.index_set(): - self.blocks[t].cost_per_cycle_generation.set_value(round(om_dollar_per_mwh_electric, self.round_digits)) + self.blocks[t].cost_per_cycle_generation.set_value( + round(om_dollar_per_mwh_electric, self.round_digits) + ) @property def cost_per_cycle_start(self) -> float: @@ -1002,9 +1542,10 @@ def cost_per_cycle_start(self) -> float: @cost_per_cycle_start.setter def cost_per_cycle_start(self, dollars_per_start: float): - """Penalty for power cycle start [$/start]""" for t in self.blocks.index_set(): - self.blocks[t].cost_per_cycle_start.set_value(round(dollars_per_start, self.round_digits)) + self.blocks[t].cost_per_cycle_start.set_value( + round(dollars_per_start, self.round_digits) + ) @property def cost_per_change_thermal_input(self) -> float: @@ -1014,9 +1555,10 @@ def cost_per_change_thermal_input(self) -> float: @cost_per_change_thermal_input.setter def cost_per_change_thermal_input(self, dollars_per_thermal_power: float): - """Penalty for change in power cycle thermal input [$/MWt]""" for t in self.blocks.index_set(): - self.blocks[t].cost_per_change_thermal_input.set_value(round(dollars_per_thermal_power, self.round_digits)) + self.blocks[t].cost_per_change_thermal_input.set_value( + round(dollars_per_thermal_power, self.round_digits) + ) @property def field_startup_losses(self) -> float: @@ -1026,9 +1568,10 @@ def field_startup_losses(self) -> float: @field_startup_losses.setter def field_startup_losses(self, field_startup_losses: float): - """Solar field startup or shutdown parasitic loss [MWhe]""" for t in self.blocks.index_set(): - self.blocks[t].field_startup_losses.set_value(round(field_startup_losses, self.round_digits)) + self.blocks[t].field_startup_losses.set_value( + round(field_startup_losses, self.round_digits) + ) @property def receiver_required_startup_energy(self) -> float: @@ -1038,9 +1581,10 @@ def receiver_required_startup_energy(self) -> float: @receiver_required_startup_energy.setter def receiver_required_startup_energy(self, energy: float): - """Required energy expended to start receiver [MWht]""" for t in self.blocks.index_set(): - self.blocks[t].receiver_required_startup_energy.set_value(round(energy, self.round_digits)) + self.blocks[t].receiver_required_startup_energy.set_value( + round(energy, self.round_digits) + ) @property def storage_capacity(self) -> float: @@ -1050,7 +1594,6 @@ def storage_capacity(self) -> float: @storage_capacity.setter def storage_capacity(self, energy: float): - """Thermal energy storage capacity [MWht]""" for t in self.blocks.index_set(): self.blocks[t].storage_capacity.set_value(round(energy, self.round_digits)) @@ -1062,9 +1605,10 @@ def receiver_pumping_losses(self) -> float: @receiver_pumping_losses.setter def receiver_pumping_losses(self, electric_per_thermal: float): - """Solar field and/or receiver pumping power per unit power produced [MWe/MWt]""" for t in self.blocks.index_set(): - self.blocks[t].receiver_pumping_losses.set_value(round(electric_per_thermal, self.round_digits)) + self.blocks[t].receiver_pumping_losses.set_value( + round(electric_per_thermal, self.round_digits) + ) @property def minimum_receiver_power(self) -> float: @@ -1074,9 +1618,10 @@ def minimum_receiver_power(self) -> float: @minimum_receiver_power.setter def minimum_receiver_power(self, thermal_power: float): - """Minimum operational thermal power delivered by receiver [MWt]""" for t in self.blocks.index_set(): - self.blocks[t].minimum_receiver_power.set_value(round(thermal_power, self.round_digits)) + self.blocks[t].minimum_receiver_power.set_value( + round(thermal_power, self.round_digits) + ) @property def allowable_receiver_startup_power(self) -> float: @@ -1086,9 +1631,10 @@ def allowable_receiver_startup_power(self) -> float: @allowable_receiver_startup_power.setter def allowable_receiver_startup_power(self, thermal_power: float): - """Allowable power per period for receiver start-up [MWt]""" for t in self.blocks.index_set(): - self.blocks[t].allowable_receiver_startup_power.set_value(round(thermal_power, self.round_digits)) + self.blocks[t].allowable_receiver_startup_power.set_value( + round(thermal_power, self.round_digits) + ) @property def field_track_losses(self) -> float: @@ -1098,9 +1644,10 @@ def field_track_losses(self) -> float: @field_track_losses.setter def field_track_losses(self, electric_power: float): - """Solar field tracking parasitic loss [MWe]""" for t in self.blocks.index_set(): - self.blocks[t].field_track_losses.set_value(round(electric_power, self.round_digits)) + self.blocks[t].field_track_losses.set_value( + round(electric_power, self.round_digits) + ) # @property # def heat_trace_losses(self) -> float: @@ -1122,9 +1669,10 @@ def cycle_required_startup_energy(self) -> float: @cycle_required_startup_energy.setter def cycle_required_startup_energy(self, thermal_energy: float): - """Required energy expended to start cycle [MWht]""" for t in self.blocks.index_set(): - self.blocks[t].cycle_required_startup_energy.set_value(round(thermal_energy, self.round_digits)) + self.blocks[t].cycle_required_startup_energy.set_value( + round(thermal_energy, self.round_digits) + ) @property def cycle_nominal_efficiency(self) -> float: @@ -1134,10 +1682,11 @@ def cycle_nominal_efficiency(self) -> float: @cycle_nominal_efficiency.setter def cycle_nominal_efficiency(self, efficiency: float): - """Power cycle nominal efficiency [-]""" efficiency = self._check_efficiency_value(efficiency) for t in self.blocks.index_set(): - self.blocks[t].cycle_nominal_efficiency.set_value(round(efficiency, self.round_digits)) + self.blocks[t].cycle_nominal_efficiency.set_value( + round(efficiency, self.round_digits) + ) @property def cycle_performance_slope(self) -> float: @@ -1147,9 +1696,10 @@ def cycle_performance_slope(self) -> float: @cycle_performance_slope.setter def cycle_performance_slope(self, slope: float): - """Slope of linear approximation of power cycle performance curve [MWe/MWt]""" for t in self.blocks.index_set(): - self.blocks[t].cycle_performance_slope.set_value(round(slope, self.round_digits)) + self.blocks[t].cycle_performance_slope.set_value( + round(slope, self.round_digits) + ) @property def cycle_pumping_losses(self) -> float: @@ -1159,9 +1709,10 @@ def cycle_pumping_losses(self) -> float: @cycle_pumping_losses.setter def cycle_pumping_losses(self, electric_per_thermal: float): - """Cycle heat transfer fluid pumping power per unit energy expended [MWe/MWt]""" for t in self.blocks.index_set(): - self.blocks[t].cycle_pumping_losses.set_value(round(electric_per_thermal, self.round_digits)) + self.blocks[t].cycle_pumping_losses.set_value( + round(electric_per_thermal, self.round_digits) + ) @property def allowable_cycle_startup_power(self) -> float: @@ -1171,9 +1722,10 @@ def allowable_cycle_startup_power(self) -> float: @allowable_cycle_startup_power.setter def allowable_cycle_startup_power(self, thermal_power: float): - """Allowable power per period for cycle start-up [MWt]""" for t in self.blocks.index_set(): - self.blocks[t].allowable_cycle_startup_power.set_value(round(thermal_power, self.round_digits)) + self.blocks[t].allowable_cycle_startup_power.set_value( + round(thermal_power, self.round_digits) + ) @property def minimum_cycle_thermal_power(self) -> float: @@ -1183,9 +1735,10 @@ def minimum_cycle_thermal_power(self) -> float: @minimum_cycle_thermal_power.setter def minimum_cycle_thermal_power(self, thermal_power: float): - """Minimum operational thermal power delivered to the power cycle [MWt]""" for t in self.blocks.index_set(): - self.blocks[t].minimum_cycle_thermal_power.set_value(round(thermal_power, self.round_digits)) + self.blocks[t].minimum_cycle_thermal_power.set_value( + round(thermal_power, self.round_digits) + ) @property def maximum_cycle_thermal_power(self) -> float: @@ -1195,9 +1748,10 @@ def maximum_cycle_thermal_power(self) -> float: @maximum_cycle_thermal_power.setter def maximum_cycle_thermal_power(self, thermal_power: float): - """Maximum operational thermal power delivered to the power cycle [MWt]""" for t in self.blocks.index_set(): - self.blocks[t].maximum_cycle_thermal_power.set_value(round(thermal_power, self.round_digits)) + self.blocks[t].maximum_cycle_thermal_power.set_value( + round(thermal_power, self.round_digits) + ) # @property # def minimum_cycle_power(self) -> float: @@ -1219,9 +1773,10 @@ def maximum_cycle_power(self) -> float: @maximum_cycle_power.setter def maximum_cycle_power(self, electric_power: float): - """Maximum cycle electric power output [MWe]""" for t in self.blocks.index_set(): - self.blocks[t].maximum_cycle_power.set_value(round(electric_power, self.round_digits)) + self.blocks[t].maximum_cycle_power.set_value( + round(electric_power, self.round_digits) + ) # INITIAL CONDITIONS @property @@ -1231,8 +1786,9 @@ def initial_thermal_energy_storage(self) -> float: @initial_thermal_energy_storage.setter def initial_thermal_energy_storage(self, initial_energy: float): - """Initial thermal energy storage reserve quantity at beginning of the horizon [MWht]""" - self.model.initial_thermal_energy_storage = round(initial_energy, self.round_digits) + self.model.initial_thermal_energy_storage = round( + initial_energy, self.round_digits + ) @property def initial_receiver_startup_inventory(self) -> float: @@ -1241,19 +1797,18 @@ def initial_receiver_startup_inventory(self) -> float: @initial_receiver_startup_inventory.setter def initial_receiver_startup_inventory(self, initial_energy: float): - """Initial receiver start-up energy inventory at beginning of the horizon [MWht]""" - self.model.initial_receiver_startup_inventory = round(initial_energy, self.round_digits) + self.model.initial_receiver_startup_inventory = round( + initial_energy, self.round_digits + ) @property def is_field_generating_initial(self) -> bool: """True (1) if solar field is generating 'usable' thermal power at beginning of the horizon; - False (0) Otherwise [-]""" + False (0) Otherwise [-]""" return bool(self.model.is_field_generating_initial.value) @is_field_generating_initial.setter def is_field_generating_initial(self, is_field_generating: Union[bool, int]): - """True (1) if solar field is generating 'usable' thermal power at beginning of the horizon; - False (0) Otherwise [-]""" self.model.is_field_generating_initial = int(is_field_generating) @property @@ -1263,7 +1818,6 @@ def is_field_starting_initial(self) -> bool: @is_field_starting_initial.setter def is_field_starting_initial(self, is_field_starting: Union[bool, int]): - """True (1) if solar field is starting up at beginning of the horizon; False (0) Otherwise [-]""" self.model.is_field_starting_initial = int(is_field_starting) @property @@ -1273,8 +1827,9 @@ def initial_cycle_startup_inventory(self) -> float: @initial_cycle_startup_inventory.setter def initial_cycle_startup_inventory(self, initial_energy: float): - """Initial cycle start-up energy inventory at beginning of the horizon [MWht]""" - self.model.initial_cycle_startup_inventory = round(initial_energy, self.round_digits) + self.model.initial_cycle_startup_inventory = round( + initial_energy, self.round_digits + ) @property def initial_cycle_thermal_power(self) -> float: @@ -1283,7 +1838,6 @@ def initial_cycle_thermal_power(self) -> float: @initial_cycle_thermal_power.setter def initial_cycle_thermal_power(self, initial_power: float): - """Initial cycle thermal power at beginning of the horizon [MWt]""" self.model.initial_cycle_thermal_power = round(initial_power, self.round_digits) @property @@ -1293,7 +1847,6 @@ def is_cycle_generating_initial(self) -> bool: @is_cycle_generating_initial.setter def is_cycle_generating_initial(self, is_cycle_generating: Union[bool, int]): - """True (1) if cycle is generating electric power at beginning of the horizon; False (0) Otherwise [-]""" self.model.is_cycle_generating_initial = int(is_cycle_generating) @property @@ -1303,82 +1856,125 @@ def is_cycle_starting_initial(self) -> bool: @is_cycle_starting_initial.setter def is_cycle_starting_initial(self, is_cycle_starting: Union[bool, int]): - """True (1) if cycle is starting up at beginning of the horizon; False (0) Otherwise [-]""" self.model.is_cycle_starting_initial = int(is_cycle_starting) # OUTPUTS @property def thermal_energy_storage(self) -> list: """Thermal energy storage reserve quantity [MWht]""" - return [round(self.blocks[t].thermal_energy_storage.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].thermal_energy_storage.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def receiver_startup_inventory(self) -> list: """Receiver start-up energy inventory [MWht]""" - return [round(self.blocks[t].receiver_startup_inventory.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].receiver_startup_inventory.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def receiver_thermal_power(self) -> list: """Thermal power delivered by the receiver [MWt]""" - return [round(self.blocks[t].receiver_thermal_power.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].receiver_thermal_power.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def receiver_startup_consumption(self) -> list: """Receiver start-up power consumption [MWt]""" - return [round(self.blocks[t].receiver_startup_consumption.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].receiver_startup_consumption.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def is_field_generating(self) -> list: """1 if solar field is generating 'usable' thermal power; 0 Otherwise [-]""" - return [round(self.blocks[t].is_field_generating.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].is_field_generating.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def is_field_starting(self) -> list: """1 if solar field is starting up; 0 Otherwise [-]""" - return [round(self.blocks[t].is_field_starting.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].is_field_starting.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def incur_field_start(self) -> list: """1 if solar field start-up penalty is incurred; 0 Otherwise [-]""" - return [round(self.blocks[t].incur_field_start.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].incur_field_start.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def cycle_startup_inventory(self) -> list: """Cycle start-up energy inventory [MWht]""" - return [round(self.blocks[t].cycle_startup_inventory.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].cycle_startup_inventory.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def system_load(self) -> list: """Net generation of csp system [MWe]""" - return [round(self.blocks[t].system_load.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].system_load.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def cycle_generation(self) -> list: """Power cycle electricity generation [MWe]""" - return [round(self.blocks[t].cycle_generation.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].cycle_generation.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def cycle_thermal_ramp(self) -> list: """Power cycle positive change in thermal energy input [MWt]""" - return [round(self.blocks[t].cycle_thermal_ramp.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].cycle_thermal_ramp.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def cycle_thermal_power(self) -> list: """Cycle thermal power utilization [MWt]""" - return [round(self.blocks[t].cycle_thermal_power.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].cycle_thermal_power.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def is_cycle_generating(self) -> list: """1 if cycle is generating electric power; 0 Otherwise [-]""" - return [round(self.blocks[t].is_cycle_generating.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].is_cycle_generating.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def is_cycle_starting(self) -> list: """1 if cycle is starting up; 0 Otherwise [-]""" - return [round(self.blocks[t].is_cycle_starting.value, self.round_digits) for t in self.blocks.index_set()] + return [ + round(self.blocks[t].is_cycle_starting.value, self.round_digits) + for t in self.blocks.index_set() + ] @property def incur_cycle_start(self) -> list: """1 if cycle start-up penalty is incurred; 0 Otherwise [-]""" - return [round(self.blocks[t].incur_cycle_start.value, self.round_digits) for t in self.blocks.index_set()] - + return [ + round(self.blocks[t].incur_cycle_start.value, self.round_digits) + for t in self.blocks.index_set() + ] diff --git a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py index aa248e05c..535ab781e 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py @@ -7,22 +7,65 @@ class PowerSourceDispatch(Dispatch): """ - + Dispatch optimization model for power sources. + + Attributes: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Index set. + system_model: System model. + financial_model: Financial model. + block_set_name (str): Name of the block set. + + Methods: + dispatch_block_rule(gen): Dispatch block rule method. + initialize_parameters(): Initialize parameters method. + update_time_series_parameters(start_time): Update time series parameters method. + _create_variables(hyrbid): Create variables method (abstract). + _create_port(hyrbid): Create port method (abstract). + + Properties: + cost_per_generation: Cost per generation property. + available_generation: Available generation property. + generation: Generation property. """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model, - financial_model, - block_set_name: str = 'generator'): - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name) + + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model, + financial_model, + block_set_name: str = "generator", + ): + """ + Initialize PowerSourceDispatch. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Index set. + system_model: System model. + financial_model: Financial model. + block_set_name (str): Name of the block set. + """ + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + ) @staticmethod def dispatch_block_rule(gen): + """ + Dispatch block rule method. + + Args: + gen: Generator. + + Returns: + None + """ ################################## # Parameters # ################################## @@ -31,19 +74,22 @@ def dispatch_block_rule(gen): default=1.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.hr) + units=u.hr, + ) gen.cost_per_generation = pyomo.Param( doc="Generation cost for generator [$/MWh]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.MWh) + units=u.USD / u.MWh, + ) gen.available_generation = pyomo.Param( doc="Available generation for the generator [MW]", default=0.0, within=pyomo.Reals, mutable=True, - units=u.MW) + units=u.MW, + ) ################################## # Variables # ################################## @@ -51,7 +97,8 @@ def dispatch_block_rule(gen): doc="Power generation of generator [MW]", domain=pyomo.NonNegativeReals, bounds=(0, gen.available_generation), - units=u.MW) + units=u.MW, + ) ################################## # Constraints # ################################## @@ -62,54 +109,116 @@ def dispatch_block_rule(gen): gen.port.add(gen.generation) def initialize_parameters(self): - self.cost_per_generation = self._financial_model.value("om_capacity")[0]*1e3/8760 + """Initialize parameters method.""" + self.cost_per_generation = ( + self._financial_model.value("om_capacity")[0] * 1e3 / 8760 + ) def update_time_series_parameters(self, start_time: int): + """ + Update time series parameters method. + + Args: + start_time (int): Start time. + + Returns: + None + """ n_horizon = len(self.blocks.index_set()) generation = self._system_model.value("gen") if start_time + n_horizon > len(generation): horizon_gen = list(generation[start_time:]) - horizon_gen.extend(list(generation[0:n_horizon - len(horizon_gen)])) + horizon_gen.extend(list(generation[0 : n_horizon - len(horizon_gen)])) else: - horizon_gen = generation[start_time:start_time + n_horizon] + horizon_gen = generation[start_time : start_time + n_horizon] if len(horizon_gen) < len(self.blocks): - raise RuntimeError(f"Dispatch parameter update error at start_time {start_time}: System model " - f"{type(self._system_model)} generation profile should have at least {len(self.blocks)} " - f"length but has only {len(generation)}") + raise RuntimeError( + f"Dispatch parameter update error at start_time {start_time}: System model " + f"{type(self._system_model)} generation profile should have at least {len(self.blocks)} " + f"length but has only {len(generation)}" + ) self.available_generation = [gen_kw / 1e3 for gen_kw in horizon_gen] - def _create_variables(self, hyrbid): - raise NotImplemented("This function must be overridden for specific dispatch model") + def _create_variables(self, hybrid): + """ + Create variables method (abstract). + + Args: + hybrid: hybrid plant instance to which individual technology is added. + + Returns: + None - def _create_port(self, hyrbid): - raise NotImplemented("This function must be overridden for specific dispatch model") + Raises: + NotImplemented: Must be overridden in specific technology models. + """ + raise NotImplemented( + "This function must be overridden for specific dispatch model" + ) + + def _create_port(self, hybrid): + """ + Create port method (abstract). + + Args: + hybrid: Hybrid. + + Returns: + None + + Raises: + NotImplemented: Must be overridden in specific technology models. + """ + raise NotImplemented( + "This function must be overridden for specific dispatch model" + ) @property def cost_per_generation(self) -> float: + """Cost per generation [$/MWh]""" for t in self.blocks.index_set(): return self.blocks[t].cost_per_generation.value @cost_per_generation.setter def cost_per_generation(self, om_dollar_per_mwh: float): for t in self.blocks.index_set(): - self.blocks[t].cost_per_generation.set_value(round(om_dollar_per_mwh, self.round_digits)) + self.blocks[t].cost_per_generation.set_value( + round(om_dollar_per_mwh, self.round_digits) + ) @property def available_generation(self) -> list: - return [self.blocks[t].available_generation.value for t in self.blocks.index_set()] + """ + Available generation. + + Returns: + list: List of available generation. + """ + return [ + self.blocks[t].available_generation.value for t in self.blocks.index_set() + ] @available_generation.setter def available_generation(self, resource: list): if len(resource) == len(self.blocks): for t, gen in zip(self.blocks, resource): - self.blocks[t].available_generation.set_value(round(gen, self.round_digits)) + self.blocks[t].available_generation.set_value( + round(gen, self.round_digits) + ) else: - raise ValueError(f"'resource' list ({len(resource)}) must be the same length as time horizon ({len(self.blocks)})") + raise ValueError( + f"'resource' list ({len(resource)}) must be the same length as time horizon ({len(self.blocks)})" + ) @property def generation(self) -> list: - return [round(self.blocks[t].generation.value, self.round_digits) for t in self.blocks.index_set()] - - - + """Generation. + + Returns: + list: List of generation. + """ + return [ + round(self.blocks[t].generation.value, self.round_digits) + for t in self.blocks.index_set() + ] diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 49ffa9f00..9a2831eec 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -14,16 +14,40 @@ class PvDispatch(PowerSourceDispatch): _system_model: Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8] _financial_model: FinancialModelType """ + Dispatch optimization model for photovoltaic (PV) systems. + Attributes: + pv_obj: PV object. + _system_model: System model. + _financial_model: Financial model. + + Methods: + update_time_series_parameters(start_time): Update time series parameters method. + max_gross_profit_objective(blocks): Maximum gross profit objective method. + min_operating_cost_objective(blocks): Minimum operating cost objective method. + _create_variables(hybrid): Create variables method. + _create_port(hybrid): Create port method. """ + def __init__( self, pyomo_model: ConcreteModel, indexed_set: Set, system_model: Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8], financial_model: FinancialModelType, - block_set_name: str = 'pv', + block_set_name: str = "pv", ): + """ + Initialize PvDispatch. + + Args: + pyomo_model (ConcreteModel): Pyomo concrete model. + indexed_set (Set): Indexed set. + system_model (Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8]): System model. + financial_model (FinancialModelType): Financial model. + block_set_name (str): Name of the block set. + """ + super().__init__( pyomo_model, indexed_set, @@ -33,25 +57,43 @@ def __init__( ) def update_time_series_parameters(self, start_time: int): + """ + Update time series parameters method. + + Args: + start_time (int): Start time. + """ super().update_time_series_parameters(start_time) # zero out any negative load self.available_generation = [max(0, i) for i in self.available_generation] def max_gross_profit_objective(self, blocks): + """PV instance of maximum gross profit objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = Expression( - expr=sum( - - (1/blocks[t].time_weighting_factor) - * self.blocks[t].time_duration - * self.blocks[t].cost_per_generation - * blocks[t].pv_generation - for t in blocks.index_set() - ) + expr=sum( + -(1 / blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].pv_generation + for t in blocks.index_set() ) + ) def min_operating_cost_objective(self, blocks): + """PV instance of minimum operating cost objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = sum( - blocks[t].time_weighting_factor + blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation * blocks[t].pv_generation @@ -59,6 +101,18 @@ def min_operating_cost_objective(self, blocks): ) def _create_variables(self, hybrid): + """ + Create PV variables to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + tuple: Tuple containing created variables. + - generation: Generation from given technology. + - load: Load from given technology. + + """ hybrid.pv_generation = Var( doc="Power generation of photovoltaics [MW]", domain=NonNegativeReals, @@ -68,5 +122,14 @@ def _create_variables(self, hybrid): return hybrid.pv_generation, 0 def _create_port(self, hybrid): - hybrid.pv_port = Port(initialize={'generation': hybrid.pv_generation}) + """ + Create pv port to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + Port: PV Port object. + """ + hybrid.pv_port = Port(initialize={"generation": hybrid.pv_generation}) return hybrid.pv_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index 19b3885f8..1a2379081 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -11,54 +11,86 @@ class TowerDispatch(CspDispatch): _system_model: None _financial_model: FinancialModelType """ + Dispatch optimization model for CSP tower systems. + Attributes: + tower_obj: Tower object. + _system_model: System model. + _financial_model: Financial model. + + Methods: + update_initial_conditions(): Update initial conditions method. + max_gross_profit_objective(blocks): Maximum gross profit objective method. + min_operating_cost_objective(blocks): Minimum operating cost objective method. + _create_variables(hybrid): Create variables method. + _create_port(hybrid): Create port method. """ + def __init__( self, pyomo_model: ConcreteModel, indexed_set: Set, system_model: None, financial_model: FinancialModelType, - block_set_name: str = 'tower', + block_set_name: str = "tower", ): + """ + Initialize TowerDispatch. + + Args: + pyomo_model (ConcreteModel): Pyomo concrete model. + indexed_set (Set): Indexed set. + system_model (None): System model. + financial_model (FinancialModelType): Financial model. + block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, indexed_set, system_model, financial_model, - block_set_name=block_set_name + block_set_name=block_set_name, ) def update_initial_conditions(self): + """Update initial conditions.""" super().update_initial_conditions() csp = self._system_model # Note, SS receiver model in ssc assumes full available power is used for startup # (even if, time requirement is binding) rec_accumulate_time = max( 0.0, - csp.value('rec_su_delay') - csp.plant_state['rec_startup_time_remain_init'] + csp.value("rec_su_delay") - csp.plant_state["rec_startup_time_remain_init"], ) rec_accumulate_energy = max( 0.0, ( self.receiver_required_startup_energy - - csp.plant_state['rec_startup_energy_remain_init'] / 1e6 - ) + - csp.plant_state["rec_startup_energy_remain_init"] / 1e6 + ), ) self.initial_receiver_startup_inventory = min( rec_accumulate_energy, - rec_accumulate_time * self.allowable_receiver_startup_power + rec_accumulate_time * self.allowable_receiver_startup_power, ) if ( - self.initial_receiver_startup_inventory > (1.0 - 1.e-6) - * self.receiver_required_startup_energy + self.initial_receiver_startup_inventory + > (1.0 - 1.0e-6) * self.receiver_required_startup_energy ): - self.initial_receiver_startup_inventory = self.receiver_required_startup_energy + self.initial_receiver_startup_inventory = ( + self.receiver_required_startup_energy + ) def max_gross_profit_objective(self, blocks): + """Tower CSP instance of maximum gross profit objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( - - (1/blocks[t].time_weighting_factor) + -(1 / blocks[t].time_weighting_factor) * ( ( self.blocks[t].cost_per_field_generation @@ -88,23 +120,27 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): + """Tower CSP instance of minimum operating cost objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = sum( blocks[t].time_weighting_factor * ( - self.blocks[t].cost_per_field_start - * self.blocks[t].incur_field_start + self.blocks[t].cost_per_field_start * self.blocks[t].incur_field_start - ( self.blocks[t].cost_per_field_generation * self.blocks[t].receiver_thermal_power * self.blocks[t].time_duration - ) # Trying to incentivize TES generation + ) # Trying to incentivize TES generation + ( self.blocks[t].cost_per_cycle_generation * self.blocks[t].cycle_generation * self.blocks[t].time_duration ) - + self.blocks[t].cost_per_cycle_start - * self.blocks[t].incur_cycle_start + + self.blocks[t].cost_per_cycle_start * self.blocks[t].incur_cycle_start + self.blocks[t].cost_per_change_thermal_input * self.blocks[t].cycle_thermal_ramp ) @@ -112,6 +148,18 @@ def min_operating_cost_objective(self, blocks): ) def _create_variables(self, hybrid): + """ + Create Tower CSP variables to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + tuple: Tuple containing created variables. + - generation: Generation from given technology. + - load: Load from given technology. + + """ hybrid.tower_generation = Var( doc="Power generation of CSP tower [MW]", domain=NonNegativeReals, @@ -127,10 +175,19 @@ def _create_variables(self, hybrid): return hybrid.tower_generation, hybrid.tower_load def _create_port(self, hybrid): + """ + Create CSP tower port to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + Port: CSP Tower Port object. + """ hybrid.tower_port = Port( initialize={ - 'cycle_generation': hybrid.tower_generation, - 'system_load': hybrid.tower_load, + "cycle_generation": hybrid.tower_generation, + "system_load": hybrid.tower_load, } ) return hybrid.tower_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index 5ac9224d6..4184eaf76 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -11,25 +11,49 @@ class TroughDispatch(CspDispatch): _system_model: None _financial_model: FinancialModelType """ + Dispatch optimization model for CSP trough systems. + Attributes: + trough_obj: Trough object. + _system_model: System model. + _financial_model: Financial model. + + Methods: + update_initial_conditions(): Update initial conditions method. + max_gross_profit_objective(blocks): Maximum gross profit objective method. + min_operating_cost_objective(blocks): Minimum operating cost objective method. + _create_variables(hybrid): Create variables method. + _create_port(hybrid): Create port method. """ + def __init__( - self, - pyomo_model: ConcreteModel, - indexed_set: Set, - system_model: None, - financial_model: FinancialModelType, - block_set_name: str = 'trough', - ): + self, + pyomo_model: ConcreteModel, + indexed_set: Set, + system_model: None, + financial_model: FinancialModelType, + block_set_name: str = "trough", + ): + """ + Initialize TroughDispatch. + + Args: + pyomo_model (ConcreteModel): Pyomo concrete model. + indexed_set (Set): Indexed set. + system_model (None): System model. + financial_model (FinancialModelType): Financial model. + block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, indexed_set, system_model, financial_model, - block_set_name=block_set_name + block_set_name=block_set_name, ) def update_initial_conditions(self): + """Update initial conditions method.""" super().update_initial_conditions() self.initial_receiver_startup_inventory = 0.0 # FIXME: if self.is_field_starting_initial: @@ -40,9 +64,14 @@ def update_initial_conditions(self): ) def max_gross_profit_objective(self, blocks): + """Trough CSP instance of maximum gross profit objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( - - (1/blocks[t].time_weighting_factor) + -(1 / blocks[t].time_weighting_factor) * ( ( self.blocks[t].cost_per_field_generation @@ -72,23 +101,26 @@ def max_gross_profit_objective(self, blocks): ) def min_operating_cost_objective(self, blocks): + """Trough CSP instance of minimum operating cost objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = sum( blocks[t].time_weighting_factor * ( - self.blocks[t].cost_per_field_start - * self.blocks[t].incur_field_start + self.blocks[t].cost_per_field_start * self.blocks[t].incur_field_start - ( self.blocks[t].cost_per_field_generation * self.blocks[t].receiver_thermal_power * self.blocks[t].time_duration - ) # Trying to incentivize TES generation + ) # Trying to incentivize TES generation + ( self.blocks[t].cost_per_cycle_generation * self.blocks[t].cycle_generation * self.blocks[t].time_duration ) - + self.blocks[t].cost_per_cycle_start - * self.blocks[t].incur_cycle_start + + self.blocks[t].cost_per_cycle_start * self.blocks[t].incur_cycle_start + self.blocks[t].cost_per_change_thermal_input * self.blocks[t].cycle_thermal_ramp ) @@ -96,6 +128,16 @@ def min_operating_cost_objective(self, blocks): ) def _create_variables(self, hybrid): + """Create Trough CSP variables to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + tuple: Tuple containing created variables. + - generation: Generation from given technology. + - load: Load from given technology. + """ hybrid.trough_generation = Var( doc="Power generation of CSP trough [MW]", domain=NonNegativeReals, @@ -111,10 +153,19 @@ def _create_variables(self, hybrid): return hybrid.trough_generation, hybrid.trough_load def _create_port(self, hybrid): + """ + Create CSP trough port to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + Port: CSP Trough Port object. + """ hybrid.trough_port = Port( initialize={ - 'cycle_generation': hybrid.trough_generation, - 'system_load': hybrid.trough_load, + "cycle_generation": hybrid.trough_generation, + "system_load": hybrid.trough_load, } ) return hybrid.trough_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index 44b9c7282..a4c43c568 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -6,7 +6,7 @@ from hopp.simulation.technologies.financial import FinancialModelType from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import ( - PowerSourceDispatch + PowerSourceDispatch, ) @@ -15,16 +15,38 @@ class WaveDispatch(PowerSourceDispatch): _system_model: MhkWave.MhkWave _financial_model: FinancialModelType """ + Dispatch optimization model for mhk wave power source. + Attributes: + wave_obj: Wave object. + _system_model: System model. + _financial_model: Financial model. + + Methods: + max_gross_profit_objective(blocks): Maximum gross profit objective method. + min_operating_cost_objective(blocks): Minimum operating cost objective method. + _create_variables(hybrid): Create variables method. + _create_port(hybrid): Create port method. """ + def __init__( self, pyomo_model: ConcreteModel, indexed_set: Set, system_model: MhkWave.MhkWave, financial_model: FinancialModelType, - block_set_name: str = 'wave', + block_set_name: str = "wave", ): + """ + Initialize WaveDispatch. + + Args: + pyomo_model (ConcreteModel): Pyomo concrete model. + indexed_set (Set): Indexed set. + system_model (MhkWave.MhkWave): System model. + financial_model (FinancialModelType): Financial model. + block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, indexed_set, @@ -34,19 +56,31 @@ def __init__( ) def max_gross_profit_objective(self, blocks): + """MHK wave instance of maximum gross profit objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = Expression( - expr=sum( - - (1/blocks[t].time_weighting_factor) - * self.blocks[t].time_duration - * self.blocks[t].cost_per_generation - * blocks[t].wave_generation - for t in blocks.index_set() - ) + expr=sum( + -(1 / blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].wave_generation + for t in blocks.index_set() ) + ) def min_operating_cost_objective(self, blocks): + """MHK wave instance of minimum operating cost objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = sum( - blocks[t].time_weighting_factor + blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation * blocks[t].wave_generation @@ -54,8 +88,20 @@ def min_operating_cost_objective(self, blocks): ) def _create_variables(self, hybrid): + """ + Create MHK wave variables to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + tuple: Tuple containing created variables. + - generation: Generation from given technology. + - load: Load from given technology. + + """ hybrid.wave_generation = Var( - doc="Power generation of wind turbines [MW]", + doc="Power generation of wave devices [MW]", domain=NonNegativeReals, units=units.MW, initialize=0.0, @@ -63,5 +109,14 @@ def _create_variables(self, hybrid): return hybrid.wave_generation, 0 def _create_port(self, hybrid): - hybrid.wave_port = Port(initialize={'generation': hybrid.wave_generation}) + """ + Create mhk wave port to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + Port: MHK wave Port object. + """ + hybrid.wave_port = Port(initialize={"generation": hybrid.wave_generation}) return hybrid.wave_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 1f9c7bd02..fb38e8764 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -6,7 +6,7 @@ from hopp.simulation.technologies.financial import FinancialModelType from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import ( - PowerSourceDispatch + PowerSourceDispatch, ) if TYPE_CHECKING: @@ -15,19 +15,42 @@ class WindDispatch(PowerSourceDispatch): wind_obj: Union[Expression, float] - _system_model: Union[Windpower.Windpower,"Floris"] + _system_model: Union[Windpower.Windpower, "Floris"] _financial_model: FinancialModelType """ + Dispatch optimization model for wind power source. + Attributes: + wind_obj: Wind object. + _system_model: System model. + _financial_model: Financial model. + + Methods: + max_gross_profit_objective(blocks): Maximum gross profit objective method. + min_operating_cost_objective(blocks): Minimum operating cost objective method. + _create_variables(hybrid): Create variables method. + _create_port(hybrid): Create port method. """ + def __init__( self, pyomo_model: ConcreteModel, indexed_set: Set, - system_model: Union[Windpower.Windpower,"Floris"], + system_model: Union[Windpower.Windpower, "Floris"], financial_model: FinancialModelType, - block_set_name: str = 'wind', + block_set_name: str = "wind", ): + """ + Initialize WindDispatch. + + Args: + pyomo_model (ConcreteModel): Pyomo concrete model. + indexed_set (Set): Indexed set. + system_model (Union[Windpower.Windpower,"Floris"]): System model. + financial_model (FinancialModelType): Financial model. + block_set_name (str): Name of the block set. + """ + super().__init__( pyomo_model, indexed_set, @@ -37,19 +60,31 @@ def __init__( ) def max_gross_profit_objective(self, blocks): + """Wind instance of maximum gross profit objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = Expression( - expr=sum( - - (1/blocks[t].time_weighting_factor) - * self.blocks[t].time_duration - * self.blocks[t].cost_per_generation - * blocks[t].wind_generation - for t in blocks.index_set() - ) + expr=sum( + -(1 / blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * self.blocks[t].cost_per_generation + * blocks[t].wind_generation + for t in blocks.index_set() ) + ) def min_operating_cost_objective(self, blocks): + """Wind instance of minimum operating cost objective. + + Args: + blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ self.obj = sum( - blocks[t].time_weighting_factor + blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation * blocks[t].wind_generation @@ -57,6 +92,18 @@ def min_operating_cost_objective(self, blocks): ) def _create_variables(self, hybrid): + """ + Create wind variables to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + tuple: Tuple containing created variables. + - generation: Generation from given technology. + - load: Load from given technology. + + """ hybrid.wind_generation = Var( doc="Power generation of wind turbines [MW]", domain=NonNegativeReals, @@ -66,5 +113,14 @@ def _create_variables(self, hybrid): return hybrid.wind_generation, 0 def _create_port(self, hybrid): - hybrid.wind_port = Port(initialize={'generation': hybrid.wind_generation}) + """ + Create wind port to add to hybrid plant instance. + + Args: + hybrid: Hybrid plant instance. + + Returns: + Port: Wind Port object. + """ + hybrid.wind_port = Port(initialize={"generation": hybrid.wind_generation}) return hybrid.wind_port From 75e8ef605a0a2811c7bb95d9d23dc1e718f29ac7 Mon Sep 17 00:00:00 2001 From: bayc Date: Wed, 17 Apr 2024 13:53:24 -0600 Subject: [PATCH 22/27] changing blocks to hybrid_blocks for clarity --- .../technologies/dispatch/grid_dispatch.py | 24 +++++++++---------- .../dispatch/power_sources/pv_dispatch.py | 20 ++++++++-------- .../dispatch/power_sources/tower_dispatch.py | 16 ++++++------- .../dispatch/power_sources/trough_dispatch.py | 18 +++++++------- .../dispatch/power_sources/wave_dispatch.py | 20 ++++++++-------- .../dispatch/power_sources/wind_dispatch.py | 20 ++++++++-------- .../power_storage/power_storage_dispatch.py | 18 +++++++------- 7 files changed, 69 insertions(+), 67 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index 492dbdde3..53c49d429 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -42,43 +42,43 @@ def dispatch_block_rule(self, grid): # Ports self._create_grid_ports(grid) - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): self.obj = pyomo.Expression( expr=sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].electricity_sell_price - * blocks[t].electricity_sold - - (1/blocks[t].time_weighting_factor) + * hybrid_blocks[t].electricity_sold + - (1/hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * self.blocks[t].electricity_purchase_price - * blocks[t].electricity_purchased + * hybrid_blocks[t].electricity_purchased - self.blocks[t].epsilon * self.blocks[t].is_generating - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) ) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): self.obj = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].electricity_sell_price * ( self.blocks[t].generation_transmission_limit - - blocks[t].electricity_sold + - hybrid_blocks[t].electricity_sold ) + ( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].electricity_purchase_price - * blocks[t].electricity_purchased + * hybrid_blocks[t].electricity_purchased ) + ( self.blocks[t].epsilon * self.blocks[t].is_generating ) - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) def _create_variables(self, hybrid): diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 9a2831eec..84fda34c6 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -68,36 +68,36 @@ def update_time_series_parameters(self, start_time: int): # zero out any negative load self.available_generation = [max(0, i) for i in self.available_generation] - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): """PV instance of maximum gross profit objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( expr=sum( - -(1 / blocks[t].time_weighting_factor) + -(1 / hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * self.blocks[t].cost_per_generation - * blocks[t].pv_generation - for t in blocks.index_set() + * hybrid_blocks[t].pv_generation + for t in hybrid_blocks.index_set() ) ) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): """PV instance of minimum operating cost objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation - * blocks[t].pv_generation - for t in blocks.index_set() + * hybrid_blocks[t].pv_generation + for t in hybrid_blocks.index_set() ) def _create_variables(self, hybrid): diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index 1a2379081..4f5cd3071 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -81,16 +81,16 @@ def update_initial_conditions(self): self.receiver_required_startup_energy ) - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): """Tower CSP instance of maximum gross profit objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( expr=sum( - -(1 / blocks[t].time_weighting_factor) + -(1 / hybrid_blocks[t].time_weighting_factor) * ( ( self.blocks[t].cost_per_field_generation @@ -115,19 +115,19 @@ def max_gross_profit_objective(self, blocks): * self.blocks[t].cycle_thermal_ramp ) ) - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) ) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): """Tower CSP instance of minimum operating cost objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * ( self.blocks[t].cost_per_field_start * self.blocks[t].incur_field_start - ( @@ -144,7 +144,7 @@ def min_operating_cost_objective(self, blocks): + self.blocks[t].cost_per_change_thermal_input * self.blocks[t].cycle_thermal_ramp ) - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) def _create_variables(self, hybrid): diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index 4184eaf76..c18312c7b 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -63,15 +63,16 @@ def update_initial_conditions(self): "result in persistent receiver start-up." ) - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): """Trough CSP instance of maximum gross profit objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. """ self.obj = Expression( expr=sum( - -(1 / blocks[t].time_weighting_factor) + -(1 / hybrid_blocks[t].time_weighting_factor) * ( ( self.blocks[t].cost_per_field_generation @@ -96,18 +97,19 @@ def max_gross_profit_objective(self, blocks): * self.blocks[t].cycle_thermal_ramp ) ) - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) ) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): """Trough CSP instance of minimum operating cost objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. """ self.obj = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * ( self.blocks[t].cost_per_field_start * self.blocks[t].incur_field_start - ( @@ -124,7 +126,7 @@ def min_operating_cost_objective(self, blocks): + self.blocks[t].cost_per_change_thermal_input * self.blocks[t].cycle_thermal_ramp ) - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) def _create_variables(self, hybrid): diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index a4c43c568..cd1e1ec14 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -55,36 +55,36 @@ def __init__( block_set_name=block_set_name, ) - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): """MHK wave instance of maximum gross profit objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( expr=sum( - -(1 / blocks[t].time_weighting_factor) + -(1 / hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * self.blocks[t].cost_per_generation - * blocks[t].wave_generation - for t in blocks.index_set() + * hybrid_blocks[t].wave_generation + for t in hybrid_blocks.index_set() ) ) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): """MHK wave instance of minimum operating cost objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation - * blocks[t].wave_generation - for t in blocks.index_set() + * hybrid_blocks[t].wave_generation + for t in hybrid_blocks.index_set() ) def _create_variables(self, hybrid): diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index fb38e8764..3a268920a 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -59,36 +59,36 @@ def __init__( block_set_name=block_set_name, ) - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): """Wind instance of maximum gross profit objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( expr=sum( - -(1 / blocks[t].time_weighting_factor) + -(1 / hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * self.blocks[t].cost_per_generation - * blocks[t].wind_generation - for t in blocks.index_set() + * hybrid_blocks[t].wind_generation + for t in hybrid_blocks.index_set() ) ) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): """Wind instance of minimum operating cost objective. Args: - blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * self.blocks[t].cost_per_generation - * blocks[t].wind_generation - for t in blocks.index_set() + * hybrid_blocks[t].wind_generation + for t in hybrid_blocks.index_set() ) def _create_variables(self, hybrid): diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index bd3bfb8a0..5bf9496da 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -53,34 +53,34 @@ def dispatch_block_rule(self, storage): # Ports self._create_storage_port(storage) - def max_gross_profit_objective(self, blocks): + def max_gross_profit_objective(self, hybrid_blocks): def battery_profit_objective_rule(m): objective = 0 objective += sum( - - (1/blocks[t].time_weighting_factor) + - (1/hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * ( self.blocks[t].cost_per_charge - * blocks[t].battery_charge + * hybrid_blocks[t].battery_charge + self.blocks[t].cost_per_discharge - * blocks[t].battery_discharge + * hybrid_blocks[t].battery_discharge ) - for t in blocks.index_set() + for t in hybrid_blocks.index_set() ) if self.options.include_lifecycle_count: objective -= self.model.lifecycle_cost * sum(self.model.lifecycles) return objective self.obj = pyomo.Expression(rule=battery_profit_objective_rule) - def min_operating_cost_objective(self, blocks): + def min_operating_cost_objective(self, hybrid_blocks): objective = sum( - blocks[t].time_weighting_factor + hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * ( self.blocks[t].cost_per_discharge - * blocks[t].battery_discharge + * hybrid_blocks[t].battery_discharge - self.blocks[t].cost_per_charge - * blocks[t].battery_charge + * hybrid_blocks[t].battery_charge ) # Try to incentivize battery charging for t in self.blocks.index_set() ) From 424186cd44f3f92f93bb125ecfbccc5a6d73a620 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Wed, 17 Apr 2024 12:54:16 -0700 Subject: [PATCH 23/27] add docstrings, reformat power storage. --- .../power_sources/power_source_dispatch.py | 4 +- .../heuristic_load_following_dispatch.py | 59 ++- .../linear_voltage_convex_battery_dispatch.py | 305 ++++++++++++---- ...near_voltage_nonconvex_battery_dispatch.py | 285 +++++++++++---- .../one_cycle_battery_dispatch_heuristic.py | 175 ++++++--- .../power_storage/power_storage_dispatch.py | 342 +++++++++++++----- .../simple_battery_dispatch_heuristic.py | 176 ++++++--- 7 files changed, 992 insertions(+), 354 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py index 535ab781e..275e6d919 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py @@ -20,8 +20,8 @@ class PowerSourceDispatch(Dispatch): dispatch_block_rule(gen): Dispatch block rule method. initialize_parameters(): Initialize parameters method. update_time_series_parameters(start_time): Update time series parameters method. - _create_variables(hyrbid): Create variables method (abstract). - _create_port(hyrbid): Create port method (abstract). + _create_variables(hybrid): Create variables method (abstract). + _create_port(hybrid): Create port method (abstract). Properties: cost_per_generation: Cost per generation property. diff --git a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py index 2d6ae049a..a7e7222e7 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py @@ -5,27 +5,39 @@ import PySAM.BatteryStateful as BatteryModel import PySAM.Singleowner as Singleowner -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import SimpleBatteryDispatchHeuristic +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import ( + SimpleBatteryDispatchHeuristic, +) class HeuristicLoadFollowingDispatch(SimpleBatteryDispatchHeuristic): - """Operates the battery based on heuristic rules to meet the demand profile based power available from power generation profiles and + """Operates the battery based on heuristic rules to meet the demand profile based power available from power generation profiles and power demand profile. Currently, enforces available generation and grid limit assuming no battery charging from grid """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model: BatteryModel.BatteryStateful, - financial_model: Singleowner.Singleowner, - fixed_dispatch: Optional[List] = None, - block_set_name: str = 'heuristic_load_following_battery', - dispatch_options: Optional[dict] = None): + + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: Singleowner.Singleowner, + fixed_dispatch: Optional[List] = None, + block_set_name: str = "heuristic_load_following_battery", + dispatch_options: Optional[dict] = None, + ): """ + Initialize HeuristicLoadFollowingDispatch. Args: - fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Indexed set. + system_model (BatteryModel.BatteryStateful): System model. + financial_model (Singleowner.Singleowner): Financial model. + fixed_dispatch (Optional[List], optional): List of normalized values [-1, 1] (Charging (-), Discharging (+)). Defaults to None. + block_set_name (str, optional): Name of the block set. Defaults to 'heuristic_load_following_battery'. + dispatch_options (Optional[dict], optional): Dispatch options. Defaults to None. """ super().__init__( pyomo_model, @@ -34,29 +46,40 @@ def __init__(self, financial_model, fixed_dispatch, block_set_name, - dispatch_options + dispatch_options, ) def set_fixed_dispatch(self, gen: list, grid_limit: list, goal_power: list): - """Sets charge and discharge power of battery dispatch using fixed_dispatch attribute and enforces available - generation and grid limits. + """ + Sets charge and discharge power of battery dispatch using fixed_dispatch attribute + and enforces available generation and grid limits. + Args: + gen (list): List of power generation. + grid_limit (list): List of grid limits. + goal_power (list): List of goal power. """ + self.check_gen_grid_limit(gen, grid_limit) self._set_power_fraction_limits(gen, grid_limit) self._heuristic_method(gen, goal_power) self._fix_dispatch_model_variables() def _heuristic_method(self, gen, goal_power): - """ Enforces battery power fraction limits and sets _fixed_dispatch attribute - Sets the _fixed_dispatch based on goal_power and gen (power genration profile) + """ + Enforces battery power fraction limits and sets _fixed_dispatch attribute. + Sets the _fixed_dispatch based on goal_power and gen (power generation profile). + + Args: + gen: Power generation profile. + goal_power: Goal power. """ for t in self.blocks.index_set(): fd = (goal_power[t] - gen[t]) / self.maximum_power - if fd > 0.0: # Discharging + if fd > 0.0: # Discharging if fd > self.max_discharge_fraction[t]: fd = self.max_discharge_fraction[t] elif fd < 0.0: # Charging if -fd > self.max_charge_fraction[t]: fd = -self.max_charge_fraction[t] - self._fixed_dispatch[t] = fd \ No newline at end of file + self._fixed_dispatch[t] = fd diff --git a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py index 3632d53a0..30bbf34ed 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py @@ -4,34 +4,67 @@ import PySAM.BatteryStateful as BatteryModel import PySAM.Singleowner as Singleowner -from hopp.simulation.technologies.dispatch.power_storage.linear_voltage_nonconvex_battery_dispatch import NonConvexLinearVoltageBatteryDispatch +from hopp.simulation.technologies.dispatch.power_storage.linear_voltage_nonconvex_battery_dispatch import ( + NonConvexLinearVoltageBatteryDispatch, +) class ConvexLinearVoltageBatteryDispatch(NonConvexLinearVoltageBatteryDispatch): """ - + This class represents a convex linear voltage battery dispatch model. + + It extends the NonConvexLinearVoltageBatteryDispatch model and adds additional formulation to enforce convexity. + + Attributes: + _system_model: The battery system model. + _financial_model: The financial model. + block_set_name: The name of the block set. + dispatch_options: Dispatch options. + use_exp_voltage_point: Boolean indicating whether to use the exponential voltage point. """ + # TODO: add a reference to original paper - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model: BatteryModel.BatteryStateful, - financial_model: Singleowner.Singleowner, - block_set_name: str = 'convex_LV_battery', - dispatch_options: dict = None, - use_exp_voltage_point: bool = False): + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: Singleowner.Singleowner, + block_set_name: str = "convex_LV_battery", + dispatch_options: dict = None, + use_exp_voltage_point: bool = False, + ): + """Initialize ConvexLinearVoltageBatteryDispatch. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Indexed set. + system_model (BatteryModel.BatteryStateful): Battery system model. + financial_model (Singleowner.Singleowner): Financial model. + block_set_name (str, optional): Name of the block set. Defaults to 'convex_LV_battery'. + dispatch_options (dict, optional): Dispatch options. Defaults to None. + use_exp_voltage_point (bool, optional): Boolean indicating whether to use the exponential voltage point. Defaults to False. + """ if dispatch_options is None: dispatch_options = {} - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name, - dispatch_options=dispatch_options, - use_exp_voltage_point=use_exp_voltage_point) + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + dispatch_options=dispatch_options, + use_exp_voltage_point=use_exp_voltage_point, + ) def dispatch_block_rule(self, battery): + """ + Additional formulation for dispatch block rule. + + Args: + battery: Battery instance. + """ # Additional formulation # Variables self._create_lv_battery_auxiliary_variables(battery) @@ -39,148 +72,264 @@ def dispatch_block_rule(self, battery): @staticmethod def _create_lv_battery_auxiliary_variables(battery): + """ + Create auxiliary variables for the battery model. + + Args: + battery: Battery instance. + """ # Auxiliary Variables battery.aux_charge_current_soc = pyomo.Var( doc="Auxiliary bi-linear term equal to the product of charge current and previous state-of-charge [MA]", domain=pyomo.NonNegativeReals, - units=u.MA) # = charge_current[t] * soc[t-1] + units=u.MA, + ) # = charge_current[t] * soc[t-1] battery.aux_charge_current_is_charging = pyomo.Var( doc="Auxiliary bi-linear term equal to the product of charge current and charging binary [MA]", domain=pyomo.NonNegativeReals, - units=u.MA) # = charge_current[t] * is_charging[t] + units=u.MA, + ) # = charge_current[t] * is_charging[t] battery.aux_discharge_current_soc = pyomo.Var( doc="Auxiliary bi-linear equal to the product of discharge current and previous state-of-charge [MA]", domain=pyomo.NonNegativeReals, - units=u.MA) # = discharge_current[t] * soc[t-1] + units=u.MA, + ) # = discharge_current[t] * soc[t-1] battery.aux_discharge_current_is_discharging = pyomo.Var( doc="Auxiliary bi-linear term equal to the product of discharge current and discharging binary [MA]", domain=pyomo.NonNegativeReals, - units=u.MA) # = discharge_current[t] * is_discharging[t] + units=u.MA, + ) # = discharge_current[t] * is_discharging[t] @staticmethod def _create_lv_battery_power_equation_constraints(battery): + """ + Create power equation constraints for the battery model. + + Args: + battery: Battery instance. + """ battery.charge_power_equation = pyomo.Constraint( doc="Battery charge power equation equal to the product of current and voltage", - expr=battery.charge_power == (battery.voltage_slope * battery.aux_charge_current_soc - + (battery.voltage_intercept - + battery.average_current * battery.internal_resistance - ) * battery.aux_charge_current_is_charging)) + expr=battery.charge_power + == ( + battery.voltage_slope * battery.aux_charge_current_soc + + ( + battery.voltage_intercept + + battery.average_current * battery.internal_resistance + ) + * battery.aux_charge_current_is_charging + ), + ) battery.discharge_power_equation = pyomo.Constraint( doc="Battery discharge power equation equal to the product of current and voltage", - expr=battery.discharge_power == (battery.voltage_slope * battery.aux_discharge_current_soc - + (battery.voltage_intercept - - battery.average_current * battery.internal_resistance - ) * battery.aux_discharge_current_is_discharging)) + expr=battery.discharge_power + == ( + battery.voltage_slope * battery.aux_discharge_current_soc + + ( + battery.voltage_intercept + - battery.average_current * battery.internal_resistance + ) + * battery.aux_discharge_current_is_discharging + ), + ) # Auxiliary Variable bounds (binary*continuous exact linearization) # Charge current * charging binary battery.aux_charge_lb = pyomo.Constraint( doc="Charge current * charge binary lower bound", - expr=battery.aux_charge_current_is_charging >= battery.minimum_charge_current * battery.is_charging) + expr=battery.aux_charge_current_is_charging + >= battery.minimum_charge_current * battery.is_charging, + ) battery.aux_charge_ub = pyomo.Constraint( doc="Charge current * charge binary upper bound", - expr=battery.aux_charge_current_is_charging <= battery.maximum_charge_current * battery.is_charging) + expr=battery.aux_charge_current_is_charging + <= battery.maximum_charge_current * battery.is_charging, + ) battery.aux_charge_diff_lb = pyomo.Constraint( doc="Charge current and auxiliary difference lower bound", - expr=(battery.charge_current - battery.aux_charge_current_is_charging - >= - battery.maximum_charge_current * (1 - battery.is_charging))) + expr=( + battery.charge_current - battery.aux_charge_current_is_charging + >= -battery.maximum_charge_current * (1 - battery.is_charging) + ), + ) battery.aux_charge_diff_ub = pyomo.Constraint( doc="Charge current and auxiliary difference upper bound", - expr=(battery.charge_current - battery.aux_charge_current_is_charging - <= battery.maximum_charge_current * (1 - battery.is_charging))) + expr=( + battery.charge_current - battery.aux_charge_current_is_charging + <= battery.maximum_charge_current * (1 - battery.is_charging) + ), + ) # Discharge current * discharging binary battery.aux_discharge_lb = pyomo.Constraint( doc="discharge current * discharge binary lower bound", - expr=(battery.aux_discharge_current_is_discharging - >= battery.minimum_discharge_current * battery.is_discharging)) + expr=( + battery.aux_discharge_current_is_discharging + >= battery.minimum_discharge_current * battery.is_discharging + ), + ) battery.aux_discharge_ub = pyomo.Constraint( doc="discharge current * discharge binary upper bound", - expr=(battery.aux_discharge_current_is_discharging - <= battery.maximum_discharge_current * battery.is_discharging)) + expr=( + battery.aux_discharge_current_is_discharging + <= battery.maximum_discharge_current * battery.is_discharging + ), + ) battery.aux_discharge_diff_lb = pyomo.Constraint( doc="discharge current and auxiliary difference lower bound", - expr=(battery.discharge_current - battery.aux_discharge_current_is_discharging - >= - battery.maximum_discharge_current * (1 - battery.is_discharging))) + expr=( + battery.discharge_current - battery.aux_discharge_current_is_discharging + >= -battery.maximum_discharge_current * (1 - battery.is_discharging) + ), + ) battery.aux_discharge_diff_ub = pyomo.Constraint( doc="discharge current and auxiliary difference upper bound", - expr=(battery.discharge_current - battery.aux_discharge_current_is_discharging - <= battery.maximum_discharge_current * (1 - battery.is_discharging))) + expr=( + battery.discharge_current - battery.aux_discharge_current_is_discharging + <= battery.maximum_discharge_current * (1 - battery.is_discharging) + ), + ) # Auxiliary Variable bounds (continuous*continuous approx. linearization) # TODO: The error in these constraints should be quantified # TODO: scaling the problem to between [0,1] might help battery.aux_charge_soc_lower1 = pyomo.Constraint( doc="McCormick envelope underestimate 1", - expr=battery.aux_charge_current_soc >= (battery.maximum_charge_current * battery.soc0 - + battery.maximum_soc * battery.charge_current - - battery.maximum_soc * battery.maximum_charge_current)) + expr=battery.aux_charge_current_soc + >= ( + battery.maximum_charge_current * battery.soc0 + + battery.maximum_soc * battery.charge_current + - battery.maximum_soc * battery.maximum_charge_current + ), + ) battery.aux_charge_soc_lower2 = pyomo.Constraint( doc="McCormick envelope underestimate 2", - expr=battery.aux_charge_current_soc >= (battery.minimum_charge_current * battery.soc0 - + battery.minimum_soc * battery.charge_current - - battery.minimum_soc * battery.minimum_charge_current)) + expr=battery.aux_charge_current_soc + >= ( + battery.minimum_charge_current * battery.soc0 + + battery.minimum_soc * battery.charge_current + - battery.minimum_soc * battery.minimum_charge_current + ), + ) battery.aux_charge_soc_upper1 = pyomo.Constraint( doc="McCormick envelope overestimate 1", - expr=battery.aux_charge_current_soc <= (battery.maximum_charge_current * battery.soc0 - + battery.minimum_soc * battery.charge_current - - battery.minimum_soc * battery.maximum_charge_current)) + expr=battery.aux_charge_current_soc + <= ( + battery.maximum_charge_current * battery.soc0 + + battery.minimum_soc * battery.charge_current + - battery.minimum_soc * battery.maximum_charge_current + ), + ) battery.aux_charge_soc_upper2 = pyomo.Constraint( doc="McCormick envelope overestimate 2", - expr=battery.aux_charge_current_soc <= (battery.minimum_charge_current * battery.soc0 - + battery.maximum_soc * battery.charge_current - - battery.maximum_soc * battery.minimum_charge_current)) + expr=battery.aux_charge_current_soc + <= ( + battery.minimum_charge_current * battery.soc0 + + battery.maximum_soc * battery.charge_current + - battery.maximum_soc * battery.minimum_charge_current + ), + ) battery.aux_discharge_soc_lower1 = pyomo.Constraint( doc="McCormick envelope underestimate 1", - expr=battery.aux_discharge_current_soc >= (battery.maximum_discharge_current * battery.soc0 - + battery.maximum_soc * battery.discharge_current - - battery.maximum_soc * battery.maximum_discharge_current)) + expr=battery.aux_discharge_current_soc + >= ( + battery.maximum_discharge_current * battery.soc0 + + battery.maximum_soc * battery.discharge_current + - battery.maximum_soc * battery.maximum_discharge_current + ), + ) battery.aux_discharge_soc_lower2 = pyomo.Constraint( doc="McCormick envelope underestimate 2", - expr=battery.aux_discharge_current_soc >= (battery.minimum_discharge_current * battery.soc0 - + battery.minimum_soc * battery.discharge_current - - battery.minimum_soc * battery.minimum_discharge_current)) + expr=battery.aux_discharge_current_soc + >= ( + battery.minimum_discharge_current * battery.soc0 + + battery.minimum_soc * battery.discharge_current + - battery.minimum_soc * battery.minimum_discharge_current + ), + ) battery.aux_discharge_soc_upper1 = pyomo.Constraint( doc="McCormick envelope overestimate 1", - expr=battery.aux_discharge_current_soc <= (battery.maximum_discharge_current * battery.soc0 - + battery.minimum_soc * battery.discharge_current - - battery.minimum_soc * battery.maximum_discharge_current)) + expr=battery.aux_discharge_current_soc + <= ( + battery.maximum_discharge_current * battery.soc0 + + battery.minimum_soc * battery.discharge_current + - battery.minimum_soc * battery.maximum_discharge_current + ), + ) battery.aux_discharge_soc_upper2 = pyomo.Constraint( doc="McCormick envelope overestimate 2", - expr=battery.aux_discharge_current_soc <= (battery.minimum_discharge_current * battery.soc0 - + battery.maximum_soc * battery.discharge_current - - battery.maximum_soc * battery.minimum_discharge_current)) + expr=battery.aux_discharge_current_soc + <= ( + battery.minimum_discharge_current * battery.soc0 + + battery.maximum_soc * battery.discharge_current + - battery.maximum_soc * battery.minimum_discharge_current + ), + ) def _lifecycle_count_rule(self, m, i): + """ + Lifecycle count rule. + + Args: + m: Model instance. + i: Index. + """ # current accounting # TODO: Check for cheating -> there seems to be a lot of error start = int(i * self.timesteps_per_day) end = int((i + 1) * self.timesteps_per_day) - return self.model.lifecycles[i] == sum(self.blocks[t].time_duration - * (0.8 * self.blocks[t].discharge_current - - 0.8 * self.blocks[t].aux_discharge_current_soc) - / self.blocks[t].capacity for t in range(start, end)) + return self.model.lifecycles[i] == sum( + self.blocks[t].time_duration + * ( + 0.8 * self.blocks[t].discharge_current + - 0.8 * self.blocks[t].aux_discharge_current_soc + ) + / self.blocks[t].capacity + for t in range(start, end) + ) # Auxiliary Variables @property def aux_charge_current_soc(self) -> list: - return [self.blocks[t].aux_charge_current_soc.value for t in self.blocks.index_set()] + """List of auxiliary charge current state of charge.""" + return [ + self.blocks[t].aux_charge_current_soc.value for t in self.blocks.index_set() + ] @property def real_charge_current_soc(self) -> list: - return [self.blocks[t].charge_current.value * self.blocks[t].soc0.value for t in self.blocks.index_set()] + """List of real charge current state of charge.""" + return [ + self.blocks[t].charge_current.value * self.blocks[t].soc0.value + for t in self.blocks.index_set() + ] @property def aux_charge_current_is_charging(self) -> list: - return [self.blocks[t].aux_charge_current_is_charging.value for t in self.blocks.index_set()] + """List of auxiliary charge current charging status.""" + return [ + self.blocks[t].aux_charge_current_is_charging.value + for t in self.blocks.index_set() + ] @property def aux_discharge_current_soc(self) -> list: - return [self.blocks[t].aux_discharge_current_soc.value for t in self.blocks.index_set()] + """List of auxiliary discharge current state of charge.""" + return [ + self.blocks[t].aux_discharge_current_soc.value + for t in self.blocks.index_set() + ] @property def real_discharge_current_soc(self) -> list: - return [self.blocks[t].discharge_current.value * self.blocks[t].soc0.value for t in self.blocks.index_set()] + """List of real discharge current state of charge.""" + return [ + self.blocks[t].discharge_current.value * self.blocks[t].soc0.value + for t in self.blocks.index_set() + ] @property def aux_discharge_current_is_discharging(self) -> list: - return [self.blocks[t].aux_discharge_current_is_discharging.value for t in self.blocks.index_set()] - + """List of auxiliary discharge current discharging status.""" + return [ + self.blocks[t].aux_discharge_current_is_discharging.value + for t in self.blocks.index_set() + ] diff --git a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py index 26b5e7c1f..62911af10 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py @@ -4,35 +4,68 @@ import PySAM.BatteryStateful as BatteryModel import PySAM.Singleowner as Singleowner -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import SimpleBatteryDispatch +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import ( + SimpleBatteryDispatch, +) class NonConvexLinearVoltageBatteryDispatch(SimpleBatteryDispatch): """ + This class represents a non-convex linear voltage battery dispatch model. + It extends the SimpleBatteryDispatch model and adds additional formulation to handle non-convex behavior. + + Attributes: + _system_model: The battery system model. + _financial_model: The financial model. + block_set_name: The name of the block set. + dispatch_options: Dispatch options. + use_exp_voltage_point: Boolean indicating whether to use the exponential voltage point. """ + # TODO: add a reference to original paper - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model: BatteryModel.BatteryStateful, - financial_model: Singleowner.Singleowner, - block_set_name: str = 'LV_battery', - dispatch_options: dict = None, - use_exp_voltage_point: bool = False): - u.load_definitions_from_strings(['amp_hour = amp * hour = Ah = amphour']) + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: Singleowner.Singleowner, + block_set_name: str = "LV_battery", + dispatch_options: dict = None, + use_exp_voltage_point: bool = False, + ): + """Initialize NonConvexLinearVoltageBatteryDispatch. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Indexed set. + system_model (BatteryModel.BatteryStateful): Battery system model. + financial_model (Singleowner.Singleowner): Financial model. + block_set_name (str, optional): Name of the block set. Defaults to 'LV_battery'. + dispatch_options (dict, optional): Dispatch options. Defaults to None. + use_exp_voltage_point (bool, optional): Boolean indicating whether to use the exponential voltage point. Defaults to False. + """ + u.load_definitions_from_strings(["amp_hour = amp * hour = Ah = amphour"]) if dispatch_options is None: dispatch_options = {} - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name, - dispatch_options=dispatch_options) + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + dispatch_options=dispatch_options, + ) self.use_exp_voltage_point = use_exp_voltage_point def dispatch_block_rule(self, battery): + """ + Additional formulation for dispatch block rule. + + Args: + battery: Battery instance. + """ # Parameters self._create_lv_battery_parameters(battery) # Variables @@ -44,27 +77,41 @@ def dispatch_block_rule(self, battery): self._create_lv_battery_power_equation_constraints(battery) def _create_efficiency_parameters(self, battery): + """Not defined in this formulation.""" # Not defined in this formulation pass def _create_capacity_parameter(self, battery): + """Create capacity parameter for the battery model. + + Args: + battery: Battery instance. + """ battery.capacity = pyomo.Param( doc=self.block_set_name + " capacity [MAh]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MAh) + units=u.MAh, + ) def _create_lv_battery_parameters(self, battery): + """Create parameters for the battery model. + + Args: + battery: Battery instance. + """ battery.voltage_slope = pyomo.Param( doc=self.block_set_name + " linear voltage model slope coefficient [V]", within=pyomo.NonNegativeReals, mutable=True, - units=u.V) + units=u.V, + ) battery.voltage_intercept = pyomo.Param( doc=self.block_set_name + " linear voltage model intercept coefficient [V]", within=pyomo.NonNegativeReals, mutable=True, - units=u.V) + units=u.V, + ) # TODO: Add this if wanted # self.alphaP = Param(None) # [kW_DC] Bi-directional intercept for charge # self.betaP = Param(None) # [-] Bi-directional slope for charge @@ -74,123 +121,198 @@ def _create_lv_battery_parameters(self, battery): doc="Typical cell current for both charge and discharge [A]", within=pyomo.NonNegativeReals, mutable=True, - units=u.A) + units=u.A, + ) battery.internal_resistance = pyomo.Param( doc=self.block_set_name + " internal resistance [Ohm]", within=pyomo.NonNegativeReals, mutable=True, - units=u.ohm) + units=u.ohm, + ) battery.minimum_charge_current = pyomo.Param( doc=self.block_set_name + " minimum charge current [MA]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MA) + units=u.MA, + ) battery.maximum_charge_current = pyomo.Param( doc=self.block_set_name + " maximum charge current [MA]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MA) + units=u.MA, + ) battery.minimum_discharge_current = pyomo.Param( doc=self.block_set_name + " minimum discharge current [MA]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MA) + units=u.MA, + ) battery.maximum_discharge_current = pyomo.Param( doc=self.block_set_name + " maximum discharge current [MA]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MA) + units=u.MA, + ) @staticmethod def _create_lv_battery_variables(battery): + """Create variables for the battery model. + + Args: + battery: Battery instance. + """ battery.charge_current = pyomo.Var( doc="Current into the battery [MA]", domain=pyomo.NonNegativeReals, - units=u.MA) + units=u.MA, + ) battery.discharge_current = pyomo.Var( doc="Current out of the battery [MA]", domain=pyomo.NonNegativeReals, - units=u.MA) + units=u.MA, + ) def _create_soc_inventory_constraint(self, storage): + """Create state-of-charge inventory balance constraint. + + Args: + battery: Battery instance. + """ + def soc_inventory_rule(m): # TODO: add alpha and beta terms - return m.soc == (m.soc0 + m.time_duration * (m.charge_current - m.discharge_current) / m.capacity) + return m.soc == ( + m.soc0 + + m.time_duration + * (m.charge_current - m.discharge_current) + / m.capacity + ) + # Storage State-of-charge balance storage.soc_inventory = pyomo.Constraint( doc=self.block_set_name + " state-of-charge inventory balance", - rule=soc_inventory_rule) + rule=soc_inventory_rule, + ) @staticmethod def _create_lv_battery_constraints(battery): + """Create constraints for the battery model. + + Args: + battery: Battery instance. + """ # Charge current bounds battery.charge_current_lb = pyomo.Constraint( doc="Battery Charging current lower bound", - expr=battery.charge_current >= battery.minimum_charge_current * battery.is_charging) + expr=battery.charge_current + >= battery.minimum_charge_current * battery.is_charging, + ) battery.charge_current_ub = pyomo.Constraint( doc="Battery Charging current upper bound", - expr=battery.charge_current <= battery.maximum_charge_current * battery.is_charging) + expr=battery.charge_current + <= battery.maximum_charge_current * battery.is_charging, + ) battery.charge_current_ub_soc = pyomo.Constraint( doc="Battery Charging current upper bound state-of-charge dependence", - expr=battery.charge_current <= battery.capacity * (1.0 - battery.soc0) / battery.time_duration) + expr=battery.charge_current + <= battery.capacity * (1.0 - battery.soc0) / battery.time_duration, + ) # Discharge current bounds battery.discharge_current_lb = pyomo.Constraint( doc="Battery Discharging current lower bound", - expr=battery.discharge_current >= battery.minimum_discharge_current * battery.is_discharging) + expr=battery.discharge_current + >= battery.minimum_discharge_current * battery.is_discharging, + ) battery.discharge_current_ub = pyomo.Constraint( doc="Battery Discharging current upper bound", - expr=battery.discharge_current <= battery.maximum_discharge_current * battery.is_discharging) + expr=battery.discharge_current + <= battery.maximum_discharge_current * battery.is_discharging, + ) battery.discharge_current_ub_soc = pyomo.Constraint( doc="Battery Discharging current upper bound state-of-charge dependence", - expr=battery.discharge_current <= battery.maximum_discharge_current * battery.soc0) + expr=battery.discharge_current + <= battery.maximum_discharge_current * battery.soc0, + ) @staticmethod def _create_lv_battery_power_equation_constraints(battery): + """Create power equation constraints for the battery model. + + Args: + battery: Battery instance. + """ battery.charge_power_equation = pyomo.Constraint( doc="Battery charge power equation equal to the product of current and voltage", - expr=battery.charge_power == battery.charge_current * (battery.voltage_slope * battery.soc0 - + (battery.voltage_intercept - + battery.average_current - * battery.internal_resistance))) + expr=battery.charge_power + == battery.charge_current + * ( + battery.voltage_slope * battery.soc0 + + ( + battery.voltage_intercept + + battery.average_current * battery.internal_resistance + ) + ), + ) battery.discharge_power_equation = pyomo.Constraint( doc="Battery discharge power equation equal to the product of current and voltage", - expr=battery.discharge_power == battery.discharge_current * (battery.voltage_slope * battery.soc0 - + (battery.voltage_intercept - - battery.average_current - * battery.internal_resistance))) + expr=battery.discharge_power + == battery.discharge_current + * ( + battery.voltage_slope * battery.soc0 + + ( + battery.voltage_intercept + - battery.average_current * battery.internal_resistance + ) + ), + ) def _lifecycle_count_rule(self, m, i): + """Lifecycle count rule. + + Args: + m: Model instance. + i: Index. + """ # current accounting start = int(i * self.timesteps_per_day) end = int((i + 1) * self.timesteps_per_day) - return self.model.lifecycles[i] == sum(self.blocks[t].time_duration - * (0.8 * self.blocks[t].discharge_current - - 0.8 * self.blocks[t].discharge_current * self.blocks[t].soc0) - / self.blocks[t].capacity for t in range(start, end)) + return self.model.lifecycles[i] == sum( + self.blocks[t].time_duration + * ( + 0.8 * self.blocks[t].discharge_current + - 0.8 * self.blocks[t].discharge_current * self.blocks[t].soc0 + ) + / self.blocks[t].capacity + for t in range(start, end) + ) def _set_control_mode(self): + """Set control mode.""" self._system_model.value("control_mode", 0.0) # Current control self.control_variable = "input_current" def _set_model_specific_parameters(self): + """Set model-specific parameters.""" # Getting information from system_model - nominal_voltage = self._system_model.value('nominal_voltage') - nominal_energy = self._system_model.value('nominal_energy') - Vnom_default = self._system_model.value('Vnom_default') - C_rate = self._system_model.value('C_rate') - resistance = self._system_model.value('resistance') + nominal_voltage = self._system_model.value("nominal_voltage") + nominal_energy = self._system_model.value("nominal_energy") + Vnom_default = self._system_model.value("Vnom_default") + C_rate = self._system_model.value("C_rate") + resistance = self._system_model.value("resistance") - Qfull = self._system_model.value('Qfull') - Qnom = self._system_model.value('Qnom') - Qexp = self._system_model.value('Qexp') + Qfull = self._system_model.value("Qfull") + Qnom = self._system_model.value("Qnom") + Qexp = self._system_model.value("Qexp") - Vfull = self._system_model.value('Vfull') - Vnom = self._system_model.value('Vnom') - Vexp = self._system_model.value('Vexp') + Vfull = self._system_model.value("Vfull") + Vnom = self._system_model.value("Vnom") + Vexp = self._system_model.value("Vexp") # Using the Ceiling for both these -> Ceil(a/b) = -(-a//b) - cells_in_series = - (- nominal_voltage // Vnom_default) - strings_in_parallel = - (- nominal_energy * 1e3 // (Qfull * cells_in_series * Vnom_default)) + cells_in_series = -(-nominal_voltage // Vnom_default) + strings_in_parallel = -( + -nominal_energy * 1e3 // (Qfull * cells_in_series * Vnom_default) + ) self.capacity = Qfull * strings_in_parallel / 1e6 # [MAh] @@ -211,7 +333,7 @@ def _set_model_specific_parameters(self): self.voltage_slope = cells_in_series * a self.voltage_intercept = cells_in_series * b - self.average_current = (Qfull * strings_in_parallel * C_rate / 2.) + self.average_current = Qfull * strings_in_parallel * C_rate / 2.0 self.internal_resistance = resistance * cells_in_series / strings_in_parallel # TODO: These parameters might need updating self.minimum_charge_current = 0.0 @@ -222,6 +344,7 @@ def _set_model_specific_parameters(self): # Inputs @property def voltage_slope(self) -> float: + """Voltage slope.""" for t in self.blocks.index_set(): return self.blocks[t].voltage_slope.value @@ -232,13 +355,16 @@ def voltage_slope(self, voltage_slope: float): @property def voltage_intercept(self) -> float: + """Voltage intercept.""" for t in self.blocks.index_set(): return self.blocks[t].voltage_intercept.value @voltage_intercept.setter def voltage_intercept(self, voltage_intercept: float): for t in self.blocks.index_set(): - self.blocks[t].voltage_intercept = round(voltage_intercept, self.round_digits) + self.blocks[t].voltage_intercept = round( + voltage_intercept, self.round_digits + ) # # TODO: Add this if wanted # # self.alphaP = Param(None) # [kW_DC] Bi-directional intercept for charge @@ -248,6 +374,7 @@ def voltage_intercept(self, voltage_intercept: float): @property def average_current(self) -> float: + """Average current.""" for t in self.blocks.index_set(): return self.blocks[t].average_current.value @@ -258,64 +385,84 @@ def average_current(self, average_current: float): @property def internal_resistance(self) -> float: + """Internal resistance.""" for t in self.blocks.index_set(): return self.blocks[t].internal_resistance.value @internal_resistance.setter def internal_resistance(self, internal_resistance: float): for t in self.blocks.index_set(): - self.blocks[t].internal_resistance = round(internal_resistance, self.round_digits) + self.blocks[t].internal_resistance = round( + internal_resistance, self.round_digits + ) @property def minimum_charge_current(self) -> float: + """Minimum charge current.""" for t in self.blocks.index_set(): return self.blocks[t].minimum_charge_current.value @minimum_charge_current.setter def minimum_charge_current(self, minimum_charge_current: float): for t in self.blocks.index_set(): - self.blocks[t].minimum_charge_current = round(minimum_charge_current, self.round_digits) + self.blocks[t].minimum_charge_current = round( + minimum_charge_current, self.round_digits + ) @property def maximum_charge_current(self) -> float: + """Maximum charge current.""" for t in self.blocks.index_set(): return self.blocks[t].maximum_charge_current.value @maximum_charge_current.setter def maximum_charge_current(self, maximum_charge_current: float): for t in self.blocks.index_set(): - self.blocks[t].maximum_charge_current = round(maximum_charge_current, self.round_digits) + self.blocks[t].maximum_charge_current = round( + maximum_charge_current, self.round_digits + ) @property def minimum_discharge_current(self) -> float: + """Minimum discharge current.""" for t in self.blocks.index_set(): return self.blocks[t].minimum_discharge_current.value @minimum_discharge_current.setter def minimum_discharge_current(self, minimum_discharge_current: float): for t in self.blocks.index_set(): - self.blocks[t].minimum_discharge_current = round(minimum_discharge_current, self.round_digits) + self.blocks[t].minimum_discharge_current = round( + minimum_discharge_current, self.round_digits + ) @property def maximum_discharge_current(self) -> float: + """Maximum discharge current.""" for t in self.blocks.index_set(): return self.blocks[t].maximum_discharge_current.value @maximum_discharge_current.setter def maximum_discharge_current(self, maximum_discharge_current: float): for t in self.blocks.index_set(): - self.blocks[t].maximum_discharge_current = round(maximum_discharge_current, self.round_digits) + self.blocks[t].maximum_discharge_current = round( + maximum_discharge_current, self.round_digits + ) # Outputs @property def charge_current(self) -> list: + """Charge current.""" return [self.blocks[t].charge_current.value for t in self.blocks.index_set()] @property def discharge_current(self) -> list: + """Discharge current.""" return [self.blocks[t].discharge_current.value for t in self.blocks.index_set()] @property def current(self) -> list: - return [self.blocks[t].discharge_current.value - self.blocks[t].charge_current.value - for t in self.blocks.index_set()] + """Current.""" + return [ + self.blocks[t].discharge_current.value - self.blocks[t].charge_current.value + for t in self.blocks.index_set() + ] diff --git a/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py index 8bc097b46..9d15063ec 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py @@ -5,41 +5,69 @@ import PySAM.BatteryStateful as BatteryModel import PySAM.Singleowner as Singleowner -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import SimpleBatteryDispatchHeuristic +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import ( + SimpleBatteryDispatchHeuristic, +) class OneCycleBatteryDispatchHeuristic(SimpleBatteryDispatchHeuristic): """ - - fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) + One cycle per day heuristic battery dispatch. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo model instance. + index_set (pyomo.Set): Index set. + system_model (BatteryModel.BatteryStateful): Battery model instance. + financial_model (Singleowner.Singleowner): Single owner financial model instance. + block_set_name (str, optional): Name of the block set. Defaults to 'one_cycle_heuristic_battery'. + dispatch_options (dict, optional): Dispatch options. Defaults to None. + + Attributes: + prices (list): List of normalized prices [-1, 1] (Charging (-), Discharging (+)). """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model: BatteryModel.BatteryStateful, - financial_model: Singleowner.Singleowner, - block_set_name: str = 'one_cycle_heuristic_battery', - dispatch_options: dict = None): + + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: Singleowner.Singleowner, + block_set_name: str = "one_cycle_heuristic_battery", + dispatch_options: dict = None, + ): + """Initialize OneCycleBatteryDispatchHeuristic. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Indexed set. + system_model (BatteryModel.BatteryStateful): Battery system model. + financial_model (Singleowner.Singleowner): Financial model. + block_set_name (str, optional):Name of the block set. Defaults to 'one_cycle_heuristic_battery'. + dispatch_options (dict, optional): Dispatch options. Defaults to None. + """ if dispatch_options is None: dispatch_options = {} - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name, - dispatch_options=dispatch_options) + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + dispatch_options=dispatch_options, + ) self.prices = list([0.0] * len(self.blocks.index_set())) def _heuristic_method(self, gen): - """This sets battery dispatch using a 1 cycle per day assumption. + """ + Sets battery dispatch using a one cycle per day assumption. Method: - 1. Sort input prices - 2. Determine the duration required to fully discharge and charge the battery - 3. Set discharge and charge operations based on sorted prices - 3. Check SOC feasibility - 4. If infeasible, find infeasibility, shift operation to the next sorted price periods - 5. Repeat step 4 until SOC feasible + 1. Sort input prices + 2. Determine the duration required to fully discharge and charge the battery + 3. Set discharge and charge operations based on sorted prices + 3. Check SOC feasibility + 4. If infeasible, find infeasibility, shift operation to the next sorted price periods + 5. Repeat step 4 until SOC feasible NOTE: If operation is tried on half of time periods, then operation defaults to 'do nothing' """ if sum(self.prices) == 0.0 and max(self.prices) == 0.0: @@ -52,13 +80,13 @@ def _heuristic_method(self, gen): sorted_prices = sorted(sorted_prices, key=lambda i: i[1]) # Set initial fixed dispatch - fixed_dispatch, next_charge_idx = self._charge_battery(charge_time, 0, - sorted_prices, - fixed_dispatch) + fixed_dispatch, next_charge_idx = self._charge_battery( + charge_time, 0, sorted_prices, fixed_dispatch + ) - fixed_dispatch, next_discharge_idx = self._discharge_battery(discharge_time, 0, - sorted_prices, - fixed_dispatch) + fixed_dispatch, next_discharge_idx = self._discharge_battery( + discharge_time, 0, sorted_prices, fixed_dispatch + ) # test feasibility and find infeasibility feasible = self.test_soc_feasibility(fixed_dispatch) @@ -67,29 +95,41 @@ def _heuristic_method(self, gen): idx_infeasible = feasible[1] infeasible_value = fixed_dispatch[idx_infeasible] if infeasible_value > 0: # Discharging - discharge_remaining = fixed_dispatch[idx_infeasible] * self.time_duration[idx_infeasible] + discharge_remaining = ( + fixed_dispatch[idx_infeasible] * self.time_duration[idx_infeasible] + ) fixed_dispatch[idx_infeasible] = 0 - if next_discharge_idx < len(sorted_prices)/2: - fixed_dispatch, next_discharge_idx = self._discharge_battery(discharge_remaining, - next_discharge_idx, - sorted_prices, - fixed_dispatch) - elif infeasible_value < 0: # Charging - charge_remaining = -fixed_dispatch[idx_infeasible] * self.time_duration[idx_infeasible] + if next_discharge_idx < len(sorted_prices) / 2: + fixed_dispatch, next_discharge_idx = self._discharge_battery( + discharge_remaining, + next_discharge_idx, + sorted_prices, + fixed_dispatch, + ) + elif infeasible_value < 0: # Charging + charge_remaining = ( + -fixed_dispatch[idx_infeasible] * self.time_duration[idx_infeasible] + ) fixed_dispatch[idx_infeasible] = 0 - if next_charge_idx < len(sorted_prices)/2: # TODO: maybe too restrictive - fixed_dispatch, next_charge_idx = self._charge_battery(charge_remaining, - next_charge_idx, - sorted_prices, - fixed_dispatch) + if ( + next_charge_idx < len(sorted_prices) / 2 + ): # TODO: maybe too restrictive + fixed_dispatch, next_charge_idx = self._charge_battery( + charge_remaining, next_charge_idx, sorted_prices, fixed_dispatch + ) feasible = self.test_soc_feasibility(fixed_dispatch) self._fixed_dispatch = fixed_dispatch - def _discharge_battery(self, discharge_remaining, next_discharge_idx, sorted_prices, fixed_dispatch): - """Discharge battery using the remaining discharge and the next best discharge period. + def _discharge_battery( + self, discharge_remaining, next_discharge_idx, sorted_prices, fixed_dispatch + ): + """ + Discharges battery using the remaining discharge and the next best discharge period. - Returns adjusted fixed_dispatch and next discharge index to be tried.""" + Returns: + Tuple[list, int]: Adjusted fixed dispatch and next discharge index to be tried. + """ period_count = next_discharge_idx while discharge_remaining > 0: if period_count < len(sorted_prices): @@ -106,18 +146,23 @@ def _discharge_battery(self, discharge_remaining, next_discharge_idx, sorted_pri next_discharge_idx = period_count return fixed_dispatch, next_discharge_idx - def _charge_battery(self, charge_remaining, next_charge_idx, sorted_prices, fixed_dispatch): - """Charge battery using the remaining charge and the next best charge period. + def _charge_battery( + self, charge_remaining, next_charge_idx, sorted_prices, fixed_dispatch + ): + """ + Charges battery using the remaining charge and the next best charge period. - Returns adjusted fixed_dispatch and next charge index to be tried.""" + Returns: + Tuple[list, int]: Adjusted fixed dispatch and next charge index to be tried. + """ period_count = next_charge_idx while charge_remaining > 0: if period_count < len(sorted_prices): idx = sorted_prices[period_count][0] if self.max_charge_fraction[idx] < charge_remaining: - fixed_dispatch[idx] = - self.max_charge_fraction[idx] + fixed_dispatch[idx] = -self.max_charge_fraction[idx] else: - fixed_dispatch[idx] = - charge_remaining + fixed_dispatch[idx] = -charge_remaining # update count and remaining discharge charge_remaining += fixed_dispatch[idx] * self.time_duration[idx] period_count += 1 @@ -127,28 +172,46 @@ def _charge_battery(self, charge_remaining, next_charge_idx, sorted_prices, fixe return fixed_dispatch, next_charge_idx def _get_duration_battery_full_cycle(self) -> Tuple[float, float]: - """ Calculates discharge and charge hours required to fully cycle the battery.""" + """ + Calculates discharge and charge hours required to fully cycle the battery. + + Returns: + Tuple[float, float]: Discharge and charge hours. + """ true_capacity = (self.maximum_soc - self.minimum_soc) * self.capacity / 100.0 - n_discharge = true_capacity / (1/(self.discharge_efficiency/100.) * self.maximum_power) - n_charge = true_capacity / (self.charge_efficiency / 100. * self.maximum_power) + n_discharge = true_capacity / ( + 1 / (self.discharge_efficiency / 100.0) * self.maximum_power + ) + n_charge = true_capacity / (self.charge_efficiency / 100.0 * self.maximum_power) return n_discharge, n_charge def test_soc_feasibility(self, fixed_dispatch) -> Tuple[bool, int]: - """Steps through fixed_dispatch and test SOC feasibility. + """ + Steps through fixed_dispatch and tests SOC feasibility. - If fixed_dispatch is infeasible, return index of first infeasibility operation. + Returns: + Tuple[bool, int]: Tuple indicating SOC feasibility and index of first infeasible operation. """ soc0 = self.model.initial_soc.value for idx, fd in enumerate(fixed_dispatch): soc = self.update_soc(fd, soc0) - if round(soc, 6)*100. < self.minimum_soc or round(soc, 6)*100. > self.maximum_soc: + if ( + round(soc, 6) * 100.0 < self.minimum_soc + or round(soc, 6) * 100.0 > self.maximum_soc + ): return False, idx soc0 = soc return True, None @property def prices(self) -> list: + """ + List of normalized prices [-1, 1] (Charging (-), Discharging (+)). + + Returns: + list: Prices. + """ return self._prices @prices.setter diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 5bf9496da..73918b09c 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -8,7 +8,18 @@ class PowerStorageDispatch(Dispatch): """ - + Dispatch algorithm for power storage. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo model instance. + index_set (pyomo.Set): Index set. + system_model: System model. + financial_model: Financial model. + block_set_name (str): Name of the block set. + dispatch_options: Dispatch options. + + Attributes: + options (object): Dispatch options. """ def __init__( @@ -20,6 +31,16 @@ def __init__( block_set_name: str, dispatch_options, ): + """Intialize PowerStorageDispatch. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Indexed set. + system_model: System model. + financial_model: Financial model. + block_set_name (str, optional): Name of the block set. + dispatch_options (dict, optional): Dispatch options. + """ super().__init__( pyomo_model, @@ -38,8 +59,12 @@ def __init__( self._create_lifecycle_count_constraint() def dispatch_block_rule(self, storage): - """ - Called during Dispatch's __init__ + """Initializes storage parameters, variables, and constraints. + Called during Dispatch's __init__. + + + Args: + storage: Storage instance. """ # Parameters self._create_storage_parameters(storage) @@ -54,25 +79,38 @@ def dispatch_block_rule(self, storage): self._create_storage_port(storage) def max_gross_profit_objective(self, hybrid_blocks): + """Sets the max gross profit objective for the dispatch. + + Args: + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ def battery_profit_objective_rule(m): - objective = 0 - objective += sum( - - (1/hybrid_blocks[t].time_weighting_factor) - * self.blocks[t].time_duration - * ( - self.blocks[t].cost_per_charge - * hybrid_blocks[t].battery_charge - + self.blocks[t].cost_per_discharge - * hybrid_blocks[t].battery_discharge - ) - for t in hybrid_blocks.index_set() + objective = 0 + objective += sum( + - (1/hybrid_blocks[t].time_weighting_factor) + * self.blocks[t].time_duration + * ( + self.blocks[t].cost_per_charge + * hybrid_blocks[t].battery_charge + + self.blocks[t].cost_per_discharge + * hybrid_blocks[t].battery_discharge ) - if self.options.include_lifecycle_count: - objective -= self.model.lifecycle_cost * sum(self.model.lifecycles) - return objective + for t in hybrid_blocks.index_set() + ) + if self.options.include_lifecycle_count: + objective -= self.model.lifecycle_cost * sum(self.model.lifecycles) + return objective + self.obj = pyomo.Expression(rule=battery_profit_objective_rule) def min_operating_cost_objective(self, hybrid_blocks): + """Sets the min operating cost objective for the dispatch. + + Args: + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + models by adding modeling components as attributes. + """ objective = sum( hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration @@ -90,6 +128,15 @@ def min_operating_cost_objective(self, hybrid_blocks): self.obj = objective def _create_variables(self, hybrid): + """ + Creates storage variables. + + Args: + hybrid: Hybrid instance. + + Returns: + Tuple: Tuple containing battery discharge and charge variables. + """ hybrid.battery_charge = pyomo.Var( doc="Power charging the electric battery [MW]", domain=pyomo.NonNegativeReals, @@ -105,15 +152,29 @@ def _create_variables(self, hybrid): return hybrid.battery_discharge, hybrid.battery_charge def _create_port(self, hybrid): + """ + Creates storage port. + + Args: + hybrid: Hybrid instance. + + Returns: + Port: Storage port. + """ hybrid.battery_port = Port( initialize={ - 'charge_power': hybrid.battery_charge, - 'discharge_power': hybrid.battery_discharge, + "charge_power": hybrid.battery_charge, + "discharge_power": hybrid.battery_discharge, } ) return hybrid.battery_port def _create_storage_parameters(self, storage): + """Creates storage parameters. + + Args: + storage: Storage instance. + """ ################################## # Parameters # ################################## @@ -122,94 +183,126 @@ def _create_storage_parameters(self, storage): default=1.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.hr) + units=u.hr, + ) storage.cost_per_charge = pyomo.Param( doc="Operating cost of " + self.block_set_name + " charging [$/MWh]", - default=0., + default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.MWh) + units=u.USD / u.MWh, + ) storage.cost_per_discharge = pyomo.Param( doc="Operating cost of " + self.block_set_name + " discharging [$/MWh]", - default=0., + default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.MWh) + units=u.USD / u.MWh, + ) storage.minimum_power = pyomo.Param( doc=self.block_set_name + " minimum power rating [MW]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) storage.maximum_power = pyomo.Param( doc=self.block_set_name + " maximum power rating [MW]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) storage.minimum_soc = pyomo.Param( doc=self.block_set_name + " minimum state-of-charge [-]", default=0.1, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) storage.maximum_soc = pyomo.Param( doc=self.block_set_name + " maximum state-of-charge [-]", default=0.9, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) def _create_efficiency_parameters(self, storage): + """Creates storage efficiency parameters. + + Args: + storage: Storage instance. + """ storage.charge_efficiency = pyomo.Param( doc=self.block_set_name + " Charging efficiency [-]", default=0.938, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) storage.discharge_efficiency = pyomo.Param( doc=self.block_set_name + " discharging efficiency [-]", default=0.938, within=pyomo.PercentFraction, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) def _create_capacity_parameter(self, storage): + """Creates storage capacity parameter. + + Args: + storage: Storage instance. + """ storage.capacity = pyomo.Param( doc=self.block_set_name + " capacity [MWh]", within=pyomo.NonNegativeReals, mutable=True, - units=u.MWh) + units=u.MWh, + ) def _create_storage_variables(self, storage): + """Creates storage variables. + + Args: + storage: Storage instance. + """ ################################## # Variables # ################################## storage.is_charging = pyomo.Var( doc="1 if " + self.block_set_name + " is charging; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) storage.is_discharging = pyomo.Var( doc="1 if " + self.block_set_name + " is discharging; 0 Otherwise [-]", domain=pyomo.Binary, - units=u.dimensionless) + units=u.dimensionless, + ) storage.soc0 = pyomo.Var( - doc=self.block_set_name + " initial state-of-charge at beginning of period[-]", + doc=self.block_set_name + + " initial state-of-charge at beginning of period[-]", domain=pyomo.PercentFraction, bounds=(storage.minimum_soc, storage.maximum_soc), - units=u.dimensionless) + units=u.dimensionless, + ) storage.soc = pyomo.Var( doc=self.block_set_name + " state-of-charge at end of period [-]", domain=pyomo.PercentFraction, bounds=(storage.minimum_soc, storage.maximum_soc), - units=u.dimensionless) + units=u.dimensionless, + ) storage.charge_power = pyomo.Var( doc="Power into " + self.block_set_name + " [MW]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) storage.discharge_power = pyomo.Var( doc="Power out of " + self.block_set_name + " [MW]", domain=pyomo.NonNegativeReals, - units=u.MW) + units=u.MW, + ) def _create_storage_constraints(self, storage): ################################## @@ -218,41 +311,61 @@ def _create_storage_constraints(self, storage): # Charge power bounds storage.charge_power_ub = pyomo.Constraint( doc=self.block_set_name + " charging power upper bound", - expr=storage.charge_power <= storage.maximum_power * storage.is_charging) + expr=storage.charge_power <= storage.maximum_power * storage.is_charging, + ) storage.charge_power_lb = pyomo.Constraint( doc=self.block_set_name + " charging power lower bound", - expr=storage.charge_power >= storage.minimum_power * storage.is_charging) + expr=storage.charge_power >= storage.minimum_power * storage.is_charging, + ) # Discharge power bounds storage.discharge_power_lb = pyomo.Constraint( doc=self.block_set_name + " Discharging power lower bound", - expr=storage.discharge_power >= storage.minimum_power * storage.is_discharging) + expr=storage.discharge_power + >= storage.minimum_power * storage.is_discharging, + ) storage.discharge_power_ub = pyomo.Constraint( doc=self.block_set_name + " Discharging power upper bound", - expr=storage.discharge_power <= storage.maximum_power * storage.is_discharging) + expr=storage.discharge_power + <= storage.maximum_power * storage.is_discharging, + ) # Storage packing constraint storage.charge_discharge_packing = pyomo.Constraint( - doc=self.block_set_name + " packing constraint for charging and discharging binaries", - expr=storage.is_charging + storage.is_discharging <= 1) + doc=self.block_set_name + + " packing constraint for charging and discharging binaries", + expr=storage.is_charging + storage.is_discharging <= 1, + ) def _create_soc_inventory_constraint(self, storage): + """Creates state-of-charge inventory constraint for storage. + + Args: + storage: Storage instance. + """ + def soc_inventory_rule(m): return m.soc == ( m.soc0 + m.time_duration * ( - m.charge_efficiency - * m.charge_power - - (1 / m.discharge_efficiency) - * m.discharge_power - ) / m.capacity + m.charge_efficiency * m.charge_power + - (1 / m.discharge_efficiency) * m.discharge_power + ) + / m.capacity ) + # Storage State-of-charge balance storage.soc_inventory = pyomo.Constraint( doc=self.block_set_name + " state-of-charge inventory balance", - rule=soc_inventory_rule) + rule=soc_inventory_rule, + ) @staticmethod def _create_storage_port(storage): + """Creates storage port. + + Args: + storage: Storage instance. + """ ################################## # Ports # ################################## @@ -261,15 +374,18 @@ def _create_storage_port(storage): storage.port.add(storage.discharge_power) def _create_soc_linking_constraint(self): + """Creates state-of-charge linking constraint.""" ################################## # Parameters # ################################## self.model.initial_soc = pyomo.Param( - doc=self.block_set_name + " initial state-of-charge at beginning of the horizon[-]", + doc=self.block_set_name + + " initial state-of-charge at beginning of the horizon[-]", within=pyomo.PercentFraction, default=0.5, mutable=True, - units=u.dimensionless) + units=u.dimensionless, + ) ################################## # Constraints # ################################## @@ -279,31 +395,49 @@ def storage_soc_linking_rule(m, t): if t == self.blocks.index_set().first(): return self.blocks[t].soc0 == self.model.initial_soc return self.blocks[t].soc0 == self.blocks[t - 1].soc + self.model.soc_linking = pyomo.Constraint( self.blocks.index_set(), doc=self.block_set_name + " state-of-charge block linking constraint", - rule=storage_soc_linking_rule) + rule=storage_soc_linking_rule, + ) def _lifecycle_count_rule(self, m, i): + """Calculates lifecycle count rule. + + Args: + m: Model instance. + i: Index. + + Returns: + float: Lifecycle count. + """ # Use full-energy cycles start = int(i * self.timesteps_per_day) end = int((i + 1) * self.timesteps_per_day) - return m.lifecycles[i] == sum(self.blocks[t].time_duration - * self.blocks[t].discharge_power - / self.blocks[t].capacity for t in range(start, end)) + return m.lifecycles[i] == sum( + self.blocks[t].time_duration + * self.blocks[t].discharge_power + / self.blocks[t].capacity + for t in range(start, end) + ) def _create_lifecycle_model(self): + """Creates lifecycle model.""" ################################## # Parameters # ################################## self.timesteps_per_day = 24 / pyomo.value(self.blocks[0].time_duration) - self.model.days = pyomo.RangeSet(0, int(len(self.blocks)) / self.timesteps_per_day - 1) + self.model.days = pyomo.RangeSet( + 0, int(len(self.blocks)) / self.timesteps_per_day - 1 + ) self.model.lifecycle_cost = pyomo.Param( doc="Lifecycle cost of " + self.block_set_name + " [$/lifecycle]", default=0.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD / u.lifecycle) + units=u.USD / u.lifecycle, + ) ################################## # Variables # ################################## @@ -311,14 +445,15 @@ def _create_lifecycle_model(self): self.model.days, doc=self.block_set_name + " lifecycle count", domain=pyomo.NonNegativeReals, - units=u.lifecycle) + units=u.lifecycle, + ) ################################## # Constraints # ################################## self.model.lifecycle_count = pyomo.Constraint( self.model.days, doc=self.block_set_name + " lifecycle counting", - rule=self._lifecycle_count_rule + rule=self._lifecycle_count_rule, ) ################################## # Ports # @@ -327,32 +462,41 @@ def _create_lifecycle_model(self): self.model.lifecycles_port.add(self.model.lifecycles) self.model.lifecycles_port.add(self.model.lifecycle_cost) - def _create_lifecycle_count_constraint(self): + """Creates lifecycle count constraint.""" self.model.max_cycles_per_day = pyomo.Param( doc="Max number of full energy cycles per day for " + self.block_set_name, default=self.options.max_lifecycle_per_day, within=pyomo.NonNegativeReals, mutable=True, - units=u.lifecycle) - + units=u.lifecycle, + ) + self.model.lifecycle_count_constraint = pyomo.Constraint( - self.model.days, - rule=lambda m, i: m.lifecycles[i] <= m.max_cycles_per_day + self.model.days, rule=lambda m, i: m.lifecycles[i] <= m.max_cycles_per_day ) def _check_initial_soc(self, initial_soc): + """Checks initial state-of-charge. + + Args: + initial_soc: Initial state-of-charge value. + + Returns: + float: Checked initial state-of-charge. + """ if initial_soc > 1: - initial_soc /= 100. + initial_soc /= 100.0 initial_soc = round(initial_soc, self.round_digits) - if initial_soc > self.maximum_soc/100: + if initial_soc > self.maximum_soc / 100: print( "Warning: Storage dispatch was initialized with a state-of-charge greater than " - "maximum value!") + "maximum value!" + ) print("Initial SOC = {}".format(initial_soc)) print("Initial SOC was set to maximum value.") initial_soc = self.maximum_soc / 100 - elif initial_soc < self.minimum_soc/100: + elif initial_soc < self.minimum_soc / 100: print( "Warning: Storage dispatch was initialized with a state-of-charge less than " "minimum value!" @@ -363,11 +507,14 @@ def _check_initial_soc(self, initial_soc): return initial_soc def update_dispatch_initial_soc(self, initial_soc: float = None): - raise NotImplemented("This function must be overridden for specific storage dispatch model") + raise NotImplemented( + "This function must be overridden for specific storage dispatch model" + ) # INPUTS @property def time_duration(self) -> list: + """Time duration.""" return [self.blocks[t].time_duration.value for t in self.blocks.index_set()] @time_duration.setter @@ -377,11 +524,13 @@ def time_duration(self, time_duration: list): self.blocks[t].time_duration = round(delta, self.round_digits) else: raise ValueError( - self.time_duration.__name__ + " list must be the same length as time horizon" + self.time_duration.__name__ + + " list must be the same length as time horizon" ) @property def cost_per_charge(self) -> float: + """Cost per charge.""" for t in self.blocks.index_set(): return self.blocks[t].cost_per_charge.value @@ -392,16 +541,20 @@ def cost_per_charge(self, om_dollar_per_mwh: float): @property def cost_per_discharge(self) -> float: + """Cost per discharge.""" for t in self.blocks.index_set(): return self.blocks[t].cost_per_discharge.value @cost_per_discharge.setter def cost_per_discharge(self, om_dollar_per_mwh: float): for t in self.blocks.index_set(): - self.blocks[t].cost_per_discharge = round(om_dollar_per_mwh, self.round_digits) + self.blocks[t].cost_per_discharge = round( + om_dollar_per_mwh, self.round_digits + ) @property def minimum_power(self) -> float: + """Minimum power.""" for t in self.blocks.index_set(): return self.blocks[t].minimum_power.value @@ -412,6 +565,7 @@ def minimum_power(self, minimum_power_mw: float): @property def maximum_power(self) -> float: + """Maximum power.""" for t in self.blocks.index_set(): return self.blocks[t].maximum_power.value @@ -422,32 +576,35 @@ def maximum_power(self, maximum_power_mw: float): @property def minimum_soc(self) -> float: + """Minimum state-of-charge.""" for t in self.blocks.index_set(): - return self.blocks[t].minimum_soc.value * 100. + return self.blocks[t].minimum_soc.value * 100.0 @minimum_soc.setter def minimum_soc(self, minimum_soc: float): if minimum_soc > 1: - minimum_soc /= 100. + minimum_soc /= 100.0 for t in self.blocks.index_set(): self.blocks[t].minimum_soc = round(minimum_soc, self.round_digits) @property def maximum_soc(self) -> float: + """Maximum state-of-charge.""" for t in self.blocks.index_set(): - return self.blocks[t].maximum_soc.value * 100. + return self.blocks[t].maximum_soc.value * 100.0 @maximum_soc.setter def maximum_soc(self, maximum_soc: float): if maximum_soc > 1: - maximum_soc /= 100. + maximum_soc /= 100.0 for t in self.blocks.index_set(): self.blocks[t].maximum_soc = round(maximum_soc, self.round_digits) @property def charge_efficiency(self) -> float: + """Charge efficiency.""" for t in self.blocks.index_set(): - return self.blocks[t].charge_efficiency.value * 100. + return self.blocks[t].charge_efficiency.value * 100.0 @charge_efficiency.setter def charge_efficiency(self, efficiency: float): @@ -457,8 +614,9 @@ def charge_efficiency(self, efficiency: float): @property def discharge_efficiency(self) -> float: + """Discharge efficiency.""" for t in self.blocks.index_set(): - return self.blocks[t].discharge_efficiency.value * 100. + return self.blocks[t].discharge_efficiency.value * 100.0 @discharge_efficiency.setter def discharge_efficiency(self, efficiency: float): @@ -468,18 +626,20 @@ def discharge_efficiency(self, efficiency: float): @property def round_trip_efficiency(self) -> float: - return self.charge_efficiency * self.discharge_efficiency / 100. + """Round trip efficiency.""" + return self.charge_efficiency * self.discharge_efficiency / 100.0 @round_trip_efficiency.setter def round_trip_efficiency(self, round_trip_efficiency: float): round_trip_efficiency = self._check_efficiency_value(round_trip_efficiency) # Assumes equal charge and discharge efficiencies - efficiency = round_trip_efficiency ** (1 / 2) + efficiency = round_trip_efficiency ** (1 / 2) self.charge_efficiency = efficiency self.discharge_efficiency = efficiency @property def capacity(self) -> float: + """Capacity.""" for t in self.blocks.index_set(): return self.blocks[t].capacity.value @@ -490,7 +650,8 @@ def capacity(self, capacity_mwh: float): @property def initial_soc(self) -> float: - return self.model.initial_soc.value * 100. + """Initial state-of-charge.""" + return self.model.initial_soc.value * 100.0 @initial_soc.setter def initial_soc(self, initial_soc: float): @@ -499,6 +660,7 @@ def initial_soc(self, initial_soc: float): @property def lifecycle_cost(self) -> float: + """Lifecycle cost.""" return self.model.lifecycle_cost.value @lifecycle_cost.setter @@ -507,38 +669,45 @@ def lifecycle_cost(self, lifecycle_cost: float): @property def lifecycle_cost_per_kWh_cycle(self) -> float: + """Lifecycle cost per kWh cycle.""" return self.options.lifecycle_cost_per_kWh_cycle @lifecycle_cost_per_kWh_cycle.setter def lifecycle_cost_per_kWh_cycle(self, lifecycle_cost_per_kWh_cycle: float): self.options.lifecycle_cost_per_kWh_cycle = lifecycle_cost_per_kWh_cycle self.model.lifecycle_cost = ( - lifecycle_cost_per_kWh_cycle * self._system_model.value('nominal_energy') + lifecycle_cost_per_kWh_cycle * self._system_model.value("nominal_energy") ) # Outputs @property def is_charging(self) -> list: + """Storage is charging.""" return [self.blocks[t].is_charging.value for t in self.blocks.index_set()] @property def is_discharging(self) -> list: + """Storage is discharging.""" return [self.blocks[t].is_discharging.value for t in self.blocks.index_set()] @property def soc(self) -> list: + """State-of-charge.""" return [self.blocks[t].soc.value * 100.0 for t in self.blocks.index_set()] @property def charge_power(self) -> list: + """Charge power.""" return [self.blocks[t].charge_power.value for t in self.blocks.index_set()] @property def discharge_power(self) -> list: + """Discharge power.""" return [self.blocks[t].discharge_power.value for t in self.blocks.index_set()] @property def lifecycles(self) -> float: + """Lifecycles.""" if self.options.include_lifecycle_count: return [pyomo.value(i) for _, i in self.model.lifecycles.items()] else: @@ -546,13 +715,18 @@ def lifecycles(self) -> float: @property def power(self) -> list: - return [self.blocks[t].discharge_power.value - self.blocks[t].charge_power.value - for t in self.blocks.index_set()] + """Power.""" + return [ + self.blocks[t].discharge_power.value - self.blocks[t].charge_power.value + for t in self.blocks.index_set() + ] @property def current(self) -> list: + """Current.""" return [0.0 for t in self.blocks.index_set()] @property def generation(self) -> list: + """Generation.""" return self.power diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index 8c5760693..7d90a8c52 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -6,38 +6,67 @@ import PySAM.BatteryStateful as BatteryModel import PySAM.Singleowner as Singleowner -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import SimpleBatteryDispatch +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import ( + SimpleBatteryDispatch, +) class SimpleBatteryDispatchHeuristic(SimpleBatteryDispatch): """Fixes battery dispatch operations based on user input. - Currently, enforces available generation and grid limit assuming no battery charging from grid + Currently, enforces available generation and grid limit assuming no battery charging from grid. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo model instance. + index_set (pyomo.Set): Index set. + system_model (BatteryModel.BatteryStateful): BatteryStateful model instance. + financial_model (Singleowner.Singleowner): Singleowner model instance. + fixed_dispatch (Optional[List]): List of normalized values [-1, 1] (Charging (-), Discharging (+)). Defaults to None. + block_set_name (str): Name of the block set. Defaults to 'heuristic_battery'. + dispatch_options (Optional[Dict]): Dispatch options. Defaults to None. + + Attributes: + max_charge_fraction (List[float]): List of maximum charge fractions for each time period. + max_discharge_fraction (List[float]): List of maximum discharge fractions for each time period. + user_fixed_dispatch (List[float]): List of user-defined fixed dispatch values for each time period. + _fixed_dispatch (List[float]): List of fixed dispatch values based on heuristic method for each time period. """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model: BatteryModel.BatteryStateful, - financial_model: Singleowner.Singleowner, - fixed_dispatch: Optional[List] = None, - block_set_name: str = 'heuristic_battery', - dispatch_options: Optional[Dict] = None): - """ - :param fixed_dispatch: list of normalized values [-1, 1] (Charging (-), Discharging (+)) + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model: BatteryModel.BatteryStateful, + financial_model: Singleowner.Singleowner, + fixed_dispatch: Optional[List] = None, + block_set_name: str = "heuristic_battery", + dispatch_options: Optional[Dict] = None, + ): + """Initialize SimpleBatteryDispatchHeuristic. + + Args: + pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. + index_set (pyomo.Set): Indexed set. + system_model (BatteryModel.BatteryStateful): Battery system model. + financial_model (Singleowner.Singleowner): Financial model. + fixed_dispatch (Optional[List], optional): List of normalized values [-1, 1] (Charging (-), Discharging (+)). Defaults to None. + block_set_name (str, optional): Name of block set. Defaults to 'heuristic_battery'. + dispatch_options (dict, optional): Dispatch options. Defaults to None. """ if dispatch_options is None: dispatch_options = {} - super().__init__(pyomo_model, - index_set, - system_model, - financial_model, - block_set_name=block_set_name, - dispatch_options=dispatch_options) - - self.max_charge_fraction = list([0.0]*len(self.blocks.index_set())) - self.max_discharge_fraction = list([0.0]*len(self.blocks.index_set())) - self.user_fixed_dispatch = list([0.0]*len(self.blocks.index_set())) + super().__init__( + pyomo_model, + index_set, + system_model, + financial_model, + block_set_name=block_set_name, + dispatch_options=dispatch_options, + ) + + self.max_charge_fraction = list([0.0] * len(self.blocks.index_set())) + self.max_discharge_fraction = list([0.0] * len(self.blocks.index_set())) + self.user_fixed_dispatch = list([0.0] * len(self.blocks.index_set())) # TODO: should I enforce either a day schedule or a year schedule year and save it as user input. # Additionally, Should I drop it as input in the init function? if fixed_dispatch is not None: @@ -49,6 +78,13 @@ def set_fixed_dispatch(self, gen: list, grid_limit: list): """Sets charge and discharge power of battery dispatch using fixed_dispatch attribute and enforces available generation and grid limits. + Args: + gen (list): Generation blocks. + grid_limit (list): Grid capacity. + + Raises: + ValueError: If gen or grid_limit length does not match fixed_dispatch length. + """ self.check_gen_grid_limit(gen, grid_limit) self._set_power_fraction_limits(gen, grid_limit) @@ -56,6 +92,16 @@ def set_fixed_dispatch(self, gen: list, grid_limit: list): self._fix_dispatch_model_variables() def check_gen_grid_limit(self, gen: list, grid_limit: list): + """Checks if generation and grid limit lengths match fixed_dispatch length. + + Args: + gen (list): Generation blocks. + grid_limit (list): Grid capacity. + + Raises: + ValueError: If gen or grid_limit length does not match fixed_dispatch length. + + """ if len(gen) != len(self.fixed_dispatch): raise ValueError("gen must be the same length as fixed_dispatch.") elif len(grid_limit) != len(self.fixed_dispatch): @@ -67,26 +113,31 @@ def _set_power_fraction_limits(self, gen: list, grid_limit: list): available generation and grid capacity, respectively. Args: - gen: generation Blocks - grid_limit: grid capacity + gen (list): Generation blocks. + grid_limit (list): Grid capacity. NOTE: This method assumes that battery cannot be charged by the grid. + """ for t in self.blocks.index_set(): - self.max_charge_fraction[t] = self.enforce_power_fraction_simple_bounds(gen[t] / self.maximum_power) - self.max_discharge_fraction[t] = self.enforce_power_fraction_simple_bounds((grid_limit[t] - gen[t]) - / self.maximum_power) + self.max_charge_fraction[t] = self.enforce_power_fraction_simple_bounds( + gen[t] / self.maximum_power + ) + self.max_discharge_fraction[t] = self.enforce_power_fraction_simple_bounds( + (grid_limit[t] - gen[t]) / self.maximum_power + ) @staticmethod def enforce_power_fraction_simple_bounds(power_fraction: float) -> float: """ Enforces simple bounds (0, .9) for battery power fractions. - + Args: - power_fraction: power fraction from heuristic method + power_fraction (float): Power fraction from heuristic method. Returns: - bounded power fraction + power_fraction (float): Bounded power fraction. + """ if power_fraction > 0.9: power_fraction = 0.9 @@ -97,21 +148,32 @@ def enforce_power_fraction_simple_bounds(power_fraction: float) -> float: def update_soc(self, power_fraction: float, soc0: float) -> float: """ Updates SOC based on power fraction threshold (0.1). - + Args: - power_fraction: power fraction from heuristic method. Below threshold - is charging, above is discharging - soc0: initial SOC - + power_fraction (float): Power fraction from heuristic method. Below threshold + is charging, above is discharging. + soc0 (float): Initial SOC. + Returns: - Updated SOC. + soc (float): Updated SOC. + """ if power_fraction > 0.0: discharge_power = power_fraction * self.maximum_power - soc = soc0 - self.time_duration[0] * (1/(self.discharge_efficiency/100.) * discharge_power) / self.capacity + soc = ( + soc0 + - self.time_duration[0] + * (1 / (self.discharge_efficiency / 100.0) * discharge_power) + / self.capacity + ) elif power_fraction < 0.0: charge_power = -power_fraction * self.maximum_power - soc = soc0 + self.time_duration[0] * (self.charge_efficiency / 100. * charge_power) / self.capacity + soc = ( + soc0 + + self.time_duration[0] + * (self.charge_efficiency / 100.0 * charge_power) + / self.capacity + ) else: soc = soc0 @@ -123,22 +185,23 @@ def update_soc(self, power_fraction: float, soc0: float) -> float: return soc def _heuristic_method(self, _): - """Does specific heuristic method to fix battery dispatch.""" + """Executes specific heuristic method to fix battery dispatch.""" self._enforce_power_fraction_limits() def _enforce_power_fraction_limits(self): - """ Enforces battery power fraction limits and sets _fixed_dispatch attribute""" + """Enforces battery power fraction limits and sets _fixed_dispatch attribute.""" for t in self.blocks.index_set(): fd = self.user_fixed_dispatch[t] - if fd > 0.0: # Discharging + if fd > 0.0: # Discharging if fd > self.max_discharge_fraction[t]: fd = self.max_discharge_fraction[t] elif fd < 0.0: # Charging - if - fd > self.max_charge_fraction[t]: - fd = - self.max_charge_fraction[t] + if -fd > self.max_charge_fraction[t]: + fd = -self.max_charge_fraction[t] self._fixed_dispatch[t] = fd def _fix_dispatch_model_variables(self): + """Fixes dispatch model variables based on the fixed dispatch values.""" soc0 = self.model.initial_soc.value for t in self.blocks.index_set(): dispatch_factor = self._fixed_dispatch[t] @@ -156,23 +219,42 @@ def _fix_dispatch_model_variables(self): elif dispatch_factor < 0.0: # Charging self.blocks[t].discharge_power.fix(0.0) - self.blocks[t].charge_power.fix(- dispatch_factor * self.maximum_power) + self.blocks[t].charge_power.fix(-dispatch_factor * self.maximum_power) @property def fixed_dispatch(self) -> list: + """ + list: List of fixed dispatch. + """ return self._fixed_dispatch @property def user_fixed_dispatch(self) -> list: + """ + list: List of user fixed dispatch. + """ return self._user_fixed_dispatch @user_fixed_dispatch.setter def user_fixed_dispatch(self, fixed_dispatch: list): + """ + Setter for user fixed dispatch. + + Args: + fixed_dispatch (list): List of user fixed dispatch. + + Raises: + ValueError: If fixed_dispatch length does not match dispatch index set or if values are not between -1 and 1. + + """ # TODO: Annual dispatch array... if len(fixed_dispatch) != len(self.blocks.index_set()): - raise ValueError("fixed_dispatch must be the same length as dispatch index set.") + raise ValueError( + "fixed_dispatch must be the same length as dispatch index set." + ) elif max(fixed_dispatch) > 1.0 or min(fixed_dispatch) < -1.0: - raise ValueError("fixed_dispatch must be normalized values between -1 and 1.") + raise ValueError( + "fixed_dispatch must be normalized values between -1 and 1." + ) else: self._user_fixed_dispatch = fixed_dispatch - From 6066982043f425ebc2081eb2df10db2087dfc3bb Mon Sep 17 00:00:00 2001 From: kbrunik Date: Thu, 18 Apr 2024 08:52:52 -0700 Subject: [PATCH 24/27] reformatting --- .../technologies/dispatch/__init__.py | 28 +- .../technologies/dispatch/dispatch.py | 34 +- .../dispatch/dispatch_problem_state.py | 40 +- .../technologies/dispatch/grid_dispatch.py | 140 ++-- .../technologies/dispatch/hybrid_dispatch.py | 89 ++- .../hybrid_dispatch_builder_solver.py | 645 ++++++++++++------ .../dispatch/hybrid_dispatch_options.py | 68 +- .../dispatch/power_sources/__init__.py | 12 +- .../dispatch/power_sources/pv_dispatch.py | 4 +- .../dispatch/power_sources/tower_dispatch.py | 4 +- .../dispatch/power_sources/trough_dispatch.py | 4 +- .../dispatch/power_storage/__init__.py | 28 +- .../power_storage/power_storage_dispatch.py | 16 +- 13 files changed, 714 insertions(+), 398 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/__init__.py b/hopp/simulation/technologies/dispatch/__init__.py index 4bd6aace3..1a2b6c417 100644 --- a/hopp/simulation/technologies/dispatch/__init__.py +++ b/hopp/simulation/technologies/dispatch/__init__.py @@ -1,12 +1,26 @@ from hopp.simulation.technologies.dispatch.power_sources.pv_dispatch import PvDispatch -from hopp.simulation.technologies.dispatch.power_sources.wind_dispatch import WindDispatch +from hopp.simulation.technologies.dispatch.power_sources.wind_dispatch import ( + WindDispatch, +) from hopp.simulation.technologies.dispatch.power_sources.csp_dispatch import CspDispatch -from hopp.simulation.technologies.dispatch.power_sources.trough_dispatch import TroughDispatch -from hopp.simulation.technologies.dispatch.power_sources.tower_dispatch import TowerDispatch -from hopp.simulation.technologies.dispatch.power_sources.wave_dispatch import WaveDispatch +from hopp.simulation.technologies.dispatch.power_sources.trough_dispatch import ( + TroughDispatch, +) +from hopp.simulation.technologies.dispatch.power_sources.tower_dispatch import ( + TowerDispatch, +) +from hopp.simulation.technologies.dispatch.power_sources.wave_dispatch import ( + WaveDispatch, +) from hopp.simulation.technologies.dispatch.grid_dispatch import GridDispatch -from hopp.simulation.technologies.dispatch.hybrid_dispatch_options import HybridDispatchOptions +from hopp.simulation.technologies.dispatch.hybrid_dispatch_options import ( + HybridDispatchOptions, +) from hopp.simulation.technologies.dispatch.hybrid_dispatch import HybridDispatch -from hopp.simulation.technologies.dispatch.dispatch_problem_state import DispatchProblemState -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import SimpleBatteryDispatch +from hopp.simulation.technologies.dispatch.dispatch_problem_state import ( + DispatchProblemState, +) +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import ( + SimpleBatteryDispatch, +) diff --git a/hopp/simulation/technologies/dispatch/dispatch.py b/hopp/simulation/technologies/dispatch/dispatch.py index 48791afc6..c2f6e9175 100644 --- a/hopp/simulation/technologies/dispatch/dispatch.py +++ b/hopp/simulation/technologies/dispatch/dispatch.py @@ -4,18 +4,22 @@ try: u.USD except AttributeError: - u.load_definitions_from_strings(['USD = [currency]', 'lifecycle = [energy] / [energy]']) + u.load_definitions_from_strings( + ["USD = [currency]", "lifecycle = [energy] / [energy]"] + ) + class Dispatch: - """ + """ """ - """ - def __init__(self, - pyomo_model: pyomo.ConcreteModel, - index_set: pyomo.Set, - system_model, - financial_model, - block_set_name: str = 'dispatch'): + def __init__( + self, + pyomo_model: pyomo.ConcreteModel, + index_set: pyomo.Set, + system_model, + financial_model, + block_set_name: str = "dispatch", + ): self.block_set_name = block_set_name self.round_digits = int(4) @@ -29,13 +33,19 @@ def __init__(self, @staticmethod def dispatch_block_rule(block, t): - raise NotImplemented("This function must be overridden for specific dispatch model") + raise NotImplemented( + "This function must be overridden for specific dispatch model" + ) def initialize_parameters(self): - raise NotImplemented("This function must be overridden for specific dispatch model") + raise NotImplemented( + "This function must be overridden for specific dispatch model" + ) def update_time_series_parameters(self, start_time: int): - raise NotImplemented("This function must be overridden for specific dispatch model") + raise NotImplemented( + "This function must be overridden for specific dispatch model" + ) @staticmethod def _check_efficiency_value(efficiency): diff --git a/hopp/simulation/technologies/dispatch/dispatch_problem_state.py b/hopp/simulation/technologies/dispatch/dispatch_problem_state.py index 2d30493f9..c57db9a7c 100644 --- a/hopp/simulation/technologies/dispatch/dispatch_problem_state.py +++ b/hopp/simulation/technologies/dispatch/dispatch_problem_state.py @@ -18,7 +18,9 @@ def __init__(self): self._gap = () self._n_non_optimal_solves = 0 - def store_problem_metrics(self, solver_results, start_time, n_days, objective_value): + def store_problem_metrics( + self, solver_results, start_time, n_days, objective_value + ): self.start_time = start_time self.n_days = n_days self.termination_condition = str(solver_results.solver.termination_condition) @@ -35,20 +37,24 @@ def store_problem_metrics(self, solver_results, start_time, n_days, objective_va # solver_results.solution.Gap not define if solver_results.problem.upper_bound != 0.0: - self.gap = (abs(solver_results.problem.upper_bound - solver_results.problem.lower_bound) - / abs(solver_results.problem.upper_bound)) + self.gap = abs( + solver_results.problem.upper_bound - solver_results.problem.lower_bound + ) / abs(solver_results.problem.upper_bound) elif solver_results.problem.lower_bound == 0.0: self.gap = 0.0 else: - self.gap = float('inf') + self.gap = float("inf") - if not solver_results.solver.termination_condition == TerminationCondition.optimal: + if ( + not solver_results.solver.termination_condition + == TerminationCondition.optimal + ): self._n_non_optimal_solves += 1 def _update_metric(self, metric_name, value): data = list(getattr(self, metric_name)) data.append(value) - setattr(self, '_' + metric_name, tuple(data)) + setattr(self, "_" + metric_name, tuple(data)) @property def start_time(self) -> tuple: @@ -56,7 +62,7 @@ def start_time(self) -> tuple: @start_time.setter def start_time(self, start_hour: int): - self._update_metric('start_time', start_hour) + self._update_metric("start_time", start_hour) @property def n_days(self) -> tuple: @@ -64,7 +70,7 @@ def n_days(self) -> tuple: @n_days.setter def n_days(self, solve_days: int): - self._update_metric('n_days', solve_days) + self._update_metric("n_days", solve_days) @property def termination_condition(self) -> tuple: @@ -72,7 +78,7 @@ def termination_condition(self) -> tuple: @termination_condition.setter def termination_condition(self, condition: str): - self._update_metric('termination_condition', condition) + self._update_metric("termination_condition", condition) @property def solve_time(self) -> tuple: @@ -80,7 +86,7 @@ def solve_time(self) -> tuple: @solve_time.setter def solve_time(self, time: float): - self._update_metric('solve_time', time) + self._update_metric("solve_time", time) @property def objective(self) -> tuple: @@ -88,7 +94,7 @@ def objective(self) -> tuple: @objective.setter def objective(self, objective_value: float): - self._update_metric('objective', objective_value) + self._update_metric("objective", objective_value) @property def upper_bound(self) -> tuple: @@ -96,7 +102,7 @@ def upper_bound(self) -> tuple: @upper_bound.setter def upper_bound(self, bound: float): - self._update_metric('upper_bound', bound) + self._update_metric("upper_bound", bound) @property def lower_bound(self) -> tuple: @@ -104,7 +110,7 @@ def lower_bound(self) -> tuple: @lower_bound.setter def lower_bound(self, bound: float): - self._update_metric('lower_bound', bound) + self._update_metric("lower_bound", bound) @property def constraints(self) -> tuple: @@ -112,7 +118,7 @@ def constraints(self) -> tuple: @constraints.setter def constraints(self, constraint_count: int): - self._update_metric('constraints', constraint_count) + self._update_metric("constraints", constraint_count) @property def variables(self) -> tuple: @@ -120,7 +126,7 @@ def variables(self) -> tuple: @variables.setter def variables(self, variable_count: int): - self._update_metric('variables', variable_count) + self._update_metric("variables", variable_count) @property def non_zeros(self) -> tuple: @@ -128,7 +134,7 @@ def non_zeros(self) -> tuple: @non_zeros.setter def non_zeros(self, non_zeros_count: int): - self._update_metric('non_zeros', non_zeros_count) + self._update_metric("non_zeros", non_zeros_count) @property def gap(self) -> tuple: @@ -136,7 +142,7 @@ def gap(self) -> tuple: @gap.setter def gap(self, mip_gap: int): - self._update_metric('gap', mip_gap) + self._update_metric("gap", mip_gap) @property def n_non_optimal_solves(self) -> int: diff --git a/hopp/simulation/technologies/dispatch/grid_dispatch.py b/hopp/simulation/technologies/dispatch/grid_dispatch.py index 53c49d429..21f79f581 100644 --- a/hopp/simulation/technologies/dispatch/grid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/grid_dispatch.py @@ -21,7 +21,7 @@ def __init__( index_set: pyomo.Set, system_model, financial_model, - block_set_name: str = 'grid', + block_set_name: str = "grid", ): super().__init__( @@ -49,12 +49,11 @@ def max_gross_profit_objective(self, hybrid_blocks): * self.blocks[t].time_duration * self.blocks[t].electricity_sell_price * hybrid_blocks[t].electricity_sold - - (1/hybrid_blocks[t].time_weighting_factor) + - (1 / hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * self.blocks[t].electricity_purchase_price * hybrid_blocks[t].electricity_purchased - - self.blocks[t].epsilon - * self.blocks[t].is_generating + - self.blocks[t].epsilon * self.blocks[t].is_generating for t in hybrid_blocks.index_set() ) ) @@ -74,10 +73,7 @@ def min_operating_cost_objective(self, hybrid_blocks): * self.blocks[t].electricity_purchase_price * hybrid_blocks[t].electricity_purchased ) - + ( - self.blocks[t].epsilon - * self.blocks[t].is_generating - ) + + (self.blocks[t].epsilon * self.blocks[t].is_generating) for t in hybrid_blocks.index_set() ) @@ -108,10 +104,10 @@ def _create_variables(self, hybrid): def _create_port(self, hybrid): hybrid.grid_port = Port( initialize={ - 'system_generation': hybrid.system_generation, - 'system_load': hybrid.system_load, - 'electricity_sold': hybrid.electricity_sold, - 'electricity_purchased': hybrid.electricity_purchased, + "system_generation": hybrid.system_generation, + "system_load": hybrid.system_load, + "electricity_sold": hybrid.electricity_sold, + "electricity_purchased": hybrid.electricity_purchased, } ) return hybrid.grid_port @@ -132,37 +128,43 @@ def _create_grid_parameters(grid): default=1e-3, within=pyomo.NonNegativeReals, mutable=True, - units=u.USD) + units=u.USD, + ) grid.time_duration = pyomo.Param( doc="Time step [hour]", default=1.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.hr) + units=u.hr, + ) grid.electricity_sell_price = pyomo.Param( doc="Electricity sell price [$/MWh]", default=0.0, within=pyomo.Reals, mutable=True, - units=u.USD / u.MWh) + units=u.USD / u.MWh, + ) grid.electricity_purchase_price = pyomo.Param( doc="Electricity purchase price [$/MWh]", default=0.0, within=pyomo.Reals, mutable=True, - units=u.USD / u.MWh) + units=u.USD / u.MWh, + ) grid.generation_transmission_limit = pyomo.Param( doc="Grid transmission limit for generation [MW]", default=1000.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) grid.load_transmission_limit = pyomo.Param( doc="Grid transmission limit for load [MW]", default=1000.0, within=pyomo.NonNegativeReals, mutable=True, - units=u.MW) + units=u.MW, + ) @staticmethod def _create_grid_variables(grid): @@ -170,27 +172,26 @@ def _create_grid_variables(grid): # Variables # ################################## grid.system_generation = pyomo.Var( - doc="System generation [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW) + doc="System generation [MW]", domain=pyomo.NonNegativeReals, units=u.MW + ) grid.system_load = pyomo.Var( - doc="System load [MW]", - domain=pyomo.NonNegativeReals, - units=u.MW) + doc="System load [MW]", domain=pyomo.NonNegativeReals, units=u.MW + ) grid.electricity_sold = pyomo.Var( doc="Electricity sold to the grid [MW]", domain=pyomo.NonNegativeReals, bounds=(0, grid.generation_transmission_limit), - units=u.MW) + units=u.MW, + ) grid.electricity_purchased = pyomo.Var( doc="Electricity purchased from the grid [MW]", domain=pyomo.NonNegativeReals, bounds=(0, grid.load_transmission_limit), - units=u.MW) + units=u.MW, + ) grid.is_generating = pyomo.Var( - doc="System is generating power", - domain=pyomo.Binary, - units=u.dimensionless) + doc="System is generating power", domain=pyomo.Binary, units=u.dimensionless + ) @staticmethod def _create_grid_constraints(grid): @@ -200,23 +201,21 @@ def _create_grid_constraints(grid): grid.balance = pyomo.Constraint( doc="Transmission energy balance", expr=( - grid.electricity_sold - - grid.electricity_purchased - == grid.system_generation - - grid.system_load - ) + grid.electricity_sold - grid.electricity_purchased + == grid.system_generation - grid.system_load + ), ) grid.sales_transmission_limit = pyomo.Constraint( doc="Transmission limit on electricity sales", - expr=grid.electricity_sold <= grid.generation_transmission_limit * grid.is_generating + expr=grid.electricity_sold + <= grid.generation_transmission_limit * grid.is_generating, ) grid.purchases_transmission_limit = pyomo.Constraint( doc="Transmission limit on electricity purchases", expr=( grid.electricity_purchased - <= grid.load_transmission_limit - * (1 - grid.is_generating) - ) + <= grid.load_transmission_limit * (1 - grid.is_generating) + ), ) @staticmethod @@ -231,9 +230,13 @@ def _create_grid_ports(grid): grid.port.add(grid.electricity_purchased) def initialize_parameters(self): - grid_limit_kw = self._system_model.value('grid_interconnection_limit_kwac') - self.generation_transmission_limit = [grid_limit_kw / 1e3] * len(self.blocks.index_set()) - self.load_transmission_limit = [grid_limit_kw / 1e3] * len(self.blocks.index_set()) + grid_limit_kw = self._system_model.value("grid_interconnection_limit_kwac") + self.generation_transmission_limit = [grid_limit_kw / 1e3] * len( + self.blocks.index_set() + ) + self.load_transmission_limit = [grid_limit_kw / 1e3] * len( + self.blocks.index_set() + ) def update_time_series_parameters(self, start_time: int): n_horizon = len(self.blocks.index_set()) @@ -241,40 +244,60 @@ def update_time_series_parameters(self, start_time: int): ppa_price = self._financial_model.value("ppa_price_input")[0] if start_time + n_horizon > len(dispatch_factors): prices = list(dispatch_factors[start_time:]) - prices.extend(list(dispatch_factors[0:n_horizon - len(prices)])) + prices.extend(list(dispatch_factors[0 : n_horizon - len(prices)])) else: - prices = dispatch_factors[start_time:start_time + n_horizon] + prices = dispatch_factors[start_time : start_time + n_horizon] # NOTE: Assuming the same prices - self.electricity_sell_price = [norm_price * ppa_price * 1e3 for norm_price in prices] - self.electricity_purchase_price = [norm_price * ppa_price * 1e3 for norm_price in prices] + self.electricity_sell_price = [ + norm_price * ppa_price * 1e3 for norm_price in prices + ] + self.electricity_purchase_price = [ + norm_price * ppa_price * 1e3 for norm_price in prices + ] @property def electricity_sell_price(self) -> list: - return [self.blocks[t].electricity_sell_price.value for t in self.blocks.index_set()] + return [ + self.blocks[t].electricity_sell_price.value for t in self.blocks.index_set() + ] @electricity_sell_price.setter def electricity_sell_price(self, price_per_mwh: list): if len(price_per_mwh) == len(self.blocks): for t, price in zip(self.blocks, price_per_mwh): - self.blocks[t].electricity_sell_price.set_value(round(price, self.round_digits)) + self.blocks[t].electricity_sell_price.set_value( + round(price, self.round_digits) + ) else: - raise ValueError("'price_per_mwh' list must be the same length as time horizon") + raise ValueError( + "'price_per_mwh' list must be the same length as time horizon" + ) @property def electricity_purchase_price(self) -> list: - return [self.blocks[t].electricity_purchase_price.value for t in self.blocks.index_set()] + return [ + self.blocks[t].electricity_purchase_price.value + for t in self.blocks.index_set() + ] @electricity_purchase_price.setter def electricity_purchase_price(self, price_per_mwh: list): if len(price_per_mwh) == len(self.blocks): for t, price in zip(self.blocks, price_per_mwh): - self.blocks[t].electricity_purchase_price.set_value(round(price, self.round_digits)) + self.blocks[t].electricity_purchase_price.set_value( + round(price, self.round_digits) + ) else: - raise ValueError("'price_per_mwh' list must be the same length as time horizon") + raise ValueError( + "'price_per_mwh' list must be the same length as time horizon" + ) @property def generation_transmission_limit(self) -> list: - return [self.blocks[t].generation_transmission_limit.value for t in self.blocks.index_set()] + return [ + self.blocks[t].generation_transmission_limit.value + for t in self.blocks.index_set() + ] @generation_transmission_limit.setter def generation_transmission_limit(self, limit_mw: list): @@ -288,13 +311,18 @@ def generation_transmission_limit(self, limit_mw: list): @property def load_transmission_limit(self) -> list: - return [self.blocks[t].load_transmission_limit.value for t in self.blocks.index_set()] + return [ + self.blocks[t].load_transmission_limit.value + for t in self.blocks.index_set() + ] @load_transmission_limit.setter def load_transmission_limit(self, limit_mw: list): if len(limit_mw) == len(self.blocks): for t, limit in zip(self.blocks, limit_mw): - self.blocks[t].load_transmission_limit.set_value(round(limit, self.round_digits)) + self.blocks[t].load_transmission_limit.set_value( + round(limit, self.round_digits) + ) else: raise ValueError("'limit_mw' list must be the same length as time horizon") @@ -328,7 +356,9 @@ def electricity_sold(self) -> list: @property def electricity_purchased(self) -> list: - return [self.blocks[t].electricity_purchased.value for t in self.blocks.index_set()] + return [ + self.blocks[t].electricity_purchased.value for t in self.blocks.index_set() + ] @property def is_generating(self) -> list: diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py index f95933189..ae9727f01 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch.py @@ -3,20 +3,21 @@ from pyomo.environ import units as u from hopp.simulation.technologies.dispatch.dispatch import Dispatch -from hopp.simulation.technologies.dispatch.hybrid_dispatch_options import HybridDispatchOptions +from hopp.simulation.technologies.dispatch.hybrid_dispatch_options import ( + HybridDispatchOptions, +) class HybridDispatch(Dispatch): - """ + """ """ - """ def __init__( self, pyomo_model: pyomo.ConcreteModel, index_set: pyomo.Set, power_sources: dict, dispatch_options: HybridDispatchOptions = None, - block_set_name: str = 'hybrid', + block_set_name: str = "hybrid", ): """ @@ -69,14 +70,22 @@ def _create_parameters(hybrid): def _create_variables_and_ports(self, hybrid, t): for tech in self.power_sources.keys(): try: - gen_var, load_var = self.power_sources[tech]._dispatch._create_variables(hybrid) + gen_var, load_var = self.power_sources[ + tech + ]._dispatch._create_variables(hybrid) self.power_source_gen_vars[t].append(gen_var) self.load_vars[t].append(load_var) - self.ports[t].append(self.power_sources[tech]._dispatch._create_port(hybrid)) + self.ports[t].append( + self.power_sources[tech]._dispatch._create_port(hybrid) + ) except AttributeError: - raise ValueError("'{}' is not supported in the hybrid dispatch model.".format(tech)) + raise ValueError( + "'{}' is not supported in the hybrid dispatch model.".format(tech) + ) except Exception as e: - raise RuntimeError("Error in setting up dispatch for {}: {}".format(tech, e)) + raise RuntimeError( + "Error in setting up dispatch for {}: {}".format(tech, e) + ) def _create_hybrid_constraints(self, hybrid, t): hybrid.generation_total = pyomo.Constraint( @@ -89,7 +98,7 @@ def _create_hybrid_constraints(self, hybrid, t): rule=hybrid.system_load == sum(self.load_vars[t]), ) - if 'battery' in self.power_sources.keys(): + if "battery" in self.power_sources.keys(): if self.options.pv_charging_only: self._create_pv_battery_limitation(hybrid) elif not self.options.grid_charging: @@ -99,14 +108,14 @@ def _create_hybrid_constraints(self, hybrid, t): def _create_grid_battery_limitation(hybrid): hybrid.no_grid_battery_charge = pyomo.Constraint( doc="Battery storage cannot charge via the grid", - expr=hybrid.system_generation >= hybrid.battery_charge + expr=hybrid.system_generation >= hybrid.battery_charge, ) @staticmethod def _create_pv_battery_limitation(hybrid): hybrid.only_pv_battery_charge = pyomo.Constraint( doc="Battery storage can only charge from pv", - expr=hybrid.pv_generation >= hybrid.battery_charge + expr=hybrid.pv_generation >= hybrid.battery_charge, ) def create_arcs(self): @@ -114,18 +123,25 @@ def create_arcs(self): # Arcs # ################################## for tech in self.power_sources.keys(): + def arc_rule(m, t): source_port = self.power_sources[tech].dispatch.blocks[t].port destination_port = getattr(self.blocks[t], tech + "_port") - return {'source': source_port, 'destination': destination_port} + return {"source": source_port, "destination": destination_port} - setattr(self.model, tech + "_hybrid_arc", Arc(self.blocks.index_set(), rule=arc_rule)) + setattr( + self.model, + tech + "_hybrid_arc", + Arc(self.blocks.index_set(), rule=arc_rule), + ) self.arcs.append(getattr(self.model, tech + "_hybrid_arc")) pyomo.TransformationFactory("network.expand_arcs").apply_to(self.model) def initialize_parameters(self): - self.time_weighting_factor = self.options.time_weighting_factor # Discount factor + self.time_weighting_factor = ( + self.options.time_weighting_factor + ) # Discount factor for tech in self.power_sources.values(): tech.dispatch.initialize_parameters() @@ -141,17 +157,15 @@ def create_max_gross_profit_objective(self): self._delete_objective() def gross_profit_objective_rule(m) -> float: - obj = 0. + obj = 0.0 for tech in self.power_sources.keys(): # Create the max_gross_profit_objective within each of the technology # dispatch classes. - self.power_sources[tech]._dispatch.max_gross_profit_objective(self.blocks) - # Copy the technology objective to the pyomo model. - setattr( - m, - tech + "_obj", - self.power_sources[tech]._dispatch.obj + self.power_sources[tech]._dispatch.max_gross_profit_objective( + self.blocks ) + # Copy the technology objective to the pyomo model. + setattr(m, tech + "_obj", self.power_sources[tech]._dispatch.obj) # TODO: Does the objective really need to be stored on the self.model object? # Trying to grab the attribute 'obj' from the dispatch classes # themselves doesn't seem to work within pyomo, e.g.: @@ -163,18 +177,20 @@ def gross_profit_objective_rule(m) -> float: return obj self.model.objective = pyomo.Objective( - expr=gross_profit_objective_rule, - sense=pyomo.maximize) + expr=gross_profit_objective_rule, sense=pyomo.maximize + ) def create_min_operating_cost_objective(self): self._delete_objective() def operating_cost_objective_rule(m) -> float: - obj = 0. + obj = 0.0 for tech in self.power_sources.keys(): # Create the min_operating_cost_objective within each of the technology # dispatch classes. - self.power_sources[tech]._dispatch.min_operating_cost_objective(self.blocks) + self.power_sources[tech]._dispatch.min_operating_cost_objective( + self.blocks + ) # Assemble the objective as a linear summation. obj += self.power_sources[tech]._dispatch.obj @@ -182,23 +198,26 @@ def operating_cost_objective_rule(m) -> float: return obj self.model.objective = pyomo.Objective( - rule=operating_cost_objective_rule, - sense=pyomo.minimize + rule=operating_cost_objective_rule, sense=pyomo.minimize ) @property def time_weighting_factor(self) -> float: for t in self.blocks.index_set(): - return self.blocks[t+1].time_weighting_factor.value + return self.blocks[t + 1].time_weighting_factor.value @time_weighting_factor.setter def time_weighting_factor(self, weighting: float): for t in self.blocks.index_set(): - self.blocks[t].time_weighting_factor = round(weighting ** t, self.round_digits) + self.blocks[t].time_weighting_factor = round( + weighting**t, self.round_digits + ) @property def time_weighting_factor_list(self) -> list: - return [self.blocks[t].time_weighting_factor.value for t in self.blocks.index_set()] + return [ + self.blocks[t].time_weighting_factor.value for t in self.blocks.index_set() + ] # Outputs @property @@ -216,7 +235,7 @@ def wind_generation(self) -> list: @property def wave_generation(self) -> list: return [self.blocks[t].wave_generation.value for t in self.blocks.index_set()] - + @property def tower_generation(self) -> list: return [self.blocks[t].tower_generation.value for t in self.blocks.index_set()] @@ -251,8 +270,8 @@ def system_load(self) -> list: @property def electricity_sales(self) -> list: - if 'grid' in self.power_sources: - tb = self.power_sources['grid'].dispatch.blocks + if "grid" in self.power_sources: + tb = self.power_sources["grid"].dispatch.blocks return [ tb[t].time_duration.value * tb[t].electricity_sell_price.value @@ -262,8 +281,8 @@ def electricity_sales(self) -> list: @property def electricity_purchases(self) -> list: - if 'grid' in self.power_sources: - tb = self.power_sources['grid'].dispatch.blocks + if "grid" in self.power_sources: + tb = self.power_sources["grid"].dispatch.blocks return [ tb[t].time_duration.value * tb[t].electricity_purchase_price.value diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py index cbc5d694f..9367e17b0 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_builder_solver.py @@ -7,7 +7,11 @@ from pyomo.util.check_units import assert_units_consistent from hopp.simulation.technologies.sites.site_info import SiteInfo -from hopp.simulation.technologies.dispatch import HybridDispatch, HybridDispatchOptions, DispatchProblemState +from hopp.simulation.technologies.dispatch import ( + HybridDispatch, + HybridDispatchOptions, + DispatchProblemState, +) from hopp.simulation.technologies.clustering import Clustering from hopp.utilities.log import hybrid_logger as logger @@ -15,10 +19,10 @@ class HybridDispatchBuilderSolver: """Helper class for building hybrid system dispatch problem, solving dispatch problem, and simulating system with dispatch solution.""" - def __init__(self, - site: SiteInfo, - power_sources: dict, - dispatch_options: dict = None): + + def __init__( + self, site: SiteInfo, power_sources: dict, dispatch_options: dict = None + ): """ Parameters @@ -37,7 +41,9 @@ def __init__(self, if os.path.isfile(self.options.log_name): os.remove(self.options.log_name) - self.needs_dispatch = any(item in ['battery', 'tower', 'trough'] for item in self.power_sources.keys()) + self.needs_dispatch = any( + item in ["battery", "tower", "trough"] for item in self.power_sources.keys() + ) if self.needs_dispatch: self._pyomo_model = self._create_dispatch_optimization_model() @@ -48,17 +54,27 @@ def __init__(self, self.dispatch.create_arcs() assert_units_consistent(self.pyomo_model) self.problem_state = DispatchProblemState() - + # Clustering (optional) self.clustering = None if self.options.use_clustering: - #TODO: Add resource data for wind - self.clustering = Clustering(power_sources.keys(), self.site.solar_resource.filename, wind_resource_data = None, price_data = self.site.elec_prices.data) + # TODO: Add resource data for wind + self.clustering = Clustering( + power_sources.keys(), + self.site.solar_resource.filename, + wind_resource_data=None, + price_data=self.site.elec_prices.data, + ) self.clustering.n_cluster = self.options.n_clusters if len(self.options.clustering_weights.keys()) == 0: self.clustering.use_default_weights = True - elif self.options.clustering_divisions.keys() != self.options.clustering_weights.keys(): - print ('Warning: Keys in user-specified dictionaries for clustering weights and divisions do not match. Reverting to default weights/divisions') + elif ( + self.options.clustering_divisions.keys() + != self.options.clustering_weights.keys() + ): + print( + "Warning: Keys in user-specified dictionaries for clustering weights and divisions do not match. Reverting to default weights/divisions" + ) self.clustering.use_default_weights = True else: self.clustering.weights = self.options.clustering_weights @@ -70,252 +86,331 @@ def _create_dispatch_optimization_model(self): """ Creates monolith dispatch model """ - model = pyomo.ConcreteModel(name='hybrid_dispatch') + model = pyomo.ConcreteModel(name="hybrid_dispatch") ################################# # Sets # ################################# - model.forecast_horizon = pyomo.Set(doc="Set of time periods in time horizon", - initialize=range(self.options.n_look_ahead_periods)) + model.forecast_horizon = pyomo.Set( + doc="Set of time periods in time horizon", + initialize=range(self.options.n_look_ahead_periods), + ) ################################# # Blocks (technologies) # ################################# module = getattr(__import__("hopp").simulation.technologies, "dispatch") for source, tech in self.power_sources.items(): - if source == 'battery': + if source == "battery": tech._dispatch = self.options.battery_dispatch_class( model, model.forecast_horizon, tech._system_model, tech._financial_model, block_set_name=source, - dispatch_options=self.options) + dispatch_options=self.options, + ) else: try: - dispatch_class_name = getattr(module, source.capitalize() + "Dispatch") + dispatch_class_name = getattr( + module, source.capitalize() + "Dispatch" + ) except AttributeError: - raise ValueError("Could not find {} in dispatch module. Is {} supported in the hybrid " - "dispatch model?".format(source.capitalize() + "Dispatch", source)) + raise ValueError( + "Could not find {} in dispatch module. Is {} supported in the hybrid " + "dispatch model?".format( + source.capitalize() + "Dispatch", source + ) + ) tech._dispatch = dispatch_class_name( model, model.forecast_horizon, tech._system_model, - tech._financial_model) + tech._financial_model, + ) self._dispatch = HybridDispatch( - model, - model.forecast_horizon, - self.power_sources, - self.options) + model, model.forecast_horizon, self.power_sources, self.options + ) return model def solve_dispatch_model(self, start_time: int, n_days: int): # Solve dispatch model - if self.options.solver == 'glpk': + if self.options.solver == "glpk": solver_results = self.glpk_solve() - elif self.options.solver == 'cbc': + elif self.options.solver == "cbc": solver_results = self.cbc_solve() - elif self.options.solver == 'xpress': + elif self.options.solver == "xpress": solver_results = self.xpress_solve() - elif self.options.solver == 'xpress_persistent': + elif self.options.solver == "xpress_persistent": solver_results = self.xpress_persistent_solve() - elif self.options.solver == 'gurobi_ampl': + elif self.options.solver == "gurobi_ampl": solver_results = self.gurobi_ampl_solve() - elif self.options.solver == 'gurobi': + elif self.options.solver == "gurobi": solver_results = self.gurobi_solve() else: raise ValueError("{} is not a supported solver".format(self.options.solver)) - self.problem_state.store_problem_metrics(solver_results, start_time, n_days, - self.dispatch.objective_value) + self.problem_state.store_problem_metrics( + solver_results, start_time, n_days, self.dispatch.objective_value + ) @staticmethod - def glpk_solve_call(pyomo_model: pyomo.ConcreteModel, - log_name: str = "", - user_solver_options: dict = None): + def glpk_solve_call( + pyomo_model: pyomo.ConcreteModel, + log_name: str = "", + user_solver_options: dict = None, + ): # log_name = "annual_solve_GLPK.log" # For debugging MILP solver # Ref. on solver options: https://en.wikibooks.org/wiki/GLPK/Using_GLPSOL - glpk_solver_options = {'cuts': None, - 'presol': None, - # 'mostf': None, - # 'mipgap': 0.001, - 'tmlim': 30 - } - solver_options = SolverOptions(glpk_solver_options, log_name, user_solver_options,'log') - with pyomo.SolverFactory('glpk') as solver: + glpk_solver_options = { + "cuts": None, + "presol": None, + # 'mostf': None, + # 'mipgap': 0.001, + "tmlim": 30, + } + solver_options = SolverOptions( + glpk_solver_options, log_name, user_solver_options, "log" + ) + with pyomo.SolverFactory("glpk") as solver: results = solver.solve(pyomo_model, options=solver_options.constructed) - HybridDispatchBuilderSolver.log_and_solution_check(log_name, solver_options.instance_log, results.solver.termination_condition, pyomo_model) + HybridDispatchBuilderSolver.log_and_solution_check( + log_name, + solver_options.instance_log, + results.solver.termination_condition, + pyomo_model, + ) return results - + def glpk_solve(self): - return HybridDispatchBuilderSolver.glpk_solve_call(self.pyomo_model, - self.options.log_name, - self.options.solver_options) - + return HybridDispatchBuilderSolver.glpk_solve_call( + self.pyomo_model, self.options.log_name, self.options.solver_options + ) + @staticmethod - def gurobi_ampl_solve_call(pyomo_model: pyomo.ConcreteModel, - log_name: str = "", - user_solver_options: dict = None): + def gurobi_ampl_solve_call( + pyomo_model: pyomo.ConcreteModel, + log_name: str = "", + user_solver_options: dict = None, + ): # Ref. on solver options: https://www.gurobi.com/documentation/9.1/ampl-gurobi/parameters.html - gurobi_solver_options = {'timelim': 60, - 'threads': 1} - solver_options = SolverOptions(gurobi_solver_options, log_name, user_solver_options,'logfile') - - with pyomo.SolverFactory('gurobi', executable='/opt/solvers/gurobi', solver_io='nl') as solver: + gurobi_solver_options = {"timelim": 60, "threads": 1} + solver_options = SolverOptions( + gurobi_solver_options, log_name, user_solver_options, "logfile" + ) + + with pyomo.SolverFactory( + "gurobi", executable="/opt/solvers/gurobi", solver_io="nl" + ) as solver: results = solver.solve(pyomo_model, options=solver_options.constructed) - HybridDispatchBuilderSolver.log_and_solution_check(log_name, solver_options.instance_log, results.solver.termination_condition, pyomo_model) + HybridDispatchBuilderSolver.log_and_solution_check( + log_name, + solver_options.instance_log, + results.solver.termination_condition, + pyomo_model, + ) return results def gurobi_ampl_solve(self): - return HybridDispatchBuilderSolver.gurobi_ampl_solve_call(self.pyomo_model, - self.options.log_name, - self.options.solver_options) - + return HybridDispatchBuilderSolver.gurobi_ampl_solve_call( + self.pyomo_model, self.options.log_name, self.options.solver_options + ) + @staticmethod - def gurobi_solve_call(opt: pyomo.SolverFactory, - pyomo_model: pyomo.ConcreteModel, - log_name: str = "", - user_solver_options: dict = None): + def gurobi_solve_call( + opt: pyomo.SolverFactory, + pyomo_model: pyomo.ConcreteModel, + log_name: str = "", + user_solver_options: dict = None, + ): # Ref. on solver options: https://www.gurobi.com/documentation/9.1/ampl-gurobi/parameters.html - gurobi_solver_options = {'timelim': 60, - 'threads': 1} - solver_options = SolverOptions(gurobi_solver_options, log_name, user_solver_options,'logfile') - + gurobi_solver_options = {"timelim": 60, "threads": 1} + solver_options = SolverOptions( + gurobi_solver_options, log_name, user_solver_options, "logfile" + ) + opt.options.update(solver_options.constructed) opt.set_instance(pyomo_model) results = opt.solve(save_results=False) - HybridDispatchBuilderSolver.log_and_solution_check(log_name, solver_options.instance_log, results.solver.termination_condition, pyomo_model) + HybridDispatchBuilderSolver.log_and_solution_check( + log_name, + solver_options.instance_log, + results.solver.termination_condition, + pyomo_model, + ) return results def gurobi_solve(self): if self.opt is None: - self.opt = pyomo.SolverFactory('gurobi', solver_io='persistent') - - return HybridDispatchBuilderSolver.gurobi_solve_call(self.opt, - self.pyomo_model, - self.options.log_name, - self.options.solver_options) + self.opt = pyomo.SolverFactory("gurobi", solver_io="persistent") + + return HybridDispatchBuilderSolver.gurobi_solve_call( + self.opt, + self.pyomo_model, + self.options.log_name, + self.options.solver_options, + ) @staticmethod - def cbc_solve_call(pyomo_model: pyomo.ConcreteModel, - log_name: str = "", - user_solver_options: dict = None): + def cbc_solve_call( + pyomo_model: pyomo.ConcreteModel, + log_name: str = "", + user_solver_options: dict = None, + ): # log_name = "annual_solve_CBC.log" # Solver options can be found by launching executable 'start cbc.exe', verbose 15, ? # https://coin-or.github.io/Cbc/faq.html (a bit outdated) - cbc_solver_options = { # 'ratioGap': 0.001, - 'seconds': 60} - solver_options = SolverOptions(cbc_solver_options, log_name, user_solver_options,'log') + cbc_solver_options = {"seconds": 60} # 'ratioGap': 0.001, + solver_options = SolverOptions( + cbc_solver_options, log_name, user_solver_options, "log" + ) - if sys.platform == 'win32' or sys.platform == 'cygwin': + if sys.platform == "win32" or sys.platform == "cygwin": cbc_path = Path(__file__).parent / "cbc_solver" / "cbc-win64" / "cbc" if log_name != "": - logger.warning("Warning: CBC solver logging is active... This will significantly increase simulation time.") - solver_options.constructed['log'] = 2 - solver = pyomo.SolverFactory('asl:cbc', executable=cbc_path) - results = solver.solve(pyomo_model, logfile=solver_options.instance_log, options=solver_options.constructed) + logger.warning( + "Warning: CBC solver logging is active... This will significantly increase simulation time." + ) + solver_options.constructed["log"] = 2 + solver = pyomo.SolverFactory("asl:cbc", executable=cbc_path) + results = solver.solve( + pyomo_model, + logfile=solver_options.instance_log, + options=solver_options.constructed, + ) else: - solver = pyomo.SolverFactory('cbc', executable=cbc_path, solver_io='nl') + solver = pyomo.SolverFactory("cbc", executable=cbc_path, solver_io="nl") results = solver.solve(pyomo_model, options=solver_options.constructed) - elif sys.platform == 'darwin' or sys.platform == 'linux': - solver = pyomo.SolverFactory('cbc') + elif sys.platform == "darwin" or sys.platform == "linux": + solver = pyomo.SolverFactory("cbc") results = solver.solve(pyomo_model, options=solver_options.constructed) else: - raise SystemError('Platform not supported ', sys.platform) - - HybridDispatchBuilderSolver.log_and_solution_check(log_name, solver_options.instance_log, results.solver.termination_condition, pyomo_model) + raise SystemError("Platform not supported ", sys.platform) + + HybridDispatchBuilderSolver.log_and_solution_check( + log_name, + solver_options.instance_log, + results.solver.termination_condition, + pyomo_model, + ) return results def cbc_solve(self): - return HybridDispatchBuilderSolver.cbc_solve_call(self.pyomo_model, - self.options.log_name, - self.options.solver_options) + return HybridDispatchBuilderSolver.cbc_solve_call( + self.pyomo_model, self.options.log_name, self.options.solver_options + ) @staticmethod - def xpress_solve_call(pyomo_model: pyomo.ConcreteModel, - log_name: str = "", - user_solver_options: dict = None): + def xpress_solve_call( + pyomo_model: pyomo.ConcreteModel, + log_name: str = "", + user_solver_options: dict = None, + ): # FIXME: Logging does not work # log_name = "annual_solve_Xpress.log" # For debugging MILP solver # Ref. on solver options: https://ampl.com/products/solvers/solvers-we-sell/xpress/options/ - xpress_solver_options = {'mipgap': 0.001, - 'maxtime': 30} - solver_options = SolverOptions(xpress_solver_options, log_name, user_solver_options,'LOGFILE') + xpress_solver_options = {"mipgap": 0.001, "maxtime": 30} + solver_options = SolverOptions( + xpress_solver_options, log_name, user_solver_options, "LOGFILE" + ) - with pyomo.SolverFactory('xpress_direct') as solver: + with pyomo.SolverFactory("xpress_direct") as solver: results = solver.solve(pyomo_model, options=solver_options.constructed) - HybridDispatchBuilderSolver.log_and_solution_check(log_name, solver_options.instance_log, results.solver.termination_condition, pyomo_model) + HybridDispatchBuilderSolver.log_and_solution_check( + log_name, + solver_options.instance_log, + results.solver.termination_condition, + pyomo_model, + ) return results def xpress_solve(self): - return HybridDispatchBuilderSolver.xpress_solve_call(self.pyomo_model, - self.options.log_name, - self.options.solver_options) + return HybridDispatchBuilderSolver.xpress_solve_call( + self.pyomo_model, self.options.log_name, self.options.solver_options + ) @staticmethod - def xpress_persistent_solve_call(opt: pyomo.SolverFactory, - pyomo_model: pyomo.ConcreteModel, - log_name: str = "", - user_solver_options: dict = None): + def xpress_persistent_solve_call( + opt: pyomo.SolverFactory, + pyomo_model: pyomo.ConcreteModel, + log_name: str = "", + user_solver_options: dict = None, + ): # log_name = "annual_solve_Xpress.log" # For debugging MILP solver # Ref. on solver options: https://ampl.com/products/solvers/solvers-we-sell/xpress/options/ - xpress_solver_options = {'mipgap': 0.001, - 'MAXTIME': 30} - solver_options = SolverOptions(xpress_solver_options, log_name, user_solver_options,'LOGFILE') + xpress_solver_options = {"mipgap": 0.001, "MAXTIME": 30} + solver_options = SolverOptions( + xpress_solver_options, log_name, user_solver_options, "LOGFILE" + ) opt.options.update(solver_options.constructed) opt.set_instance(pyomo_model) results = opt.solve(save_results=False) - HybridDispatchBuilderSolver.log_and_solution_check(log_name, solver_options.instance_log, results.solver.termination_condition, pyomo_model) + HybridDispatchBuilderSolver.log_and_solution_check( + log_name, + solver_options.instance_log, + results.solver.termination_condition, + pyomo_model, + ) return results def xpress_persistent_solve(self): if self.opt is None: - self.opt = pyomo.SolverFactory('xpress', solver_io='persistent') + self.opt = pyomo.SolverFactory("xpress", solver_io="persistent") + + return HybridDispatchBuilderSolver.xpress_persistent_solve_call( + self.opt, + self.pyomo_model, + self.options.log_name, + self.options.solver_options, + ) - return HybridDispatchBuilderSolver.xpress_persistent_solve_call(self.opt, - self.pyomo_model, - self.options.log_name, - self.options.solver_options) @staticmethod - def mindtpy_solve_call(pyomo_model: pyomo.ConcreteModel, - log_name: str = ""): + def mindtpy_solve_call(pyomo_model: pyomo.ConcreteModel, log_name: str = ""): raise NotImplementedError - solver = pyomo.SolverFactory('mindtpy') - results = solver.solve(pyomo_model, - mip_solver='glpk', - nlp_solver='ipopt', - tee=True) - - HybridDispatchBuilderSolver.log_and_solution_check("", "", results.solver.termination_condition, pyomo_model) + solver = pyomo.SolverFactory("mindtpy") + results = solver.solve( + pyomo_model, mip_solver="glpk", nlp_solver="ipopt", tee=True + ) + + HybridDispatchBuilderSolver.log_and_solution_check( + "", "", results.solver.termination_condition, pyomo_model + ) return results @staticmethod - def log_and_solution_check(log_name:str, solve_log: str, solver_termination_condition, pyomo_model): + def log_and_solution_check( + log_name: str, solve_log: str, solver_termination_condition, pyomo_model + ): if log_name != "": HybridDispatchBuilderSolver.append_solve_to_log(log_name, solve_log) - HybridDispatchBuilderSolver.check_solve_condition(solver_termination_condition, pyomo_model) + HybridDispatchBuilderSolver.check_solve_condition( + solver_termination_condition, pyomo_model + ) @staticmethod def check_solve_condition(solver_termination_condition, pyomo_model): if solver_termination_condition == TerminationCondition.infeasible: HybridDispatchBuilderSolver.print_infeasible_problem(pyomo_model) elif not solver_termination_condition == TerminationCondition.optimal: - logger.warning("Warning: Dispatch problem termination condition was '" - + str(solver_termination_condition) + "'") + logger.warning( + "Warning: Dispatch problem termination condition was '" + + str(solver_termination_condition) + + "'" + ) @staticmethod def append_solve_to_log(log_name: str, solve_log: str): # Appends single problem instance log to annual log file - fin = open(solve_log, 'r') + fin = open(solve_log, "r") data = fin.read() fin.close() - ann_log = open(log_name, 'a+') + ann_log = open(log_name, "a+") ann_log.write("=" * 50 + "\n") ann_log.write(data) ann_log.close() @@ -323,15 +418,17 @@ def append_solve_to_log(log_name: str, solve_log: str): @staticmethod def print_infeasible_problem(model: pyomo.ConcreteModel): original_stdout = sys.stdout - with open('infeasible_instance.txt', 'w') as f: + with open("infeasible_instance.txt", "w") as f: sys.stdout = f - print('\n' + '#' * 20 + ' Model Parameter Values ' + '#' * 20 + '\n') + print("\n" + "#" * 20 + " Model Parameter Values " + "#" * 20 + "\n") HybridDispatchBuilderSolver.print_all_parameters(model) - print('\n' + '#' * 20 + ' Model Blocks Display ' + '#' * 20 + '\n') + print("\n" + "#" * 20 + " Model Blocks Display " + "#" * 20 + "\n") HybridDispatchBuilderSolver.display_all_blocks(model) sys.stdout = original_stdout - raise ValueError("Dispatch optimization model is infeasible.\n" - "See 'infeasible_instance.txt' for parameter values.") + raise ValueError( + "Dispatch optimization model is infeasible.\n" + "See 'infeasible_instance.txt' for parameter values." + ) @staticmethod def print_all_parameters(model: pyomo.ConcreteModel): @@ -347,7 +444,9 @@ def print_all_parameters(model: pyomo.ConcreteModel): print("\nParent Block Name: ", block_name) print("Parameter: ", name_to_print) for index in parent_block.index_set(): - val_to_print = pyomo.value(getattr(parent_block[index], param_object.getname())) + val_to_print = pyomo.value( + getattr(parent_block[index], param_object.getname()) + ) print("\t", index, "\t", val_to_print) @staticmethod @@ -370,78 +469,138 @@ def simulate_power(self): # Solving the year in series for i, t in enumerate(ti): if self.options.is_test_start_year or self.options.is_test_end_year: - if (self.options.is_test_start_year and i < 5) or (self.options.is_test_end_year and i > 359): + if (self.options.is_test_start_year and i < 5) or ( + self.options.is_test_end_year and i > 359 + ): start_time = time.time() self.simulate_with_dispatch(t) sim_w_dispath_time = time.time() - logger.info('Day {} dispatch optimized.'.format(i)) - logger.info(" %6.2f seconds required to simulate with dispatch" % (sim_w_dispath_time - start_time)) + logger.info("Day {} dispatch optimized.".format(i)) + logger.info( + " %6.2f seconds required to simulate with dispatch" + % (sim_w_dispath_time - start_time) + ) else: continue # TODO: can we make the csp and battery model run with heuristic dispatch here? # Maybe calling a simulate_with_heuristic() method else: if (i % 73) == 0: - logger.info("\t {:.0f} % complete".format(i*20/73)) + logger.info("\t {:.0f} % complete".format(i * 20 / 73)) self.simulate_with_dispatch(t) else: - initial_states = {tech:{'day':[], 'soc':[], 'load':[]} for tech in ['trough', 'tower', 'battery'] if tech in self.power_sources.keys()} # List of known charge states at 12 am from completed simulations - npercluster = self.clustering.clusters['count'] - inds = sorted(range(len(npercluster)), key=npercluster.__getitem__) # Indicies to sort clusters by low-to-high number of days represented - for i in range(self.clustering.clusters['n_cluster']): + initial_states = { + tech: {"day": [], "soc": [], "load": []} + for tech in ["trough", "tower", "battery"] + if tech in self.power_sources.keys() + } # List of known charge states at 12 am from completed simulations + npercluster = self.clustering.clusters["count"] + inds = sorted( + range(len(npercluster)), key=npercluster.__getitem__ + ) # Indicies to sort clusters by low-to-high number of days represented + for i in range(self.clustering.clusters["n_cluster"]): j = inds[i] # cluster index time_start, time_stop = self.clustering.get_sim_start_end_times(j) - battery_soc = self.clustering.battery_soc_heuristic(j, initial_states['battery']) if 'battery' in self.power_sources.keys() else None + battery_soc = ( + self.clustering.battery_soc_heuristic(j, initial_states["battery"]) + if "battery" in self.power_sources.keys() + else None + ) # Set CSP initial states (need to do this prior to update_time_series_parameters() or update_initial_conditions(), both pull from the stored plant state) - for tech in ['trough', 'tower']: + for tech in ["trough", "tower"]: if tech in self.power_sources.keys(): - self.power_sources[tech].plant_state = self.power_sources[tech].set_initial_plant_state() # Reset to default initial state - csp_soc, is_cycle_on, initial_cycle_load = self.clustering.csp_initial_state_heuristic(j, self.power_sources[tech].solar_multiple, initial_states[tech]) - self.power_sources[tech].set_tes_soc(csp_soc) - self.power_sources[tech].set_cycle_state(is_cycle_on) + self.power_sources[tech].plant_state = self.power_sources[ + tech + ].set_initial_plant_state() # Reset to default initial state + csp_soc, is_cycle_on, initial_cycle_load = ( + self.clustering.csp_initial_state_heuristic( + j, + self.power_sources[tech].solar_multiple, + initial_states[tech], + ) + ) + self.power_sources[tech].set_tes_soc(csp_soc) + self.power_sources[tech].set_cycle_state(is_cycle_on) self.power_sources[tech].set_cycle_load(initial_cycle_load) - self.simulate_with_dispatch(time_start, self.clustering.ndays+1, battery_soc, n_initial_sims = 1) + self.simulate_with_dispatch( + time_start, self.clustering.ndays + 1, battery_soc, n_initial_sims=1 + ) # Update lists of known states at 12am - for tech in ['trough', 'tower', 'battery']: + for tech in ["trough", "tower", "battery"]: if tech in self.power_sources.keys(): for d in range(self.clustering.ndays): - day = self.clustering.sim_start_days[j]+d - initial_states[tech]['day'].append(day) - if tech in ['trough', 'tower']: - initial_states[tech]['soc'].append(self.power_sources[tech].get_tes_soc(day*24)) - initial_states[tech]['load'].append(self.power_sources[tech].get_cycle_load(day*24)) - elif tech in ['battery']: - step = day*24 * int(self.site.n_timesteps/8760) - initial_states[tech]['soc'].append(self.power_sources[tech].Outputs.SOC[step]) + day = self.clustering.sim_start_days[j] + d + initial_states[tech]["day"].append(day) + if tech in ["trough", "tower"]: + initial_states[tech]["soc"].append( + self.power_sources[tech].get_tes_soc(day * 24) + ) + initial_states[tech]["load"].append( + self.power_sources[tech].get_cycle_load(day * 24) + ) + elif tech in ["battery"]: + step = day * 24 * int(self.site.n_timesteps / 8760) + initial_states[tech]["soc"].append( + self.power_sources[tech].Outputs.SOC[step] + ) # After exemplar simulations, update to full annual generation array for dispatchable technologies for tech in self.power_sources.keys(): - if tech in ['battery']: - for key in ['gen', 'P', 'SOC']: + if tech in ["battery"]: + for key in ["gen", "P", "SOC"]: val = getattr(self.power_sources[tech].Outputs, key) - setattr(self.power_sources[tech].Outputs, key, list(self.clustering.compute_annual_array_from_cluster_exemplar_data(val))) - elif tech in ['trough', 'tower']: - for key in ['gen', 'P_out_net', 'P_cycle', 'q_dot_pc_startup', 'q_pc_startup', 'e_ch_tes', 'eta', 'q_pb']: # Data quantities used in capacity value calculations - self.power_sources[tech].outputs.ssc_time_series[key] = list(self.clustering.compute_annual_array_from_cluster_exemplar_data(self.power_sources[tech].outputs.ssc_time_series[key])) - - def simulate_with_dispatch(self, - start_time: int, - n_days: int = 1, - initial_soc: float = None, - n_initial_sims: int = 0): + setattr( + self.power_sources[tech].Outputs, + key, + list( + self.clustering.compute_annual_array_from_cluster_exemplar_data( + val + ) + ), + ) + elif tech in ["trough", "tower"]: + for key in [ + "gen", + "P_out_net", + "P_cycle", + "q_dot_pc_startup", + "q_pc_startup", + "e_ch_tes", + "eta", + "q_pb", + ]: # Data quantities used in capacity value calculations + self.power_sources[tech].outputs.ssc_time_series[key] = list( + self.clustering.compute_annual_array_from_cluster_exemplar_data( + self.power_sources[tech].outputs.ssc_time_series[key] + ) + ) + + def simulate_with_dispatch( + self, + start_time: int, + n_days: int = 1, + initial_soc: float = None, + n_initial_sims: int = 0, + ): # this is needed for clustering effort - update_dispatch_times = list(range(start_time, - start_time + n_days * self.site.n_periods_per_day, - self.options.n_roll_periods)) + update_dispatch_times = list( + range( + start_time, + start_time + n_days * self.site.n_periods_per_day, + self.options.n_roll_periods, + ) + ) for i, sim_start_time in enumerate(update_dispatch_times): # Update battery initial state of charge - if 'battery' in self.power_sources.keys(): - self.power_sources['battery'].dispatch.update_dispatch_initial_soc(initial_soc=initial_soc) + if "battery" in self.power_sources.keys(): + self.power_sources["battery"].dispatch.update_dispatch_initial_soc( + initial_soc=initial_soc + ) initial_soc = None for model in self.power_sources.values(): @@ -450,23 +609,38 @@ def simulate_with_dispatch(self, model.dispatch.update_time_series_parameters(sim_start_time) if self.site.follow_desired_schedule: - n_horizon = len(self.power_sources['grid'].dispatch.blocks.index_set()) + n_horizon = len(self.power_sources["grid"].dispatch.blocks.index_set()) if start_time + n_horizon > len(self.site.desired_schedule): system_limit = list(self.site.desired_schedule[start_time:]) - system_limit.extend(list(self.site.desired_schedule[0:n_horizon - len(system_limit)])) + system_limit.extend( + list( + self.site.desired_schedule[ + 0 : n_horizon - len(system_limit) + ] + ) + ) else: - system_limit = self.site.desired_schedule[start_time:start_time + n_horizon] - - transmission_limit = self.power_sources['grid'].value('grid_interconnection_limit_kwac') / 1e3 + system_limit = self.site.desired_schedule[ + start_time : start_time + n_horizon + ] + + transmission_limit = ( + self.power_sources["grid"].value("grid_interconnection_limit_kwac") + / 1e3 + ) for count, value in enumerate(system_limit): if value > transmission_limit: - logger.warning('Warning: Desired schedule is greater than transmission limit. ' - 'Overwriting schedule to transmission limit') + logger.warning( + "Warning: Desired schedule is greater than transmission limit. " + "Overwriting schedule to transmission limit" + ) system_limit[count] = transmission_limit - self.power_sources['grid'].dispatch.generation_transmission_limit = system_limit + self.power_sources["grid"].dispatch.generation_transmission_limit = ( + system_limit + ) - if 'heuristic' in self.options.battery_dispatch: + if "heuristic" in self.options.battery_dispatch: # TODO: this is not a good way to do this... This won't work with CSP addition... self.battery_heuristic() # TODO: we could just run the csp model without dispatch here @@ -480,51 +654,64 @@ def simulate_with_dispatch(self, battery_sim_start_time = None # simulate using dispatch solution - if 'battery' in self.power_sources.keys(): - self.power_sources['battery'].simulate_with_dispatch(self.options.n_roll_periods, - sim_start_time=battery_sim_start_time) - - if 'trough' in self.power_sources.keys(): - self.power_sources['trough'].simulate_with_dispatch(self.options.n_roll_periods, - sim_start_time=sim_start_time, - store_outputs=store_outputs) - if 'tower' in self.power_sources.keys(): - self.power_sources['tower'].simulate_with_dispatch(self.options.n_roll_periods, - sim_start_time=sim_start_time, - store_outputs=store_outputs) + if "battery" in self.power_sources.keys(): + self.power_sources["battery"].simulate_with_dispatch( + self.options.n_roll_periods, sim_start_time=battery_sim_start_time + ) + + if "trough" in self.power_sources.keys(): + self.power_sources["trough"].simulate_with_dispatch( + self.options.n_roll_periods, + sim_start_time=sim_start_time, + store_outputs=store_outputs, + ) + if "tower" in self.power_sources.keys(): + self.power_sources["tower"].simulate_with_dispatch( + self.options.n_roll_periods, + sim_start_time=sim_start_time, + store_outputs=store_outputs, + ) def battery_heuristic(self): - tot_gen = [0.0]*self.options.n_look_ahead_periods - if 'pv' in self.power_sources.keys(): - pv_gen = self.power_sources['pv'].dispatch.available_generation + tot_gen = [0.0] * self.options.n_look_ahead_periods + if "pv" in self.power_sources.keys(): + pv_gen = self.power_sources["pv"].dispatch.available_generation tot_gen = [pv + gen for pv, gen in zip(pv_gen, tot_gen)] - if 'wind' in self.power_sources.keys(): - wind_gen = self.power_sources['wind'].dispatch.available_generation + if "wind" in self.power_sources.keys(): + wind_gen = self.power_sources["wind"].dispatch.available_generation tot_gen = [wind + gen for wind, gen in zip(wind_gen, tot_gen)] - grid_limit = self.power_sources['grid'].dispatch.generation_transmission_limit + grid_limit = self.power_sources["grid"].dispatch.generation_transmission_limit - if 'one_cycle' in self.options.battery_dispatch: + if "one_cycle" in self.options.battery_dispatch: # Get prices for one cycle heuristic - prices = self.power_sources['grid'].dispatch.electricity_sell_price - self.power_sources['battery'].dispatch.prices = prices + prices = self.power_sources["grid"].dispatch.electricity_sell_price + self.power_sources["battery"].dispatch.prices = prices - if 'load_following' in self.options.battery_dispatch: + if "load_following" in self.options.battery_dispatch: # TODO: Look into how to define a system as load following or not in the config file - required_keys = ['desired_load'] + required_keys = ["desired_load"] if self.site.follow_desired_schedule: # Get difference between baseload demand and power generation and control scenario variables load_value = self.site.desired_schedule - load_difference = [(load_value[x] - tot_gen[x]) for x in range(len(tot_gen))] - self.power_sources['battery'].dispatch.load_difference = load_difference + load_difference = [ + (load_value[x] - tot_gen[x]) for x in range(len(tot_gen)) + ] + self.power_sources["battery"].dispatch.load_difference = load_difference else: - raise ValueError(type(self).__name__ + " requires the following : desired_schedule") - # Adding goal_power for the simple battery heuristic method for power setpoint tracking - goal_power = [load_value]*self.options.n_look_ahead_periods + raise ValueError( + type(self).__name__ + " requires the following : desired_schedule" + ) + # Adding goal_power for the simple battery heuristic method for power setpoint tracking + goal_power = [load_value] * self.options.n_look_ahead_periods ### Note: the inputs grid_limit and goal_power are in MW ### - self.power_sources['battery'].dispatch.set_fixed_dispatch(tot_gen, grid_limit, load_value) + self.power_sources["battery"].dispatch.set_fixed_dispatch( + tot_gen, grid_limit, load_value + ) else: - self.power_sources['battery'].dispatch.set_fixed_dispatch(tot_gen, grid_limit) + self.power_sources["battery"].dispatch.set_fixed_dispatch( + tot_gen, grid_limit + ) @property def pyomo_model(self) -> pyomo.ConcreteModel: @@ -534,15 +721,23 @@ def pyomo_model(self) -> pyomo.ConcreteModel: def dispatch(self) -> HybridDispatch: return self._dispatch + class SolverOptions: """Class for housing solver options""" - def __init__(self, solver_spec_options: dict, log_name: str="", user_solver_options: dict = None, solver_spec_log_key: str="logfile"): + + def __init__( + self, + solver_spec_options: dict, + log_name: str = "", + user_solver_options: dict = None, + solver_spec_log_key: str = "logfile", + ): self.instance_log = "dispatch_solver.log" self.solver_spec_options = solver_spec_options self.user_solver_options = user_solver_options - + self.constructed = solver_spec_options if log_name != "": self.constructed[solver_spec_log_key] = self.instance_log if user_solver_options is not None: - self.constructed.update(user_solver_options) \ No newline at end of file + self.constructed.update(user_solver_options) diff --git a/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py b/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py index 69c26f1da..d19a78e72 100644 --- a/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py +++ b/hopp/simulation/technologies/dispatch/hybrid_dispatch_options.py @@ -15,7 +15,7 @@ class HybridDispatchOptions: Class for setting dispatch options through HybridSimulation class. Args: - dispatch_options (dict): Contains attribute key-value pairs to change default options. + dispatch_options (dict): Contains attribute key-value pairs to change default options. - **solver** (str, default='cbc'): MILP solver used for dispatch optimization problem. Options are `('glpk', 'cbc', 'xpress', 'xpress_persistent', 'gurobi_ampl', 'gurobi')`. @@ -55,22 +55,27 @@ class HybridDispatchOptions: - **use_higher_hours** bool (default = False): if True, the simulation will run extra hours analysis (must be used with load following) - - **higher_hours** (dict, default = {}): Higher hour count parameters: the value of power that must be available above the schedule and the number of hours in a row + - **higher_hours** (dict, default = {}): Higher hour count parameters: the value of power that must be available above the schedule and the number of hours in a row """ + def __init__(self, dispatch_options: dict = None): - self.solver: str = 'cbc' - self.solver_options: dict = {} # used to update solver options, look at specific solver for option names - self.battery_dispatch: str = 'simple' + self.solver: str = "cbc" + self.solver_options: dict = ( + {} + ) # used to update solver options, look at specific solver for option names + self.battery_dispatch: str = "simple" self.include_lifecycle_count: bool = True - self.lifecycle_cost_per_kWh_cycle: float = 0.0265 # Estimated using SAM output (lithium-ion battery) + self.lifecycle_cost_per_kWh_cycle: float = ( + 0.0265 # Estimated using SAM output (lithium-ion battery) + ) self.max_lifecycle_per_day: int = np.inf self.grid_charging: bool = True self.pv_charging_only: bool = False self.n_look_ahead_periods: int = 48 self.time_weighting_factor: float = 0.995 self.n_roll_periods: int = 24 - self.log_name: str = '' # NOTE: Logging is not thread safe + self.log_name: str = "" # NOTE: Logging is not thread safe self.is_test_start_year: bool = False self.is_test_end_year: bool = False @@ -92,30 +97,45 @@ def __init__(self, dispatch_options: dict = None): value = type(getattr(self, key))(value) setattr(self, key, value) except: - raise ValueError("'{}' is the wrong data type. Should be {}".format(key, type(getattr(self, key)))) + raise ValueError( + "'{}' is the wrong data type. Should be {}".format( + key, type(getattr(self, key)) + ) + ) else: - raise NameError("'{}' is not an attribute in {}".format(key, type(self).__name__)) + raise NameError( + "'{}' is not an attribute in {}".format( + key, type(self).__name__ + ) + ) if self.is_test_start_year and self.is_test_end_year: - print('WARNING: Dispatch optimization START and END of year testing is enabled!') + print( + "WARNING: Dispatch optimization START and END of year testing is enabled!" + ) elif self.is_test_start_year: - print('WARNING: Dispatch optimization START of year testing is enabled!') + print("WARNING: Dispatch optimization START of year testing is enabled!") elif self.is_test_end_year: - print('WARNING: Dispatch optimization END of year testing is enabled!') + print("WARNING: Dispatch optimization END of year testing is enabled!") if self.pv_charging_only and self.grid_charging: - raise ValueError("Battery cannot be restricted to charge from PV only if grid_charging is enabled") + raise ValueError( + "Battery cannot be restricted to charge from PV only if grid_charging is enabled" + ) self._battery_dispatch_model_options = { - 'one_cycle_heuristic': OneCycleBatteryDispatchHeuristic, - 'heuristic': SimpleBatteryDispatchHeuristic, - 'simple': SimpleBatteryDispatch, - 'non_convex_LV': NonConvexLinearVoltageBatteryDispatch, - 'convex_LV': ConvexLinearVoltageBatteryDispatch, - 'load_following_heuristic': HeuristicLoadFollowingDispatch} + "one_cycle_heuristic": OneCycleBatteryDispatchHeuristic, + "heuristic": SimpleBatteryDispatchHeuristic, + "simple": SimpleBatteryDispatch, + "non_convex_LV": NonConvexLinearVoltageBatteryDispatch, + "convex_LV": ConvexLinearVoltageBatteryDispatch, + "load_following_heuristic": HeuristicLoadFollowingDispatch, + } if self.battery_dispatch in self._battery_dispatch_model_options: - self.battery_dispatch_class = self._battery_dispatch_model_options[self.battery_dispatch] - if 'heuristic' in self.battery_dispatch: + self.battery_dispatch_class = self._battery_dispatch_model_options[ + self.battery_dispatch + ] + if "heuristic" in self.battery_dispatch: # FIXME: This should be set to the number of time steps within a day. # Dispatch time duration is not set as of now... self.n_roll_periods = 24 @@ -123,4 +143,8 @@ def __init__(self, dispatch_options: dict = None): # dispatch cycle counting is not available in heuristics self.include_lifecycle_count = False else: - raise ValueError("'{}' is not currently a battery dispatch class.".format(self.battery_dispatch)) + raise ValueError( + "'{}' is not currently a battery dispatch class.".format( + self.battery_dispatch + ) + ) diff --git a/hopp/simulation/technologies/dispatch/power_sources/__init__.py b/hopp/simulation/technologies/dispatch/power_sources/__init__.py index 308b46c51..ea7bd3306 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/__init__.py +++ b/hopp/simulation/technologies/dispatch/power_sources/__init__.py @@ -1,4 +1,10 @@ -from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import PowerSourceDispatch +from hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch import ( + PowerSourceDispatch, +) from hopp.simulation.technologies.dispatch.power_sources.pv_dispatch import PvDispatch -from hopp.simulation.technologies.dispatch.power_sources.wind_dispatch import WindDispatch -from hopp.simulation.technologies.dispatch.power_sources.wave_dispatch import WaveDispatch +from hopp.simulation.technologies.dispatch.power_sources.wind_dispatch import ( + WindDispatch, +) +from hopp.simulation.technologies.dispatch.power_sources.wave_dispatch import ( + WaveDispatch, +) diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 84fda34c6..34f8b3cd0 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -72,7 +72,7 @@ def max_gross_profit_objective(self, hybrid_blocks): """PV instance of maximum gross profit objective. Args: - hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( @@ -89,7 +89,7 @@ def min_operating_cost_objective(self, hybrid_blocks): """PV instance of minimum operating cost objective. Args: - hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index 4f5cd3071..34b9f0feb 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -85,7 +85,7 @@ def max_gross_profit_objective(self, hybrid_blocks): """Tower CSP instance of maximum gross profit objective. Args: - hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( @@ -123,7 +123,7 @@ def min_operating_cost_objective(self, hybrid_blocks): """Tower CSP instance of minimum operating cost objective. Args: - hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index c18312c7b..b7753c65a 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -67,7 +67,7 @@ def max_gross_profit_objective(self, hybrid_blocks): """Trough CSP instance of maximum gross profit objective. Args: - hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = Expression( @@ -105,7 +105,7 @@ def min_operating_cost_objective(self, hybrid_blocks): """Trough CSP instance of minimum operating cost objective. Args: - hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical + hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ self.obj = sum( diff --git a/hopp/simulation/technologies/dispatch/power_storage/__init__.py b/hopp/simulation/technologies/dispatch/power_storage/__init__.py index 73592b774..3bd3d34b4 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/__init__.py +++ b/hopp/simulation/technologies/dispatch/power_storage/__init__.py @@ -1,7 +1,21 @@ -from hopp.simulation.technologies.dispatch.power_storage.linear_voltage_convex_battery_dispatch import ConvexLinearVoltageBatteryDispatch -from hopp.simulation.technologies.dispatch.power_storage.linear_voltage_nonconvex_battery_dispatch import NonConvexLinearVoltageBatteryDispatch -from hopp.simulation.technologies.dispatch.power_storage.one_cycle_battery_dispatch_heuristic import OneCycleBatteryDispatchHeuristic -from hopp.simulation.technologies.dispatch.power_storage.power_storage_dispatch import PowerStorageDispatch -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import SimpleBatteryDispatch -from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import SimpleBatteryDispatchHeuristic -from hopp.simulation.technologies.dispatch.power_storage.heuristic_load_following_dispatch import HeuristicLoadFollowingDispatch +from hopp.simulation.technologies.dispatch.power_storage.linear_voltage_convex_battery_dispatch import ( + ConvexLinearVoltageBatteryDispatch, +) +from hopp.simulation.technologies.dispatch.power_storage.linear_voltage_nonconvex_battery_dispatch import ( + NonConvexLinearVoltageBatteryDispatch, +) +from hopp.simulation.technologies.dispatch.power_storage.one_cycle_battery_dispatch_heuristic import ( + OneCycleBatteryDispatchHeuristic, +) +from hopp.simulation.technologies.dispatch.power_storage.power_storage_dispatch import ( + PowerStorageDispatch, +) +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch import ( + SimpleBatteryDispatch, +) +from hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic import ( + SimpleBatteryDispatchHeuristic, +) +from hopp.simulation.technologies.dispatch.power_storage.heuristic_load_following_dispatch import ( + HeuristicLoadFollowingDispatch, +) diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 73918b09c..09c5dbf25 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -85,15 +85,15 @@ def max_gross_profit_objective(self, hybrid_blocks): hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. """ + def battery_profit_objective_rule(m): objective = 0 objective += sum( - - (1/hybrid_blocks[t].time_weighting_factor) + -(1 / hybrid_blocks[t].time_weighting_factor) * self.blocks[t].time_duration * ( - self.blocks[t].cost_per_charge - * hybrid_blocks[t].battery_charge - + self.blocks[t].cost_per_discharge + self.blocks[t].cost_per_charge * hybrid_blocks[t].battery_charge + + self.blocks[t].cost_per_discharge * hybrid_blocks[t].battery_discharge ) for t in hybrid_blocks.index_set() @@ -115,11 +115,9 @@ def min_operating_cost_objective(self, hybrid_blocks): hybrid_blocks[t].time_weighting_factor * self.blocks[t].time_duration * ( - self.blocks[t].cost_per_discharge - * hybrid_blocks[t].battery_discharge - - self.blocks[t].cost_per_charge - * hybrid_blocks[t].battery_charge - ) # Try to incentivize battery charging + self.blocks[t].cost_per_discharge * hybrid_blocks[t].battery_discharge + - self.blocks[t].cost_per_charge * hybrid_blocks[t].battery_charge + ) # Try to incentivize battery charging for t in self.blocks.index_set() ) if self.options.include_lifecycle_count: From 6f77700b7c16d0841389588b2ab6a1ffbcefead2 Mon Sep 17 00:00:00 2001 From: kbrunik Date: Thu, 18 Apr 2024 08:55:09 -0700 Subject: [PATCH 25/27] remove setter docstring --- .../power_storage/simple_battery_dispatch_heuristic.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index 7d90a8c52..f858bc0ef 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -237,16 +237,6 @@ def user_fixed_dispatch(self) -> list: @user_fixed_dispatch.setter def user_fixed_dispatch(self, fixed_dispatch: list): - """ - Setter for user fixed dispatch. - - Args: - fixed_dispatch (list): List of user fixed dispatch. - - Raises: - ValueError: If fixed_dispatch length does not match dispatch index set or if values are not between -1 and 1. - - """ # TODO: Annual dispatch array... if len(fixed_dispatch) != len(self.blocks.index_set()): raise ValueError( From b63c22cbbf41a01f8ecda9786611ee303c0b9ea0 Mon Sep 17 00:00:00 2001 From: bayc Date: Wed, 17 Apr 2024 14:16:28 -0600 Subject: [PATCH 26/27] add dispatch to docs --- .../hopp/simulation/technologies/dispatch.rst | 50 ++++++++++++ .../dispatch/power_sources/csp_dispatch.rst | 10 +++ .../power_sources/power_source_dispatch.rst | 10 +++ .../dispatch/power_sources/pv_dispatch.rst | 10 +++ .../dispatch/power_sources/tower_dispatch.rst | 10 +++ .../power_sources/trough_dispatch.rst | 10 +++ .../dispatch/power_sources/wave_dispatch.rst | 10 +++ .../dispatch/power_sources/wind_dispatch.rst | 10 +++ .../heuristic_load_following_dispatch.rst | 10 +++ ...linear_voltage_convex_battery_dispatch.rst | 10 +++ ...ear_voltage_nonconvex_battery_dispatch.rst | 10 +++ .../one_cycle_battery_dispatch_heuristic.rst | 10 +++ .../power_storage/power_storage_dispatch.rst | 10 +++ .../power_storage/simple_battery_dispatch.rst | 10 +++ .../simple_battery_dispatch_heuristic.rst | 10 +++ docs/index.rst | 2 +- .../dispatch/power_sources/csp_dispatch.py | 80 +++++++++---------- .../power_sources/power_source_dispatch.py | 48 ++++------- .../dispatch/power_sources/pv_dispatch.py | 33 +++----- .../dispatch/power_sources/tower_dispatch.py | 29 ++----- .../dispatch/power_sources/trough_dispatch.py | 27 ++----- .../dispatch/power_sources/wave_dispatch.py | 28 ++----- .../dispatch/power_sources/wind_dispatch.py | 28 ++----- .../heuristic_load_following_dispatch.py | 13 +-- .../linear_voltage_convex_battery_dispatch.py | 26 +++--- ...near_voltage_nonconvex_battery_dispatch.py | 23 +++--- .../one_cycle_battery_dispatch_heuristic.py | 55 +++++-------- .../power_storage/power_storage_dispatch.py | 36 ++++----- .../power_storage/simple_battery_dispatch.py | 29 ++----- .../simple_battery_dispatch_heuristic.py | 34 ++------ 30 files changed, 369 insertions(+), 312 deletions(-) create mode 100644 docs/hopp/simulation/technologies/dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.rst create mode 100644 docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.rst diff --git a/docs/hopp/simulation/technologies/dispatch.rst b/docs/hopp/simulation/technologies/dispatch.rst new file mode 100644 index 000000000..8f3849acb --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch.rst @@ -0,0 +1,50 @@ +.. _Dispatch: + +Dispatch Strategies +=================== + +These are the dispatch strategies that may be used for a standard HOPP simulation. Dispatch +settings can be defined through :class:`.HybridDispatchOptions`. + +Storage Dispatch +---------------- + +.. toctree:: + :maxdepth: 1 + + dispatch/power_storage/simple_battery_dispatch.rst + dispatch/power_storage/simple_battery_dispatch_heuristic.rst + dispatch/power_storage/heuristic_load_following_dispatch.rst + dispatch/power_storage/linear_voltage_convex_battery_dispatch.rst + dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.rst + dispatch/power_storage/one_cycle_battery_dispatch_heuristic.rst + +The above dispatch classes inherit from the :py:class:`.PowerStorageDispatch` class. + +.. toctree:: + :maxdepth: 1 + + dispatch/power_storage/power_storage_dispatch.rst + +Technology Dispatch +------------------- + +Dispatch classes are made for each technology where their specific components of the objectives, +their parameters, and other technology specific dispatch properties are defined. + +.. toctree:: + :maxdepth: 1 + + dispatch/power_sources/pv_dispatch.rst + dispatch/power_sources/wind_dispatch.rst + dispatch/power_sources/wave_dispatch.rst + dispatch/power_sources/trough_dispatch.rst + dispatch/power_sources/tower_dispatch.rst + dispatch/power_sources/csp_dispatch.rst + +The above technology classes inherit from the :py:class:`.PowerSourceDispatch` class. + +.. toctree:: + :maxdepth: 1 + + dispatch/power_sources/power_source_dispatch.rst diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.rst new file mode 100644 index 000000000..7907cb175 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.rst @@ -0,0 +1,10 @@ +.. _CSPDispatch: + + +CSP Dispatch +============ + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.csp_dispatch.CspDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.rst new file mode 100644 index 000000000..d5b6c623c --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.rst @@ -0,0 +1,10 @@ +.. _PowerSourceDispatch: + + +Power Source Dispatch +===================== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.power_source_dispatch.PowerSourceDispatch + :members: \ No newline at end of file diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.rst new file mode 100644 index 000000000..d95496a16 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.rst @@ -0,0 +1,10 @@ +.. _PVDispatch: + + +PV Dispatch +=========== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.pv_dispatch.PvDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.rst new file mode 100644 index 000000000..fd51886ac --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.rst @@ -0,0 +1,10 @@ +.. _TowerDispatch: + + +Tower Dispatch +============== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.tower_dispatch.TowerDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.rst new file mode 100644 index 000000000..1047d8245 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.rst @@ -0,0 +1,10 @@ +.. _TroughDispatch: + + +Trough Dispatch +=============== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.trough_dispatch.TroughDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.rst new file mode 100644 index 000000000..02edf00d9 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.rst @@ -0,0 +1,10 @@ +.. _WaveDispatch: + + +Wave Dispatch +============= + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.wave_dispatch.WaveDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.rst new file mode 100644 index 000000000..9d5d97bcd --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.rst @@ -0,0 +1,10 @@ +.. _WindDispatch: + + +Wind Dispatch +============= + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_sources.wind_dispatch.WindDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.rst new file mode 100644 index 000000000..6adec3df9 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.rst @@ -0,0 +1,10 @@ +.. _HeuristicLoadFollowingDispatch: + + +Heuristic Load Following Dispatch +================================= + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.heuristic_load_following_dispatch.HeuristicLoadFollowingDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.rst new file mode 100644 index 000000000..63fc049b8 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.rst @@ -0,0 +1,10 @@ +.. _ConvexLinearVoltageBatteryDispatch: + + +Convex Linear Voltage Battery Dispatch +====================================== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.linear_voltage_convex_battery_dispatch.ConvexLinearVoltageBatteryDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.rst new file mode 100644 index 000000000..8c9fb82c1 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.rst @@ -0,0 +1,10 @@ +.. _NonConvexLinearVoltageBatteryDispatch: + + +Non-Convex Linear Voltage Battery Dispatch +========================================== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.linear_voltage_nonconvex_battery_dispatch.NonConvexLinearVoltageBatteryDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.rst new file mode 100644 index 000000000..c9112d682 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.rst @@ -0,0 +1,10 @@ +.. _OneCycleBatteryDispatchHeuristic: + + +One Cycle Battery Dispatch Heuristic +==================================== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.one_cycle_battery_dispatch_heuristic.OneCycleBatteryDispatchHeuristic + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.rst new file mode 100644 index 000000000..c34dab5d3 --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.rst @@ -0,0 +1,10 @@ +.. _PowerStorageDispatch: + + +Power Storage Dispatch +====================== + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.power_storage_dispatch.PowerStorageDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.rst new file mode 100644 index 000000000..a0e1be9bb --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.rst @@ -0,0 +1,10 @@ +.. _SimpleBatteryDispatch: + + +Simple Battery Dispatch +======================= + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch.SimpleBatteryDispatch + :members: diff --git a/docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.rst b/docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.rst new file mode 100644 index 000000000..32cce175b --- /dev/null +++ b/docs/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.rst @@ -0,0 +1,10 @@ +.. _SimpleBatteryDispatchHeuristic: + + +Simple Battery Dispatch Heuristic +================================= + +.. toctree:: + +.. autoclass:: hopp.simulation.technologies.dispatch.power_storage.simple_battery_dispatch_heuristic.SimpleBatteryDispatchHeuristic + :members: diff --git a/docs/index.rst b/docs/index.rst index bddf1a085..3562f52d1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ Welcome to HOPP's documentation! hopp/simulation/hopp_interface.rst hopp/simulation/technologies/sites/site_info.rst hopp/simulation/technologies/technologies.rst - + hopp/simulation/technologies/dispatch.rst .. toctree:: :maxdepth: 1 diff --git a/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py index fa323594d..4977227cb 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/csp_dispatch.py @@ -9,9 +9,7 @@ class CspDispatch(Dispatch): - """ - Dispatch model for Concentrating Solar Power (CSP) with thermal energy storage. - """ + """Dispatch model for Concentrating Solar Power (CSP) with thermal energy storage.""" def __init__( self, @@ -21,8 +19,7 @@ def __init__( financial_model, block_set_name: str = "csp", ): - """ - Initialize a CSP dispatch model. + """Initialize a CSP dispatch model. Args: pyomo_model (pyomo.ConcreteModel): Pyomo model instance. @@ -30,6 +27,7 @@ def __init__( system_model: System model. financial_model: Financial model. block_set_name (str, optional): Name of the block. Defaults to 'csp'. + """ super().__init__( pyomo_model, @@ -49,11 +47,11 @@ def __init__( } def dispatch_block_rule(self, csp): - """ - Called during Dispatch's __init__. Define dispatch block rules. + """Called during Dispatch's __init__. Define dispatch block rules. Args: csp: CSP instance. + """ # Parameters self._create_storage_parameters(csp) @@ -77,11 +75,11 @@ def dispatch_block_rule(self, csp): @staticmethod def _create_storage_parameters(csp): - """ - Create parameters related to thermal energy storage. + """Create parameters related to thermal energy storage. Args: csp: CSP instance. + """ csp.time_duration = pyomo.Param( @@ -101,11 +99,11 @@ def _create_storage_parameters(csp): @staticmethod def _create_receiver_parameters(csp): - """ - Create parameters related to CSP receiver. + """Create parameters related to CSP receiver. Args: csp: CSP instance. + """ # Cost Parameters csp.cost_per_field_generation = pyomo.Param( @@ -195,11 +193,11 @@ def _create_receiver_parameters(csp): @staticmethod def _create_cycle_parameters(csp): - """ - Create parameters related to the power cycle. + """Create parameters related to the power cycle. Args: csp: CSP instance. + """ # Cost parameters csp.cost_per_cycle_generation = pyomo.Param( @@ -306,11 +304,11 @@ def _create_cycle_parameters(csp): @staticmethod def _create_storage_variables(csp): - """ - Create variables related to thermal energy storage. + """Create variables related to thermal energy storage. Args: csp: CSP instance. + """ csp.thermal_energy_storage = pyomo.Var( doc="Thermal energy storage reserve quantity [MWht]", @@ -328,11 +326,11 @@ def _create_storage_variables(csp): @staticmethod def _create_receiver_variables(csp): - """ - Create variables related to the receiver. + """Create variables related to the receiver. Args: csp: CSP instance. + """ csp.receiver_startup_inventory = pyomo.Var( doc="Receiver start-up energy inventory [MWht]", @@ -383,11 +381,11 @@ def _create_receiver_variables(csp): @staticmethod def _create_cycle_variables(csp): - """ - Create variables related to the power cycle. + """Create variables related to the power cycle. Args: csp: CSP instance. + """ csp.system_load = pyomo.Var( doc="Load of csp system [MWe]", domain=pyomo.NonNegativeReals, units=u.MW @@ -458,11 +456,11 @@ def _create_cycle_variables(csp): @staticmethod def _create_storage_constraints(csp): - """ - Create constraints related to thermal energy storage. + """Create constraints related to thermal energy storage. Args: csp: CSP instance. + """ csp.storage_inventory = pyomo.Constraint( doc="Thermal energy storage energy balance", @@ -500,11 +498,11 @@ def _create_storage_constraints(csp): @staticmethod def _create_receiver_constraints(csp): - """ - Create constraints related to the receiver. + """Create constraints related to the receiver. Args: csp: CSP instance. + """ # Start-up csp.receiver_startup_inventory_balance = pyomo.Constraint( @@ -570,11 +568,11 @@ def _create_receiver_constraints(csp): @staticmethod def _create_cycle_constraints(csp): - """ - Create constraints related to the power cycle. + """Create constraints related to the power cycle. Args: csp: CSP instance. + """ # Start-up csp.cycle_startup_inventory_balance = pyomo.Constraint( @@ -680,11 +678,11 @@ def _create_cycle_constraints(csp): @staticmethod def _create_csp_port(csp): - """ - Create pyomo ports related to CSP instance. + """Create pyomo ports related to CSP instance. Args: csp: CSP instance. + """ csp.port = Port() csp.port.add(csp.cycle_generation) @@ -971,8 +969,7 @@ def initialize_parameters(self): self.set_part_load_cycle_parameters() def update_time_series_parameters(self, start_time: int): - """ - Sets up SSC simulation to get time series performance parameters after simulation. + """Sets up SSC simulation to get time series performance parameters after simulation. Args: start_time (int): Hour of the year starting dispatch horizon. @@ -1047,6 +1044,7 @@ def set_linearized_cycle_part_load_params(self, norm_heat_pts, efficiency_pts): Args: norm_heat_pts (list): Normalized heat points for the power cycle. efficiency_pts (list): Efficiency points for the power cycle. + """ q_pb_design = self._system_model.cycle_thermal_rating fpts = [ @@ -1077,8 +1075,7 @@ def set_linearized_cycle_part_load_params(self, norm_heat_pts, efficiency_pts): return def set_ambient_temperature_cycle_parameters(self, dry_bulb_temperature): - """ - Set ambient temperature dependent cycle performance parameters. + """Set ambient temperature dependent cycle performance parameters. Args: dry_bulb_temperature (float or list): Ambient dry bulb temperature(s) [°C]. @@ -1090,6 +1087,7 @@ def set_ambient_temperature_cycle_parameters(self, dry_bulb_temperature): This method sets up ambient temperature dependent cycle performance parameters such as cycle efficiency corrections and condenser losses based on the provided dry bulb temperature(s). + """ # --- Cycle ambient-temperature efficiency corrections tables = self._system_model.cycle_efficiency_tables @@ -1139,8 +1137,7 @@ def set_ambient_temperature_cycle_parameters(self, dry_bulb_temperature): return def set_cycle_ambient_corrections(self, Tdb, Tpts, etapts, wcondfpts): - """ - Set cycle ambient corrections based on ambient temperature. + """Set cycle ambient corrections based on ambient temperature. Args: Tdb (float or list): Ambient temperature(s) for each dispatch time step [°C]. @@ -1176,8 +1173,7 @@ def set_cycle_ambient_corrections(self, Tdb, Tpts, etapts, wcondfpts): @staticmethod def interpret_user_defined_cycle_data(ud_ind_od): - """ - Interpret user-defined cycle data. + """Interpret user-defined cycle data. Args: ud_ind_od (list): User-defined cycle data. @@ -1198,6 +1194,7 @@ def interpret_user_defined_cycle_data(ud_ind_od): This method interprets user-defined cycle data and organizes it into a dictionary containing relevant information about temperature points, mass flow rate points, and ambient temperature points. + """ data = np.array(ud_ind_od) @@ -1230,8 +1227,7 @@ def interpret_user_defined_cycle_data(ud_ind_od): } def set_receiver_require_startup_time_fraction(self, field_gen: list): - """ - Estimates the fraction of time period required for receiver start-up. + """Estimates the fraction of time period required for receiver start-up. Args: field_gen (list): Field generation profile. @@ -1263,6 +1259,7 @@ def update_initial_conditions(self): """This method updates the initial conditions for the dispatch optimization, including the initial thermal energy storage, initial cycle startup inventory, and initial cycle thermal power. + """ csp = self._system_model @@ -1332,6 +1329,7 @@ def get_start_end_datetime(start_time: int, n_horizon: int): Notes: This method calculates the start and end datetimes based on the provided start time and horizon length, assuming hourly data. + """ # Setting simulation times start_datetime = CspDispatch.get_start_datetime_by_hour(start_time) @@ -1344,8 +1342,7 @@ def get_start_end_datetime(start_time: int, n_horizon: int): @staticmethod def get_start_datetime_by_hour(start_time: int): - """ - Get the datetime object corresponding to the start time of year in hours. + """Get the datetime object corresponding to the start time of year in hours. Args: start_time (int): Start time of the simulation in hours. @@ -1356,6 +1353,7 @@ def get_start_datetime_by_hour(start_time: int): Notes: This method calculates the datetime object corresponding to the start time in hours relative to the beginning of the year. + """ # TODO: bring in the correct year from site data - or replace outside of function? beginning_of_year = datetime.datetime(2009, 1, 1, 0) @@ -1363,8 +1361,7 @@ def get_start_datetime_by_hour(start_time: int): @staticmethod def seconds_since_newyear(dt): - """ - Get the number of seconds elapsed since the beginning of the year. + """Get the number of seconds elapsed since the beginning of the year. Args: dt (datetime.datetime): Datetime object. @@ -1375,6 +1372,7 @@ def seconds_since_newyear(dt): Notes: This method calculates the number of seconds elapsed since the beginning of the year, using a non-leap year (2009) for consistency with a multiple of 8760 hours assumption. + """ # Substitute a non-leap year (2009) to keep multiple of 8760 assumption: newyear = datetime.datetime(2009, 1, 1, 0, 0, 0, 0) diff --git a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py index 275e6d919..c1dd1d3fc 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/power_source_dispatch.py @@ -6,28 +6,7 @@ class PowerSourceDispatch(Dispatch): - """ - Dispatch optimization model for power sources. - - Attributes: - pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. - index_set (pyomo.Set): Index set. - system_model: System model. - financial_model: Financial model. - block_set_name (str): Name of the block set. - - Methods: - dispatch_block_rule(gen): Dispatch block rule method. - initialize_parameters(): Initialize parameters method. - update_time_series_parameters(start_time): Update time series parameters method. - _create_variables(hybrid): Create variables method (abstract). - _create_port(hybrid): Create port method (abstract). - - Properties: - cost_per_generation: Cost per generation property. - available_generation: Available generation property. - generation: Generation property. - """ + """Dispatch optimization model for power sources.""" def __init__( self, @@ -37,8 +16,7 @@ def __init__( financial_model, block_set_name: str = "generator", ): - """ - Initialize PowerSourceDispatch. + """Initialize PowerSourceDispatch. Args: pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. @@ -46,6 +24,7 @@ def __init__( system_model: System model. financial_model: Financial model. block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, @@ -57,14 +36,14 @@ def __init__( @staticmethod def dispatch_block_rule(gen): - """ - Dispatch block rule method. + """Dispatch block rule method. Args: gen: Generator. Returns: None + """ ################################## # Parameters # @@ -115,14 +94,14 @@ def initialize_parameters(self): ) def update_time_series_parameters(self, start_time: int): - """ - Update time series parameters method. + """Update time series parameters method. Args: start_time (int): Start time. Returns: None + """ n_horizon = len(self.blocks.index_set()) generation = self._system_model.value("gen") @@ -141,8 +120,7 @@ def update_time_series_parameters(self, start_time: int): self.available_generation = [gen_kw / 1e3 for gen_kw in horizon_gen] def _create_variables(self, hybrid): - """ - Create variables method (abstract). + """Create variables method (abstract). Args: hybrid: hybrid plant instance to which individual technology is added. @@ -152,14 +130,14 @@ def _create_variables(self, hybrid): Raises: NotImplemented: Must be overridden in specific technology models. + """ raise NotImplemented( "This function must be overridden for specific dispatch model" ) def _create_port(self, hybrid): - """ - Create port method (abstract). + """Create port method (abstract). Args: hybrid: Hybrid. @@ -169,6 +147,7 @@ def _create_port(self, hybrid): Raises: NotImplemented: Must be overridden in specific technology models. + """ raise NotImplemented( "This function must be overridden for specific dispatch model" @@ -189,11 +168,11 @@ def cost_per_generation(self, om_dollar_per_mwh: float): @property def available_generation(self) -> list: - """ - Available generation. + """Available generation. Returns: list: List of available generation. + """ return [ self.blocks[t].available_generation.value for t in self.blocks.index_set() @@ -217,6 +196,7 @@ def generation(self) -> list: Returns: list: List of generation. + """ return [ round(self.blocks[t].generation.value, self.round_digits) diff --git a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py index 34f8b3cd0..f368fedcc 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/pv_dispatch.py @@ -13,21 +13,7 @@ class PvDispatch(PowerSourceDispatch): pv_obj: Union[Expression, float] _system_model: Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8] _financial_model: FinancialModelType - """ - Dispatch optimization model for photovoltaic (PV) systems. - - Attributes: - pv_obj: PV object. - _system_model: System model. - _financial_model: Financial model. - - Methods: - update_time_series_parameters(start_time): Update time series parameters method. - max_gross_profit_objective(blocks): Maximum gross profit objective method. - min_operating_cost_objective(blocks): Minimum operating cost objective method. - _create_variables(hybrid): Create variables method. - _create_port(hybrid): Create port method. - """ + """Dispatch optimization model for photovoltaic (PV) systems.""" def __init__( self, @@ -37,8 +23,7 @@ def __init__( financial_model: FinancialModelType, block_set_name: str = "pv", ): - """ - Initialize PvDispatch. + """Initialize PvDispatch. Args: pyomo_model (ConcreteModel): Pyomo concrete model. @@ -46,6 +31,7 @@ def __init__( system_model (Union[Pvsam.Pvsamv1, Pvwatts.Pvwattsv8]): System model. financial_model (FinancialModelType): Financial model. block_set_name (str): Name of the block set. + """ super().__init__( @@ -57,11 +43,11 @@ def __init__( ) def update_time_series_parameters(self, start_time: int): - """ - Update time series parameters method. + """Update time series parameters method. Args: start_time (int): Start time. + """ super().update_time_series_parameters(start_time) @@ -74,6 +60,7 @@ def max_gross_profit_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( @@ -91,6 +78,7 @@ def min_operating_cost_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = sum( hybrid_blocks[t].time_weighting_factor @@ -101,8 +89,7 @@ def min_operating_cost_objective(self, hybrid_blocks): ) def _create_variables(self, hybrid): - """ - Create PV variables to add to hybrid plant instance. + """Create PV variables to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. @@ -122,14 +109,14 @@ def _create_variables(self, hybrid): return hybrid.pv_generation, 0 def _create_port(self, hybrid): - """ - Create pv port to add to hybrid plant instance. + """Create pv port to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. Returns: Port: PV Port object. + """ hybrid.pv_port = Port(initialize={"generation": hybrid.pv_generation}) return hybrid.pv_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py index 34b9f0feb..4edf02ab3 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/tower_dispatch.py @@ -10,21 +10,7 @@ class TowerDispatch(CspDispatch): tower_obj: Union[Expression, float] _system_model: None _financial_model: FinancialModelType - """ - Dispatch optimization model for CSP tower systems. - - Attributes: - tower_obj: Tower object. - _system_model: System model. - _financial_model: Financial model. - - Methods: - update_initial_conditions(): Update initial conditions method. - max_gross_profit_objective(blocks): Maximum gross profit objective method. - min_operating_cost_objective(blocks): Minimum operating cost objective method. - _create_variables(hybrid): Create variables method. - _create_port(hybrid): Create port method. - """ + """Dispatch optimization model for CSP tower systems.""" def __init__( self, @@ -34,8 +20,7 @@ def __init__( financial_model: FinancialModelType, block_set_name: str = "tower", ): - """ - Initialize TowerDispatch. + """Initialize TowerDispatch. Args: pyomo_model (ConcreteModel): Pyomo concrete model. @@ -43,6 +28,7 @@ def __init__( system_model (None): System model. financial_model (FinancialModelType): Financial model. block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, @@ -87,6 +73,7 @@ def max_gross_profit_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( @@ -125,6 +112,7 @@ def min_operating_cost_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = sum( hybrid_blocks[t].time_weighting_factor @@ -148,8 +136,7 @@ def min_operating_cost_objective(self, hybrid_blocks): ) def _create_variables(self, hybrid): - """ - Create Tower CSP variables to add to hybrid plant instance. + """Create Tower CSP variables to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. @@ -175,14 +162,14 @@ def _create_variables(self, hybrid): return hybrid.tower_generation, hybrid.tower_load def _create_port(self, hybrid): - """ - Create CSP tower port to add to hybrid plant instance. + """Create CSP tower port to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. Returns: Port: CSP Tower Port object. + """ hybrid.tower_port = Port( initialize={ diff --git a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py index b7753c65a..a018da01c 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/trough_dispatch.py @@ -10,21 +10,7 @@ class TroughDispatch(CspDispatch): trough_obj: Union[Expression, float] _system_model: None _financial_model: FinancialModelType - """ - Dispatch optimization model for CSP trough systems. - - Attributes: - trough_obj: Trough object. - _system_model: System model. - _financial_model: Financial model. - - Methods: - update_initial_conditions(): Update initial conditions method. - max_gross_profit_objective(blocks): Maximum gross profit objective method. - min_operating_cost_objective(blocks): Minimum operating cost objective method. - _create_variables(hybrid): Create variables method. - _create_port(hybrid): Create port method. - """ + """Dispatch optimization model for CSP trough systems.""" def __init__( self, @@ -34,8 +20,7 @@ def __init__( financial_model: FinancialModelType, block_set_name: str = "trough", ): - """ - Initialize TroughDispatch. + """Initialize TroughDispatch. Args: pyomo_model (ConcreteModel): Pyomo concrete model. @@ -43,6 +28,7 @@ def __init__( system_model (None): System model. financial_model (FinancialModelType): Financial model. block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, @@ -69,6 +55,7 @@ def max_gross_profit_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( @@ -107,6 +94,7 @@ def min_operating_cost_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = sum( hybrid_blocks[t].time_weighting_factor @@ -139,6 +127,7 @@ def _create_variables(self, hybrid): tuple: Tuple containing created variables. - generation: Generation from given technology. - load: Load from given technology. + """ hybrid.trough_generation = Var( doc="Power generation of CSP trough [MW]", @@ -155,14 +144,14 @@ def _create_variables(self, hybrid): return hybrid.trough_generation, hybrid.trough_load def _create_port(self, hybrid): - """ - Create CSP trough port to add to hybrid plant instance. + """Create CSP trough port to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. Returns: Port: CSP Trough Port object. + """ hybrid.trough_port = Port( initialize={ diff --git a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py index cd1e1ec14..298105983 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wave_dispatch.py @@ -14,20 +14,7 @@ class WaveDispatch(PowerSourceDispatch): wave_obj: Union[Expression, float] _system_model: MhkWave.MhkWave _financial_model: FinancialModelType - """ - Dispatch optimization model for mhk wave power source. - - Attributes: - wave_obj: Wave object. - _system_model: System model. - _financial_model: Financial model. - - Methods: - max_gross_profit_objective(blocks): Maximum gross profit objective method. - min_operating_cost_objective(blocks): Minimum operating cost objective method. - _create_variables(hybrid): Create variables method. - _create_port(hybrid): Create port method. - """ + """Dispatch optimization model for mhk wave power source.""" def __init__( self, @@ -37,8 +24,7 @@ def __init__( financial_model: FinancialModelType, block_set_name: str = "wave", ): - """ - Initialize WaveDispatch. + """Initialize WaveDispatch. Args: pyomo_model (ConcreteModel): Pyomo concrete model. @@ -46,6 +32,7 @@ def __init__( system_model (MhkWave.MhkWave): System model. financial_model (FinancialModelType): Financial model. block_set_name (str): Name of the block set. + """ super().__init__( pyomo_model, @@ -61,6 +48,7 @@ def max_gross_profit_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( @@ -78,6 +66,7 @@ def min_operating_cost_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = sum( hybrid_blocks[t].time_weighting_factor @@ -88,8 +77,7 @@ def min_operating_cost_objective(self, hybrid_blocks): ) def _create_variables(self, hybrid): - """ - Create MHK wave variables to add to hybrid plant instance. + """Create MHK wave variables to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. @@ -109,14 +97,14 @@ def _create_variables(self, hybrid): return hybrid.wave_generation, 0 def _create_port(self, hybrid): - """ - Create mhk wave port to add to hybrid plant instance. + """Create mhk wave port to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. Returns: Port: MHK wave Port object. + """ hybrid.wave_port = Port(initialize={"generation": hybrid.wave_generation}) return hybrid.wave_port diff --git a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py index 3a268920a..661be0fe6 100644 --- a/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_sources/wind_dispatch.py @@ -17,20 +17,7 @@ class WindDispatch(PowerSourceDispatch): wind_obj: Union[Expression, float] _system_model: Union[Windpower.Windpower, "Floris"] _financial_model: FinancialModelType - """ - Dispatch optimization model for wind power source. - - Attributes: - wind_obj: Wind object. - _system_model: System model. - _financial_model: Financial model. - - Methods: - max_gross_profit_objective(blocks): Maximum gross profit objective method. - min_operating_cost_objective(blocks): Minimum operating cost objective method. - _create_variables(hybrid): Create variables method. - _create_port(hybrid): Create port method. - """ + """Dispatch optimization model for wind power source.""" def __init__( self, @@ -40,8 +27,7 @@ def __init__( financial_model: FinancialModelType, block_set_name: str = "wind", ): - """ - Initialize WindDispatch. + """Initialize WindDispatch. Args: pyomo_model (ConcreteModel): Pyomo concrete model. @@ -49,6 +35,7 @@ def __init__( system_model (Union[Windpower.Windpower,"Floris"]): System model. financial_model (FinancialModelType): Financial model. block_set_name (str): Name of the block set. + """ super().__init__( @@ -65,6 +52,7 @@ def max_gross_profit_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = Expression( expr=sum( @@ -82,6 +70,7 @@ def min_operating_cost_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ self.obj = sum( hybrid_blocks[t].time_weighting_factor @@ -92,8 +81,7 @@ def min_operating_cost_objective(self, hybrid_blocks): ) def _create_variables(self, hybrid): - """ - Create wind variables to add to hybrid plant instance. + """Create wind variables to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. @@ -113,14 +101,14 @@ def _create_variables(self, hybrid): return hybrid.wind_generation, 0 def _create_port(self, hybrid): - """ - Create wind port to add to hybrid plant instance. + """Create wind port to add to hybrid plant instance. Args: hybrid: Hybrid plant instance. Returns: Port: Wind Port object. + """ hybrid.wind_port = Port(initialize={"generation": hybrid.wind_generation}) return hybrid.wind_port diff --git a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py index a7e7222e7..7f9ead6be 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/heuristic_load_following_dispatch.py @@ -15,6 +15,7 @@ class HeuristicLoadFollowingDispatch(SimpleBatteryDispatchHeuristic): power demand profile. Currently, enforces available generation and grid limit assuming no battery charging from grid + """ def __init__( @@ -27,8 +28,7 @@ def __init__( block_set_name: str = "heuristic_load_following_battery", dispatch_options: Optional[dict] = None, ): - """ - Initialize HeuristicLoadFollowingDispatch. + """Initialize HeuristicLoadFollowingDispatch. Args: pyomo_model (pyomo.ConcreteModel): Pyomo concrete model. @@ -38,6 +38,7 @@ def __init__( fixed_dispatch (Optional[List], optional): List of normalized values [-1, 1] (Charging (-), Discharging (+)). Defaults to None. block_set_name (str, optional): Name of the block set. Defaults to 'heuristic_load_following_battery'. dispatch_options (Optional[dict], optional): Dispatch options. Defaults to None. + """ super().__init__( pyomo_model, @@ -50,14 +51,14 @@ def __init__( ) def set_fixed_dispatch(self, gen: list, grid_limit: list, goal_power: list): - """ - Sets charge and discharge power of battery dispatch using fixed_dispatch attribute + """Sets charge and discharge power of battery dispatch using fixed_dispatch attribute and enforces available generation and grid limits. Args: gen (list): List of power generation. grid_limit (list): List of grid limits. goal_power (list): List of goal power. + """ self.check_gen_grid_limit(gen, grid_limit) @@ -66,13 +67,13 @@ def set_fixed_dispatch(self, gen: list, grid_limit: list, goal_power: list): self._fix_dispatch_model_variables() def _heuristic_method(self, gen, goal_power): - """ - Enforces battery power fraction limits and sets _fixed_dispatch attribute. + """Enforces battery power fraction limits and sets _fixed_dispatch attribute. Sets the _fixed_dispatch based on goal_power and gen (power generation profile). Args: gen: Power generation profile. goal_power: Goal power. + """ for t in self.blocks.index_set(): fd = (goal_power[t] - gen[t]) / self.maximum_power diff --git a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py index 30bbf34ed..4c8e9ddbe 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_convex_battery_dispatch.py @@ -10,17 +10,10 @@ class ConvexLinearVoltageBatteryDispatch(NonConvexLinearVoltageBatteryDispatch): - """ - This class represents a convex linear voltage battery dispatch model. + """This class represents a convex linear voltage battery dispatch model. It extends the NonConvexLinearVoltageBatteryDispatch model and adds additional formulation to enforce convexity. - Attributes: - _system_model: The battery system model. - _financial_model: The financial model. - block_set_name: The name of the block set. - dispatch_options: Dispatch options. - use_exp_voltage_point: Boolean indicating whether to use the exponential voltage point. """ # TODO: add a reference to original paper @@ -45,6 +38,7 @@ def __init__( block_set_name (str, optional): Name of the block set. Defaults to 'convex_LV_battery'. dispatch_options (dict, optional): Dispatch options. Defaults to None. use_exp_voltage_point (bool, optional): Boolean indicating whether to use the exponential voltage point. Defaults to False. + """ if dispatch_options is None: dispatch_options = {} @@ -59,11 +53,11 @@ def __init__( ) def dispatch_block_rule(self, battery): - """ - Additional formulation for dispatch block rule. + """Additional formulation for dispatch block rule. Args: battery: Battery instance. + """ # Additional formulation # Variables @@ -72,11 +66,11 @@ def dispatch_block_rule(self, battery): @staticmethod def _create_lv_battery_auxiliary_variables(battery): - """ - Create auxiliary variables for the battery model. + """Create auxiliary variables for the battery model. Args: battery: Battery instance. + """ # Auxiliary Variables battery.aux_charge_current_soc = pyomo.Var( @@ -102,11 +96,11 @@ def _create_lv_battery_auxiliary_variables(battery): @staticmethod def _create_lv_battery_power_equation_constraints(battery): - """ - Create power equation constraints for the battery model. + """Create power equation constraints for the battery model. Args: battery: Battery instance. + """ battery.charge_power_equation = pyomo.Constraint( doc="Battery charge power equation equal to the product of current and voltage", @@ -265,12 +259,12 @@ def _create_lv_battery_power_equation_constraints(battery): ) def _lifecycle_count_rule(self, m, i): - """ - Lifecycle count rule. + """Lifecycle count rule. Args: m: Model instance. i: Index. + """ # current accounting # TODO: Check for cheating -> there seems to be a lot of error diff --git a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py index 62911af10..fccbe7904 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/linear_voltage_nonconvex_battery_dispatch.py @@ -10,17 +10,10 @@ class NonConvexLinearVoltageBatteryDispatch(SimpleBatteryDispatch): - """ - This class represents a non-convex linear voltage battery dispatch model. + """This class represents a non-convex linear voltage battery dispatch model. It extends the SimpleBatteryDispatch model and adds additional formulation to handle non-convex behavior. - - Attributes: - _system_model: The battery system model. - _financial_model: The financial model. - block_set_name: The name of the block set. - dispatch_options: Dispatch options. - use_exp_voltage_point: Boolean indicating whether to use the exponential voltage point. + """ # TODO: add a reference to original paper @@ -45,6 +38,7 @@ def __init__( block_set_name (str, optional): Name of the block set. Defaults to 'LV_battery'. dispatch_options (dict, optional): Dispatch options. Defaults to None. use_exp_voltage_point (bool, optional): Boolean indicating whether to use the exponential voltage point. Defaults to False. + """ u.load_definitions_from_strings(["amp_hour = amp * hour = Ah = amphour"]) if dispatch_options is None: @@ -60,11 +54,11 @@ def __init__( self.use_exp_voltage_point = use_exp_voltage_point def dispatch_block_rule(self, battery): - """ - Additional formulation for dispatch block rule. + """Additional formulation for dispatch block rule. Args: battery: Battery instance. + """ # Parameters self._create_lv_battery_parameters(battery) @@ -86,6 +80,7 @@ def _create_capacity_parameter(self, battery): Args: battery: Battery instance. + """ battery.capacity = pyomo.Param( doc=self.block_set_name + " capacity [MAh]", @@ -99,6 +94,7 @@ def _create_lv_battery_parameters(self, battery): Args: battery: Battery instance. + """ battery.voltage_slope = pyomo.Param( doc=self.block_set_name + " linear voltage model slope coefficient [V]", @@ -160,6 +156,7 @@ def _create_lv_battery_variables(battery): Args: battery: Battery instance. + """ battery.charge_current = pyomo.Var( doc="Current into the battery [MA]", @@ -177,6 +174,7 @@ def _create_soc_inventory_constraint(self, storage): Args: battery: Battery instance. + """ def soc_inventory_rule(m): @@ -200,6 +198,7 @@ def _create_lv_battery_constraints(battery): Args: battery: Battery instance. + """ # Charge current bounds battery.charge_current_lb = pyomo.Constraint( @@ -240,6 +239,7 @@ def _create_lv_battery_power_equation_constraints(battery): Args: battery: Battery instance. + """ battery.charge_power_equation = pyomo.Constraint( doc="Battery charge power equation equal to the product of current and voltage", @@ -272,6 +272,7 @@ def _lifecycle_count_rule(self, m, i): Args: m: Model instance. i: Index. + """ # current accounting start = int(i * self.timesteps_per_day) diff --git a/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py index 9d15063ec..78a7fee52 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/one_cycle_battery_dispatch_heuristic.py @@ -11,20 +11,7 @@ class OneCycleBatteryDispatchHeuristic(SimpleBatteryDispatchHeuristic): - """ - One cycle per day heuristic battery dispatch. - - Args: - pyomo_model (pyomo.ConcreteModel): Pyomo model instance. - index_set (pyomo.Set): Index set. - system_model (BatteryModel.BatteryStateful): Battery model instance. - financial_model (Singleowner.Singleowner): Single owner financial model instance. - block_set_name (str, optional): Name of the block set. Defaults to 'one_cycle_heuristic_battery'. - dispatch_options (dict, optional): Dispatch options. Defaults to None. - - Attributes: - prices (list): List of normalized prices [-1, 1] (Charging (-), Discharging (+)). - """ + """One cycle per day heuristic battery dispatch.""" def __init__( self, @@ -44,6 +31,7 @@ def __init__( financial_model (Singleowner.Singleowner): Financial model. block_set_name (str, optional):Name of the block set. Defaults to 'one_cycle_heuristic_battery'. dispatch_options (dict, optional): Dispatch options. Defaults to None. + """ if dispatch_options is None: dispatch_options = {} @@ -58,17 +46,18 @@ def __init__( self.prices = list([0.0] * len(self.blocks.index_set())) def _heuristic_method(self, gen): - """ - Sets battery dispatch using a one cycle per day assumption. + """Sets battery dispatch using a one cycle per day assumption. Method: - 1. Sort input prices - 2. Determine the duration required to fully discharge and charge the battery - 3. Set discharge and charge operations based on sorted prices - 3. Check SOC feasibility - 4. If infeasible, find infeasibility, shift operation to the next sorted price periods - 5. Repeat step 4 until SOC feasible - NOTE: If operation is tried on half of time periods, then operation defaults to 'do nothing' + - Sort input prices + - Determine the duration required to fully discharge and charge the battery + - Set discharge and charge operations based on sorted prices + - Check SOC feasibility + - If infeasible, find infeasibility, shift operation to the next sorted price periods + - Repeat step 4 until SOC feasible + + NOTE: If operation is tried on half of time periods, then operation defaults to 'do nothing' + """ if sum(self.prices) == 0.0 and max(self.prices) == 0.0: raise ValueError("prices must be set before calling heuristic method.") @@ -124,11 +113,11 @@ def _heuristic_method(self, gen): def _discharge_battery( self, discharge_remaining, next_discharge_idx, sorted_prices, fixed_dispatch ): - """ - Discharges battery using the remaining discharge and the next best discharge period. + """Discharges battery using the remaining discharge and the next best discharge period. Returns: Tuple[list, int]: Adjusted fixed dispatch and next discharge index to be tried. + """ period_count = next_discharge_idx while discharge_remaining > 0: @@ -149,11 +138,11 @@ def _discharge_battery( def _charge_battery( self, charge_remaining, next_charge_idx, sorted_prices, fixed_dispatch ): - """ - Charges battery using the remaining charge and the next best charge period. + """Charges battery using the remaining charge and the next best charge period. Returns: Tuple[list, int]: Adjusted fixed dispatch and next charge index to be tried. + """ period_count = next_charge_idx while charge_remaining > 0: @@ -172,11 +161,11 @@ def _charge_battery( return fixed_dispatch, next_charge_idx def _get_duration_battery_full_cycle(self) -> Tuple[float, float]: - """ - Calculates discharge and charge hours required to fully cycle the battery. + """Calculates discharge and charge hours required to fully cycle the battery. Returns: Tuple[float, float]: Discharge and charge hours. + """ true_capacity = (self.maximum_soc - self.minimum_soc) * self.capacity / 100.0 @@ -187,11 +176,11 @@ def _get_duration_battery_full_cycle(self) -> Tuple[float, float]: return n_discharge, n_charge def test_soc_feasibility(self, fixed_dispatch) -> Tuple[bool, int]: - """ - Steps through fixed_dispatch and tests SOC feasibility. + """Steps through fixed_dispatch and tests SOC feasibility. Returns: Tuple[bool, int]: Tuple indicating SOC feasibility and index of first infeasible operation. + """ soc0 = self.model.initial_soc.value for idx, fd in enumerate(fixed_dispatch): @@ -206,11 +195,11 @@ def test_soc_feasibility(self, fixed_dispatch) -> Tuple[bool, int]: @property def prices(self) -> list: - """ - List of normalized prices [-1, 1] (Charging (-), Discharging (+)). + """List of normalized prices [-1, 1] (Charging (-), Discharging (+)). Returns: list: Prices. + """ return self._prices diff --git a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py index 09c5dbf25..cb72b49ed 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/power_storage_dispatch.py @@ -7,20 +7,7 @@ class PowerStorageDispatch(Dispatch): - """ - Dispatch algorithm for power storage. - - Args: - pyomo_model (pyomo.ConcreteModel): Pyomo model instance. - index_set (pyomo.Set): Index set. - system_model: System model. - financial_model: Financial model. - block_set_name (str): Name of the block set. - dispatch_options: Dispatch options. - - Attributes: - options (object): Dispatch options. - """ + """Dispatch algorithm for power storage.""" def __init__( self, @@ -40,6 +27,7 @@ def __init__( financial_model: Financial model. block_set_name (str, optional): Name of the block set. dispatch_options (dict, optional): Dispatch options. + """ super().__init__( @@ -62,9 +50,9 @@ def dispatch_block_rule(self, storage): """Initializes storage parameters, variables, and constraints. Called during Dispatch's __init__. - Args: storage: Storage instance. + """ # Parameters self._create_storage_parameters(storage) @@ -84,6 +72,7 @@ def max_gross_profit_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ def battery_profit_objective_rule(m): @@ -110,6 +99,7 @@ def min_operating_cost_objective(self, hybrid_blocks): Args: hybrid_blocks (Pyomo.block): A generalized container for defining hierarchical models by adding modeling components as attributes. + """ objective = sum( hybrid_blocks[t].time_weighting_factor @@ -126,14 +116,14 @@ def min_operating_cost_objective(self, hybrid_blocks): self.obj = objective def _create_variables(self, hybrid): - """ - Creates storage variables. + """Creates storage variables. Args: hybrid: Hybrid instance. Returns: Tuple: Tuple containing battery discharge and charge variables. + """ hybrid.battery_charge = pyomo.Var( doc="Power charging the electric battery [MW]", @@ -150,14 +140,14 @@ def _create_variables(self, hybrid): return hybrid.battery_discharge, hybrid.battery_charge def _create_port(self, hybrid): - """ - Creates storage port. + """Creates storage port. Args: hybrid: Hybrid instance. Returns: Port: Storage port. + """ hybrid.battery_port = Port( initialize={ @@ -172,6 +162,7 @@ def _create_storage_parameters(self, storage): Args: storage: Storage instance. + """ ################################## # Parameters # @@ -230,6 +221,7 @@ def _create_efficiency_parameters(self, storage): Args: storage: Storage instance. + """ storage.charge_efficiency = pyomo.Param( doc=self.block_set_name + " Charging efficiency [-]", @@ -251,6 +243,7 @@ def _create_capacity_parameter(self, storage): Args: storage: Storage instance. + """ storage.capacity = pyomo.Param( doc=self.block_set_name + " capacity [MWh]", @@ -264,6 +257,7 @@ def _create_storage_variables(self, storage): Args: storage: Storage instance. + """ ################################## # Variables # @@ -338,6 +332,7 @@ def _create_soc_inventory_constraint(self, storage): Args: storage: Storage instance. + """ def soc_inventory_rule(m): @@ -363,6 +358,7 @@ def _create_storage_port(storage): Args: storage: Storage instance. + """ ################################## # Ports # @@ -409,6 +405,7 @@ def _lifecycle_count_rule(self, m, i): Returns: float: Lifecycle count. + """ # Use full-energy cycles start = int(i * self.timesteps_per_day) @@ -482,6 +479,7 @@ def _check_initial_soc(self, initial_soc): Returns: float: Checked initial state-of-charge. + """ if initial_soc > 1: initial_soc /= 100.0 diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py index 21fead6a9..6587ebf85 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch.py @@ -10,10 +10,7 @@ class SimpleBatteryDispatch(PowerStorageDispatch): - """ - A dispatch class for simple battery operations. - - """ + """A dispatch class for simple battery operations.""" def __init__( self, @@ -24,8 +21,7 @@ def __init__( block_set_name: str, dispatch_options, ): - """ - Initializes SimpleBatteryDispatch. + """Initializes SimpleBatteryDispatch. Args: pyomo_model (pyomo.ConcreteModel): The Pyomo model instance. @@ -34,7 +30,7 @@ def __init__( financial_model (FinancialModelType): The financial model type. block_set_name (str): Name of the block set. dispatch_options: Dispatch options. - + """ super().__init__( pyomo_model, @@ -46,10 +42,7 @@ def __init__( ) def initialize_parameters(self): - """ - Initializes parameters. - - """ + """Initializes parameters.""" if self.options.include_lifecycle_count: self.lifecycle_cost = ( self.options.lifecycle_cost_per_kWh_cycle @@ -74,18 +67,14 @@ def initialize_parameters(self): self._set_model_specific_parameters() def _set_control_mode(self): - """ - Sets control mode. - - """ + """Sets control mode.""" if isinstance(self._system_model, BatteryModel.BatteryStateful): self._system_model.value("control_mode", 1.0) # Power control self._system_model.value("input_power", 0.0) self.control_variable = "input_power" def _set_model_specific_parameters(self, round_trip_efficiency=88.0): - """ - Sets model-specific parameters. + """Sets model-specific parameters. Args: round_trip_efficiency (float, optional): The round-trip efficiency including converter efficiency. @@ -98,8 +87,7 @@ def _set_model_specific_parameters(self, round_trip_efficiency=88.0): self.capacity = self._system_model.value("nominal_energy") / 1e3 # [MWh] def update_time_series_parameters(self, start_time: int): - """ - Updates time series parameters. + """Updates time series parameters. Args: start_time (int): The start time. @@ -109,8 +97,7 @@ def update_time_series_parameters(self, start_time: int): self.time_duration = [1.0] * len(self.blocks.index_set()) def update_dispatch_initial_soc(self, initial_soc: float = None): - """ - Updates dispatch initial state of charge (SOC). + """Updates dispatch initial state of charge (SOC). Args: initial_soc (float, optional): Initial state of charge. Defaults to None. diff --git a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py index f858bc0ef..7b2c2ba19 100644 --- a/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py +++ b/hopp/simulation/technologies/dispatch/power_storage/simple_battery_dispatch_heuristic.py @@ -16,20 +16,6 @@ class SimpleBatteryDispatchHeuristic(SimpleBatteryDispatch): Currently, enforces available generation and grid limit assuming no battery charging from grid. - Args: - pyomo_model (pyomo.ConcreteModel): Pyomo model instance. - index_set (pyomo.Set): Index set. - system_model (BatteryModel.BatteryStateful): BatteryStateful model instance. - financial_model (Singleowner.Singleowner): Singleowner model instance. - fixed_dispatch (Optional[List]): List of normalized values [-1, 1] (Charging (-), Discharging (+)). Defaults to None. - block_set_name (str): Name of the block set. Defaults to 'heuristic_battery'. - dispatch_options (Optional[Dict]): Dispatch options. Defaults to None. - - Attributes: - max_charge_fraction (List[float]): List of maximum charge fractions for each time period. - max_discharge_fraction (List[float]): List of maximum discharge fractions for each time period. - user_fixed_dispatch (List[float]): List of user-defined fixed dispatch values for each time period. - _fixed_dispatch (List[float]): List of fixed dispatch values based on heuristic method for each time period. """ def __init__( @@ -52,6 +38,7 @@ def __init__( fixed_dispatch (Optional[List], optional): List of normalized values [-1, 1] (Charging (-), Discharging (+)). Defaults to None. block_set_name (str, optional): Name of block set. Defaults to 'heuristic_battery'. dispatch_options (dict, optional): Dispatch options. Defaults to None. + """ if dispatch_options is None: dispatch_options = {} @@ -108,8 +95,7 @@ def check_gen_grid_limit(self, gen: list, grid_limit: list): raise ValueError("grid_limit must be the same length as fixed_dispatch.") def _set_power_fraction_limits(self, gen: list, grid_limit: list): - """ - Set battery charge and discharge power fraction limits based on + """Set battery charge and discharge power fraction limits based on available generation and grid capacity, respectively. Args: @@ -129,8 +115,7 @@ def _set_power_fraction_limits(self, gen: list, grid_limit: list): @staticmethod def enforce_power_fraction_simple_bounds(power_fraction: float) -> float: - """ - Enforces simple bounds (0, .9) for battery power fractions. + """Enforces simple bounds (0, .9) for battery power fractions. Args: power_fraction (float): Power fraction from heuristic method. @@ -146,8 +131,7 @@ def enforce_power_fraction_simple_bounds(power_fraction: float) -> float: return power_fraction def update_soc(self, power_fraction: float, soc0: float) -> float: - """ - Updates SOC based on power fraction threshold (0.1). + """Updates SOC based on power fraction threshold (0.1). Args: power_fraction (float): Power fraction from heuristic method. Below threshold @@ -156,7 +140,7 @@ def update_soc(self, power_fraction: float, soc0: float) -> float: Returns: soc (float): Updated SOC. - + """ if power_fraction > 0.0: discharge_power = power_fraction * self.maximum_power @@ -223,16 +207,12 @@ def _fix_dispatch_model_variables(self): @property def fixed_dispatch(self) -> list: - """ - list: List of fixed dispatch. - """ + """list: List of fixed dispatch.""" return self._fixed_dispatch @property def user_fixed_dispatch(self) -> list: - """ - list: List of user fixed dispatch. - """ + """list: List of user fixed dispatch.""" return self._user_fixed_dispatch @user_fixed_dispatch.setter From 9f2f0a4f704fc0fa1f9c2548ac1574c3d2ca25d0 Mon Sep 17 00:00:00 2001 From: bayc Date: Mon, 22 Apr 2024 15:02:18 -0600 Subject: [PATCH 27/27] update version to v2.2.0 --- hopp/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hopp/version.py b/hopp/version.py index 7ec1d6db4..ccbccc3dc 100644 --- a/hopp/version.py +++ b/hopp/version.py @@ -1 +1 @@ -2.1.0 +2.2.0