예제 #1
0
파일: test_wind.py 프로젝트: jlcox119/HOPP
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)
예제 #2
0
파일: test_wind.py 프로젝트: jlcox119/HOPP
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)
예제 #3
0
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)
예제 #4
0
파일: test_wind.py 프로젝트: jlcox119/HOPP
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)
예제 #5
0
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)
예제 #6
0
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)
예제 #7
0
파일: test_wind.py 프로젝트: jlcox119/HOPP
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)
예제 #8
0
    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)
예제 #9
0
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)
예제 #10
0
파일: test_layout.py 프로젝트: NREL/HOPP
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)
예제 #11
0
파일: test_wind.py 프로젝트: jlcox119/HOPP
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)
예제 #12
0
파일: test_layout.py 프로젝트: NREL/HOPP
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)
예제 #13
0
파일: test_layout.py 프로젝트: NREL/HOPP
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)
예제 #14
0
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)
예제 #15
0
파일: run_reopt.py 프로젝트: NREL/HOPP
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
예제 #16
0
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):
        """