def test_changing_turbine_rating(): wind_site = site # powercurve scaling model = WindPlant(wind_site, 48000) n_turbs = model.num_turbines for n in range(1000, 3000, 150): model.turb_rating = n assert model.system_capacity_kw == model.turb_rating * n_turbs, "system size error when rating is " + str( n)
def test_changing_rotor_diam(): wind_site = site model = WindPlant(wind_site, 20000) assert model.system_capacity_kw == 20000 ratings = range(50, 70, 5) for d in ratings: model.rotor_diameter = d assert model.rotor_diameter == d, "rotor diameter should be " + str(d) assert model.turb_rating == 1000, "new rating different when rotor diamter is " + str( d)
def test_changing_turbine_rating(): # powercurve scaling model = WindPlant(SiteInfo(flatirons_site), { 'num_turbines': 24, "turbine_rating_kw": 2000 }) n_turbs = model.num_turbines for n in range(1000, 3000, 150): model.turb_rating = n assert model.system_capacity_kw == model.turb_rating * n_turbs, "system size error when rating is " + str( n)
def test_changing_n_turbines(): wind_site = site # test with gridded layout model = WindPlant(wind_site, 20000) assert (model.system_capacity_kw == 20000) for n in range(1, 20): model.num_turbines = n assert model.num_turbines == n, "n turbs should be " + str(n) assert model.system_capacity_kw == pytest.approx( 20000, 1), "system capacity different when n turbs " + str(n)
def test_changing_rotor_diam_recalc(): model = WindPlant(SiteInfo(flatirons_site), { 'num_turbines': 10, "turbine_rating_kw": 2000 }) assert model.system_capacity_kw == 20000 diams = range(50, 70, 140) for d in diams: model.rotor_diameter = d assert model.rotor_diameter == d, "rotor diameter should be " + str(d) assert model.turb_rating == 2000, "new rating different when rotor diameter is " + str( d)
def test_changing_n_turbines(): # test with gridded layout model = WindPlant(SiteInfo(flatirons_site), { 'num_turbines': 10, "turbine_rating_kw": 2000 }) assert (model.system_capacity_kw == 20000) for n in range(1, 20): model.num_turbines = n assert model.num_turbines == n, "n turbs should be " + str(n) assert model.system_capacity_kw == pytest.approx( 20000, 1), "system capacity different when n turbs " + str(n)
def test_changing_powercurve(): wind_site = site # with power curve recalculation requires diameter changes model = WindPlant(wind_site, 48000) n_turbs = model.num_turbines d_to_r = model.rotor_diameter / model.turb_rating for n in range(1000, 3001, 500): d = math.ceil(n * d_to_r * 1) model.modify_powercurve(d, n) assert model.turb_rating == pytest.approx( n, 0.1), "turbine rating should be " + str(n) assert model.system_capacity_kw == pytest.approx( model.turb_rating * n_turbs, 0.1), "size error when rating is " + str(n)
def __init__(self, power_sources: dict, site: SiteInfo, interconnect_kw: float): """ Base class for simulating a hybrid power plant. Can be derived to add other sizing methods, financial analyses, methods for pre- or post-processing, etc :param power_sources: tuple of strings, float pairs names of power sources to include and their kw sizes choices include: ('solar', 'wind', 'geothermal', 'battery') :param site: Site layout, location and resource data :param interconnect_kw: float power limit of interconnect for the site """ self._fileout = Path.cwd() / "results" self.site = site self.interconnect_kw = interconnect_kw self.power_sources = dict() self.solar: Union[SolarPlant, None] = None self.wind: Union[WindPlant, None] = None self.grid: Union[Grid, None] = None if 'solar' in power_sources.keys(): self.solar = SolarPlant(self.site, power_sources['solar'] * 1000) self.power_sources['solar'] = self.solar logger.info( "Created HybridSystem.solar with system size {} mW".format( power_sources['solar'])) if 'wind' in power_sources.keys(): self.wind = WindPlant(self.site, power_sources['wind'] * 1000) self.power_sources['wind'] = self.wind logger.info( "Created HybridSystem.wind with system size {} mW".format( power_sources['wind'])) if 'geothermal' in power_sources.keys(): raise NotImplementedError("Geothermal plant not yet implemented") if 'battery' in power_sources: raise NotImplementedError("Battery not yet implemented") self.cost_model = None # performs interconnection and curtailment energy limits self.grid = Grid(self.site, interconnect_kw) self.outputs_factory = HybridSimulationOutput(power_sources)
def test_wind_dispatch(site): expected_objective = 20719.281 dispatch_n_look_ahead = 48 wind = WindPlant(site, technologies['wind']) model = pyomo.ConcreteModel(name='wind_only') model.forecast_horizon = pyomo.Set(initialize=range(dispatch_n_look_ahead)) wind._dispatch = WindDispatch(model, model.forecast_horizon, wind._system_model, wind._financial_model) # Manually creating objective for testing model.price = pyomo.Param(model.forecast_horizon, within=pyomo.Reals, default=60.0, # assuming flat PPA of $60/MWh mutable=True, units=u.USD / u.MWh) def create_test_objective_rule(m): return sum((m.wind[t].time_duration * (m.price[t] - m.wind[t].cost_per_generation) * m.wind[t].generation) for t in m.wind.index_set()) model.test_objective = pyomo.Objective( rule=create_test_objective_rule, sense=pyomo.maximize) assert_units_consistent(model) wind.dispatch.initialize_parameters() wind.simulate(1) wind.dispatch.update_time_series_parameters(0) results = HybridDispatchBuilderSolver.glpk_solve_call(model) assert results.solver.termination_condition == TerminationCondition.optimal assert pyomo.value(model.test_objective) == pytest.approx(expected_objective, 1e-5) available_resource = wind.generation_profile[0:dispatch_n_look_ahead] dispatch_generation = wind.dispatch.generation for t in model.forecast_horizon: assert dispatch_generation[t] * 1e3 == pytest.approx(available_resource[t], 1e-3)
def test_wind_layout(site): wind_model = WindPlant(site, technology['wind']) xcoords, ycoords = wind_model._layout.turb_pos_x, wind_model._layout.turb_pos_y expected_xcoords = [0.7510, 1004.83, 1470.38, 903.06, 658.182] expected_ycoords = [888.865, 1084.148, 929.881, 266.4096, 647.169] for i in range(len(xcoords)): assert xcoords[i] == pytest.approx(expected_xcoords[i], 1e-3) assert ycoords[i] == pytest.approx(expected_ycoords[i], 1e-3)
def test_changing_system_capacity(): wind_site = site # adjust number of turbines, system capacity won't be exactly as requested model = WindPlant(wind_site, 20000) rating = model.turb_rating for n in range(1000, 20000, 1000): model.system_capacity_by_num_turbines(n) assert model.turb_rating == rating, str(n) assert model.system_capacity_kw == rating * round(n / rating) # adjust turbine rating first, system capacity will be exact model = WindPlant(wind_site, 20000) for n in range(40000, 60000, 1000): model.system_capacity_by_rating(n) assert model.system_capacity_kw == pytest.approx(n)
def test_hybrid_layout(site): power_sources = { 'wind': WindPlant(site, technology['wind']), 'pv': PVPlant(site, technology['pv']) } layout = HybridLayout(site, power_sources) xcoords, ycoords = layout.wind.turb_pos_x, layout.wind.turb_pos_y buffer_region = layout.pv.buffer_region # turbines move from `test_wind_layout` due to the solar exclusion for i in range(len(xcoords)): assert not buffer_region.contains(Point(xcoords[i], ycoords[i])) assert (layout.pv.flicker_loss > 0.0001)
def test_hybrid_layout_wind_only(site): power_sources = { 'wind': WindPlant(site, technology['wind']), # 'solar': SolarPlant(site, technology['solar']) } layout = HybridLayout(site, power_sources) xcoords, ycoords = layout.wind.turb_pos_x, layout.wind.turb_pos_y print(xcoords, ycoords) expected_xcoords = [0.751, 1004.834, 1470.385, 903.063, 658.181] expected_ycoords = [888.865, 1084.148, 929.881, 266.409, 647.169] # turbines move from `test_wind_layout` due to the solar exclusion for i in range(len(xcoords)): assert xcoords[i] == pytest.approx(expected_xcoords[i], 1e-3) assert ycoords[i] == pytest.approx(expected_ycoords[i], 1e-3)
def test_changing_system_capacity(): # adjust number of turbines, system capacity won't be exactly as requested model = WindPlant(SiteInfo(flatirons_site), { 'num_turbines': 20, "turbine_rating_kw": 1000 }) rating = model.turb_rating for n in range(1000, 20000, 1000): model.system_capacity_by_num_turbines(n) assert model.turb_rating == rating, str(n) assert model.system_capacity_kw == rating * round(n / rating) # adjust turbine rating first, system capacity will be exact model = WindPlant(SiteInfo(flatirons_site), { 'num_turbines': 20, "turbine_rating_kw": 1000 }) for n in range(40000, 60000, 1000): model.system_capacity_by_rating(n) assert model.system_capacity_kw == pytest.approx(n)
def run_reopt(site, scenario, load, interconnection_limit_kw, critical_load_factor, useful_life, battery_can_grid_charge, storage_used, run_reopt_flag): # kw_continuous = forced_system_size # 5 MW continuous load - equivalent to 909kg H2 per hr at 55 kWh/kg electrical intensity urdb_label = "5ca4d1175457a39b23b3d45e" # https://openei.org/apps/IURDB/rate/view/5ca3d45ab718b30e03405898 pv_config = {'system_capacity_kw': 20000} solar_model = PVPlant(site, pv_config) # ('num_turbines', 'turbine_rating_kw', 'rotor_diameter', 'hub_height', 'layout_mode', 'layout_params') wind_config = {'num_turbines': np.floor(scenario['Wind Size MW'] / scenario['Turbine Rating']), 'rotor_dimeter': scenario['Rotor Diameter'], 'hub_height': scenario['Tower Height'], 'turbine_rating_kw': scenario['Turbine Rating']} wind_model = WindPlant(site, wind_config) fin_model = so.default("GenericSystemSingleOwner") filepath = os.path.dirname(os.path.abspath(__file__)) fileout = 'reopt_result_test_intergration.json' # site = SiteInfo(sample_site, hub_height=tower_height) count = 1 reopt = REopt(lat=scenario['Lat'], lon=scenario['Long'], load_profile=load, urdb_label=urdb_label, solar_model=solar_model, wind_model=wind_model, fin_model=fin_model, interconnection_limit_kw=interconnection_limit_kw, off_grid=True, fileout=fileout) reopt.set_rate_path(os.path.join(filepath, '../data')) reopt.post['Scenario']['Site']['Wind']['installed_cost_us_dollars_per_kw'] = scenario['Wind Cost KW'] # ATB reopt.post['Scenario']['Site']['PV']['installed_cost_us_dollars_per_kw'] = scenario['Solar Cost KW'] reopt.post['Scenario']['Site']['Storage'] = {'min_kw': 0.0, 'max_kw': 0.99e9, 'min_kwh': 0.0, 'max_kwh': 0.99e9, 'internal_efficiency_pct': 0.975, 'inverter_efficiency_pct': 0.96, 'rectifier_efficiency_pct': 0.96, 'soc_min_pct': 0.2, 'soc_init_pct': 0.5, 'canGridCharge': battery_can_grid_charge, 'installed_cost_us_dollars_per_kw': scenario['Storage Cost KW'], 'installed_cost_us_dollars_per_kwh': scenario['Storage Cost KWh'], 'replace_cost_us_dollars_per_kw': scenario['Storage Cost KW'], 'replace_cost_us_dollars_per_kwh': scenario['Storage Cost KWh'], 'inverter_replacement_year': 10, 'battery_replacement_year': 10, 'macrs_option_years': 7, 'macrs_bonus_pct': 1.0, 'macrs_itc_reduction': 0.5, 'total_itc_pct': 0.0, 'total_rebate_us_dollars_per_kw': 0, 'total_rebate_us_dollars_per_kwh': 0} reopt.post['Scenario']['Site']['Financial']['analysis_years'] = useful_life if not storage_used: reopt.post['Scenario']['Site']['Storage']['max_kw'] = 0 if scenario['PTC Available']: reopt.post['Scenario']['Site']['Wind']['pbi_us_dollars_per_kwh'] = 0.022 else: reopt.post['Scenario']['Site']['Wind']['pbi_us_dollars_per_kwh'] = 0.0 if scenario['ITC Available']: reopt.post['Scenario']['Site']['PV']['federal_itc_pct'] = 0.26 else: reopt.post['Scenario']['Site']['PV']['federal_itc_pct'] = 0.0 # reopt.post['Scenario']['Site']['LoadProfile']['doe_reference_name'] = "FlatLoad" # reopt.post['Scenario']['Site']['LoadProfile']['annual_kwh'] = load #8760 * kw_continuous reopt.post['Scenario']['Site']['LoadProfile']['loads_kw'] = load reopt.post['Scenario']['Site']['LoadProfile']['critical_load_pct'] = critical_load_factor off_grid = False reopt.post['Scenario']['optimality_tolerance_techs'] = 0.05 if off_grid == True: # reopt.post['Scenario']['Site'].pop('Wind') # reopt.post['Scenario']['Site']['Wind']['min_kw'] = 10000 dictforstuff = {"off_grid_flag": True} reopt.post['Scenario'].update(dictforstuff) reopt.post['Scenario']['optimality_tolerance_techs'] = 0.05 reopt.post['Scenario']["timeout_seconds"] = 3600 # reopt.post['Scenario']['Site']['LoadProfile'].pop('annual kwh') reopt.post['Scenario']['Site'].pop('ElectricTariff') reopt.post['Scenario']['Site']['LoadProfile']['critical_load_pct'] = 1.0 f = open('massproducer_offgrid (1).json') data_for_post = json.load(f) reopt.post['Scenario']['Site']['Financial'] = data_for_post['Scenario']['Site']['Financial'] else: reopt.post['Scenario']['Site']['ElectricTariff']['wholesale_rate_us_dollars_per_kwh'] = 0.01 reopt.post['Scenario']['Site']['ElectricTariff']['wholesale_rate_above_site_load_us_dollars_per_kwh'] = 0.01 reopt.post['Scenario']['Site']['LoadProfile']['outage_start_hour'] = 10 reopt.post['Scenario']['Site']['LoadProfile']['outage_end_hour'] = 11 from pathlib import Path post_path = 'results/reopt_precomputes/reopt_post' post_path_abs = Path(__file__).parent / post_path if not os.path.exists(post_path_abs.parent): os.mkdir(post_path_abs.parent) with open(post_path_abs, 'w') as outfile: json.dump(reopt.post, outfile) # mass_producer_dict = { # "mass_units": "kg", # "time_units": "hr", # "min_mass_per_time": 10.0, # "max_mass_per_time": 10.0, # "electric_consumed_to_mass_produced_ratio_kwh_per_mass": 71.7, # "thermal_consumed_to_mass_produced_ratio_kwh_per_mass": 0.0, # "feedstock_consumed_to_mass_produced_ratio": 0.0, # "installed_cost_us_dollars_per_mass_per_time": 10.0, # "om_cost_us_dollars_per_mass_per_time": 1.5, # "om_cost_us_dollars_per_mass": 0.0, # "mass_value_us_dollars_per_mass": 5.0, # "feedstock_cost_us_dollars_per_mass": 0.0, # "macrs_option_years": 0, # "macrs_bonus_pct": 0 # } # reopt.post['Scenario']['Site']['MassProducer'] = mass_producer_dict if run_reopt_flag: #NEW METHOD load_dotenv() result = reopt.get_reopt_results() #BASIC INITIAL TEST FOR NEW METHOD # result = post_and_poll.get_api_results(data_for_post, NREL_API_KEY, 'https://offgrid-electrolyzer-reopt-dev-api.its.nrel.gov/v1', # 'reopt_result_test_intergration.json') # f = open('massproducer_offgrid (1).json') # data_for_post = json.load(f) #OLD METHOD # result = reopt.get_reopt_results(force_download=True) pickle.dump(result, open("results/reopt_precomputes/results_{}_{}_{}.p".format( scenario['Site Name'], scenario['Scenario Name'], critical_load_factor), "wb")) else: print("Not running reopt. Loading Dummy data") precompute_path = 'results/reopt_precomputes/' precompute_path_abs = Path(__file__).parent / precompute_path result = pickle.load( open(os.path.join(precompute_path_abs, "results_ATB_moderate_2020_IOWA_0.9.p"), "rb")) if result['outputs']['Scenario']['Site']['PV']['size_kw']: solar_size_mw = result['outputs']['Scenario']['Site']['PV']['size_kw'] / 1000 if result['outputs']['Scenario']['Site']['Wind']['size_kw']: wind_size_mw = result['outputs']['Scenario']['Site']['Wind']['size_kw'] / 1000 if result['outputs']['Scenario']['Site']['Storage']['size_kw']: storage_size_mw = result['outputs']['Scenario']['Site']['Storage']['size_kw'] / 1000 storage_size_mwh = result['outputs']['Scenario']['Site']['Storage']['size_kwh'] / 1000 storage_hours = storage_size_mwh / storage_size_mw reopt_site_result = result['outputs']['Scenario']['Site'] generated_date = pd.date_range(start='1/1/2018 00:00:00', end='12/31/2018 23:00:00', periods=8760) if reopt_site_result['Wind']['size_kw'] == 0: reopt_site_result['Wind']['year_one_power_production_series_kw'] = np.zeros(8760) reopt_site_result['Wind']['year_one_to_grid_series_kw'] = np.zeros(8760) reopt_site_result['Wind']['year_one_to_load_series_kw'] = np.zeros(8760) reopt_site_result['Wind']['year_one_to_battery_series_kw'] = np.zeros(8760) reopt_site_result['Wind']['year_one_curtailed_production_series_kw'] = np.zeros(8760) wind_size_mw = 0 if reopt_site_result['PV']['size_kw'] == 0: reopt_site_result['PV']['year_one_power_production_series_kw'] = np.zeros(8760) reopt_site_result['PV']['year_one_to_grid_series_kw'] = np.zeros(8760) reopt_site_result['PV']['year_one_to_load_series_kw'] = np.zeros(8760) reopt_site_result['PV']['year_one_to_battery_series_kw'] = np.zeros(8760) reopt_site_result['PV']['year_one_curtailed_production_series_kw'] = np.zeros(8760) solar_size_mw = 0 if reopt_site_result['Storage']['size_kw'] == 0: reopt_site_result['Storage']['year_one_soc_series_pct'] = np.zeros(8760) reopt_site_result['Storage']['year_one_to_massproducer_series_kw'] = np.zeros(8760) storage_size_mw = 0 storage_size_mwh = 0 storage_hours = 0 combined_pv_wind_power_production = [x + y for x, y in zip(reopt_site_result['PV']['year_one_power_production_series_kw'] , reopt_site_result['Wind']['year_one_power_production_series_kw'])] combined_pv_wind_storage_power_production = [x + y for x, y in zip(combined_pv_wind_power_production, reopt_site_result['Storage'][ 'year_one_to_load_series_kw'])] energy_shortfall = [y - x for x, y in zip(combined_pv_wind_storage_power_production, load)] energy_shortfall = [x if x > 0 else 0 for x in energy_shortfall] combined_pv_wind_curtailment = [x + y for x, y in zip(reopt_site_result['PV']['year_one_curtailed_production_series_kw'] , reopt_site_result['Wind']['year_one_curtailed_production_series_kw'])] reopt_result_dict = {'Date': generated_date, 'pv_power_production': reopt_site_result['PV'] ['year_one_power_production_series_kw'], 'pv_power_to_grid': reopt_site_result['PV'] ['year_one_to_grid_series_kw'], 'pv_power_to_load': reopt_site_result['PV']['year_one_to_load_series_kw'], 'pv_power_to_battery': reopt_site_result['PV']['year_one_to_battery_series_kw'], 'pv_power_curtailed': reopt_site_result['PV']['year_one_curtailed_production_series_kw'], 'wind_power_production': reopt_site_result['Wind'] ['year_one_power_production_series_kw'], 'wind_power_to_grid': reopt_site_result['Wind'] ['year_one_to_grid_series_kw'], 'wind_power_to_load': reopt_site_result['Wind']['year_one_to_load_series_kw'], 'wind_power_to_battery': reopt_site_result['Wind']['year_one_to_battery_series_kw'], 'wind_power_curtailed': reopt_site_result['Wind']['year_one_curtailed_production_series_kw'], 'combined_pv_wind_power_production': combined_pv_wind_power_production, 'combined_pv_wind_storage_power_production': combined_pv_wind_storage_power_production, 'storage_power_to_load': reopt_site_result['Storage']['year_one_to_load_series_kw'], 'storage_power_to_grid': reopt_site_result['Storage']['year_one_to_grid_series_kw'], 'battery_soc_pct': reopt_site_result['Storage']['year_one_soc_series_pct'], 'energy_shortfall': energy_shortfall, 'combined_pv_wind_curtailment': combined_pv_wind_curtailment } REoptResultsDF = pd.DataFrame(reopt_result_dict) return wind_size_mw, solar_size_mw, storage_size_mw, storage_size_mwh, storage_hours, result, REoptResultsDF
class HybridSimulation: hybrid_system: GenericSystem.GenericSystem def __init__(self, power_sources: dict, site: SiteInfo, interconnect_kw: float): """ Base class for simulating a hybrid power plant. Can be derived to add other sizing methods, financial analyses, methods for pre- or post-processing, etc :param power_sources: tuple of strings, float pairs names of power sources to include and their kw sizes choices include: ('solar', 'wind', 'geothermal', 'battery') :param site: Site layout, location and resource data :param interconnect_kw: float power limit of interconnect for the site """ self._fileout = Path.cwd() / "results" self.site = site self.interconnect_kw = interconnect_kw self.power_sources = dict() self.solar: Union[SolarPlant, None] = None self.wind: Union[WindPlant, None] = None self.grid: Union[Grid, None] = None if 'solar' in power_sources.keys(): self.solar = SolarPlant(self.site, power_sources['solar'] * 1000) self.power_sources['solar'] = self.solar logger.info( "Created HybridSystem.solar with system size {} mW".format( power_sources['solar'])) if 'wind' in power_sources.keys(): self.wind = WindPlant(self.site, power_sources['wind'] * 1000) self.power_sources['wind'] = self.wind logger.info( "Created HybridSystem.wind with system size {} mW".format( power_sources['wind'])) if 'geothermal' in power_sources.keys(): raise NotImplementedError("Geothermal plant not yet implemented") if 'battery' in power_sources: raise NotImplementedError("Battery not yet implemented") self.cost_model = None # performs interconnection and curtailment energy limits self.grid = Grid(self.site, interconnect_kw) self.outputs_factory = HybridSimulationOutput(power_sources) def setup_cost_calculator(self, cost_calculator: object): if hasattr(cost_calculator, "calculate_total_costs"): self.cost_model = cost_calculator @property def ppa_price(self): return self.grid.financial_model.Revenue.ppa_price_input @ppa_price.setter def ppa_price(self, ppa_price): if not isinstance(ppa_price, Iterable): ppa_price = (ppa_price, ) for k, _ in self.power_sources.items(): if hasattr(self, k): getattr(self, k).financial_model.Revenue.ppa_price_input = ppa_price self.grid.financial_model.Revenue.ppa_price_input = ppa_price @property def discount_rate(self): return self.grid.financial_model.FinancialParameters.real_discount_rate @discount_rate.setter def discount_rate(self, discount_rate): for k, _ in self.power_sources.items(): if hasattr(self, k): getattr( self, k ).financial_model.FinancialParameters.real_discount_rate = discount_rate self.grid.financial_model.FinancialParameters.real_discount_rate = discount_rate def set_om_costs_per_kw(self, solar_om_per_kw=None, wind_om_per_kw=None, hybrid_om_per_kw=None): if solar_om_per_kw and wind_om_per_kw and hybrid_om_per_kw: if len(solar_om_per_kw) != len(wind_om_per_kw) != len( hybrid_om_per_kw): raise ValueError( "Length of yearly om cost per kw arrays must be equal.") if solar_om_per_kw and self.solar: self.solar.financial_model.SystemCosts.om_capacity = solar_om_per_kw if wind_om_per_kw and self.wind: self.wind.financial_model.SystemCosts.om_capacity = wind_om_per_kw if hybrid_om_per_kw: self.grid.financial_model.SystemCosts.om_capacity = hybrid_om_per_kw def size_from_reopt(self): """ Calls ReOpt API for optimal sizing with system parameters for each power source. :return: """ if not self.site.urdb_label: raise ValueError("REopt run requires urdb_label") reopt = REopt(lat=self.site.lat, lon=self.site.lon, interconnection_limit_kw=self.interconnect_kw, load_profile=[0] * 8760, urdb_label=self.site.urdb_label, solar_model=self.solar, wind_model=self.wind, fin_model=self.grid.financial_model, fileout=str(self._fileout / "REoptResult.json")) results = reopt.get_reopt_results(force_download=False) wind_size_kw = results["outputs"]["Scenario"]["Site"]["Wind"][ "size_kw"] self.wind.system_capacity_closest_fit(wind_size_kw) solar_size_kw = results["outputs"]["Scenario"]["Site"]["PV"]["size_kw"] self.solar.system_capacity_kw = solar_size_kw logger.info("HybridSystem set system capacities to REopt output") def calculate_installed_cost(self): if not self.cost_model: raise RuntimeError( "'calculate_installed_cost' called before 'setup_cost_calculator'." ) solar_cost, wind_cost, total_cost = self.cost_model.calculate_total_costs( self.wind.system_capacity_kw / 1000, self.solar.system_capacity_kw / 1000) if self.solar: self.solar.set_total_installed_cost_dollars(solar_cost) if self.wind: self.wind.set_total_installed_cost_dollars(wind_cost) self.grid.set_total_installed_cost_dollars(total_cost) logger.info( "HybridSystem set hybrid total installed cost to to {}".format( total_cost)) def calculate_financials(self): """ prepare financial parameters from individual power plants for total performance and financial metrics """ # TODO: need to make financial parameters consistent # TODO: generalize this for different plants besides wind and solar hybrid_size_kw = sum( [v.system_capacity_kw for v in self.power_sources.values()]) solar_percent = self.solar.system_capacity_kw / hybrid_size_kw wind_percent = self.wind.system_capacity_kw / hybrid_size_kw def average_cost(var_name): hybrid_avg = 0 if self.solar: hybrid_avg += np.array( self.solar.financial_model.value(var_name)) * solar_percent if self.wind: hybrid_avg += np.array( self.wind.financial_model.value(var_name)) * wind_percent self.grid.financial_model.value(var_name, hybrid_avg) return hybrid_avg # FinancialParameters hybrid_construction_financing_cost = self.wind.get_construction_financing_cost() + \ self.solar.get_construction_financing_cost() self.grid.set_construction_financing_cost_per_kw( hybrid_construction_financing_cost / hybrid_size_kw) average_cost("debt_percent") # O&M Cost Averaging average_cost("om_capacity") average_cost("om_fixed") average_cost("om_fuel_cost") average_cost("om_production") average_cost("om_replacement_cost1") average_cost("cp_system_nameplate") # Tax Incentives average_cost("ptc_fed_amount") average_cost("ptc_fed_escal") average_cost("itc_fed_amount") average_cost("itc_fed_percent") self.grid.financial_model.Revenue.ppa_soln_mode = 1 # Depreciation, copy from solar for now self.grid.financial_model.Depreciation.assign( self.solar.financial_model.Depreciation.export()) average_cost("degradation") def simulate(self, project_life: int = 25): """ Runs the individual system models then combines the financials :return: """ self.calculate_installed_cost() self.calculate_financials() hybrid_size_kw = 0 total_gen = [0] * self.site.n_timesteps if self.solar.system_capacity_kw > 0: hybrid_size_kw += self.solar.system_capacity_kw self.solar.simulate(project_life) gen = self.solar.generation_profile() total_gen = [ total_gen[i] + gen[i] for i in range(self.site.n_timesteps) ] if self.wind.system_capacity_kw > 0: hybrid_size_kw += self.wind.system_capacity_kw self.wind.simulate(project_life) gen = self.wind.generation_profile() total_gen = [ total_gen[i] + gen[i] for i in range(self.site.n_timesteps) ] self.grid.generation_profile_from_system = total_gen self.grid.financial_model.SystemOutput.system_capacity = hybrid_size_kw self.grid.simulate(project_life) @property def annual_energies(self): aep = self.outputs_factory.create() if self.solar.system_capacity_kw > 0: aep.solar = self.solar.system_model.Outputs.annual_energy if self.wind.system_capacity_kw > 0: aep.wind = self.wind.system_model.Outputs.annual_energy aep.grid = sum(self.grid.system_model.Outputs.gen) aep.hybrid = aep.solar + aep.wind return aep @property def capacity_factors(self): cf = self.outputs_factory.create() if self.solar and self.solar.system_capacity_kw > 0: cf.solar = self.solar.system_model.Outputs.capacity_factor if self.wind and self.wind.system_capacity_kw > 0: cf.wind = self.wind.system_model.Outputs.capacity_factor try: cf.grid = self.grid.system_model.Outputs.capacity_factor_curtailment_ac except: cf.grid = self.grid.system_model.Outputs.capacity_factor_interconnect_ac cf.hybrid = (self.solar.annual_energy_kw() + self.wind.annual_energy_kw()) \ / (self.solar.system_capacity_kw + self.wind.system_capacity_kw) / 87.6 return cf @property def net_present_values(self): npv = self.outputs_factory.create() if self.solar and self.solar.system_capacity_kw > 0: npv.solar = self.solar.financial_model.Outputs.project_return_aftertax_npv if self.wind and self.wind.system_capacity_kw > 0: npv.wind = self.wind.financial_model.Outputs.project_return_aftertax_npv npv.hybrid = self.grid.financial_model.Outputs.project_return_aftertax_npv return npv @property def internal_rate_of_returns(self): irr = self.outputs_factory.create() if self.solar and self.solar.system_capacity_kw > 0: irr.solar = self.solar.financial_model.Outputs.project_return_aftertax_irr if self.wind and self.wind.system_capacity_kw > 0: irr.wind = self.wind.financial_model.Outputs.project_return_aftertax_irr irr.hybrid = self.grid.financial_model.Outputs.project_return_aftertax_irr return irr @property def lcoe_real(self): lcoes_real = self.outputs_factory.create() if self.solar and self.solar.system_capacity_kw > 0: lcoes_real.solar = self.solar.financial_model.Outputs.lcoe_real if self.wind and self.wind.system_capacity_kw > 0: lcoes_real.wind = self.wind.financial_model.Outputs.lcoe_real lcoes_real.hybrid = self.grid.financial_model.Outputs.lcoe_real return lcoes_real @property def lcoe_nom(self): lcoes_nom = self.outputs_factory.create() if self.solar and self.solar.system_capacity_kw > 0: lcoes_nom.solar = self.solar.financial_model.Outputs.lcoe_nom if self.wind and self.wind.system_capacity_kw > 0: lcoes_nom.wind = self.wind.financial_model.Outputs.lcoe_nom lcoes_nom.hybrid = self.grid.financial_model.Outputs.lcoe_nom return lcoes_nom def hybrid_outputs(self): outputs = dict() # outputs['Lat'] = self.site.lat # outputs['Lon'] = self.site.lon # outputs['PPA Price'] = self.hybrid_financial.Revenue.ppa_price_input[0] outputs['Solar (MW)'] = self.solar.system_capacity_kw / 1000 outputs['Wind (MW)'] = self.wind.system_capacity_kw / 1000 solar_pct = self.solar.system_capacity_kw / ( self.solar.system_capacity_kw + self.wind.system_capacity_kw) wind_pct = self.wind.system_capacity_kw / ( self.solar.system_capacity_kw + self.wind.system_capacity_kw) outputs['Solar (%)'] = solar_pct * 100 outputs['Wind (%)'] = wind_pct * 100 annual_energies = self.annual_energies outputs['Solar AEP (GWh)'] = annual_energies.solar / 1000000 outputs['Wind AEP (GWh)'] = annual_energies.wind / 1000000 outputs["AEP (GWh)"] = annual_energies.hybrid / 1000000 capacity_factors = self.capacity_factors outputs['Solar Capacity Factor'] = capacity_factors.solar outputs['Wind Capacity Factor'] = capacity_factors.wind outputs["Capacity Factor"] = capacity_factors.hybrid outputs['Capacity Factor of Interconnect'] = capacity_factors.grid outputs['Percentage Curtailment'] = ( self.grid.system_model.Outputs.annual_ac_curtailment_loss_percent + self.grid.system_model.Outputs.annual_ac_interconnect_loss_percent) outputs[ "BOS Cost"] = self.grid.financial_model.SystemCosts.total_installed_cost outputs['BOS Cost percent reduction'] = 0 outputs["Cost / MWh Produced"] = outputs["BOS Cost"] / ( outputs['AEP (GWh)'] * 1000) outputs["NPV ($-million)"] = self.net_present_values.hybrid / 1000000 outputs['IRR (%)'] = self.internal_rate_of_returns.hybrid outputs[ 'PPA Price Used'] = self.grid.financial_model.Revenue.ppa_price_input[ 0] outputs['LCOE - Real'] = self.lcoe_real.hybrid outputs['LCOE - Nominal'] = self.lcoe_nom.hybrid # time series dispatch if self.grid.financial_model.Revenue.ppa_multiplier_model == 1: outputs['Revenue (TOD)'] = sum( self.grid.financial_model.Outputs.cf_total_revenue) outputs['Revenue (PPA)'] = outputs['TOD Profile Used'] = 0 outputs['Cost / MWh Produced percent reduction'] = 0 if solar_pct * wind_pct > 0: outputs['Pearson R Wind V Solar'] = pearsonr( self.solar.system_model.Outputs.gen[0:8760], self.wind.system_model.Outputs.gen[0:8760])[0] return outputs def copy(self): """