diff --git a/hopp/simulation/hybrid_simulation.py b/hopp/simulation/hybrid_simulation.py index 457a9c76e..e209088dc 100644 --- a/hopp/simulation/hybrid_simulation.py +++ b/hopp/simulation/hybrid_simulation.py @@ -311,8 +311,11 @@ def setup_cost_calculator(self, cost_calculator: object): def set_om_costs_per_kw(self, pv_om_per_kw=None, wind_om_per_kw=None, tower_om_per_kw=None, trough_om_per_kw=None, - wave_om_per_kw=None, + wave_om_per_kw=None, battery_om_per_kw=None, hybrid_om_per_kw=None): + """ + Sets Capacity-based O&M amount for each technology [$/kWcap]. + """ # TODO: Remove??? This doesn't seem to be used. # TODO: fix this error statement it doesn't work # om_vals = [pv_om_per_kw, wind_om_per_kw, tower_om_per_kw, trough_om_per_kw, wave_om_per_kw, hybrid_om_per_kw] @@ -320,7 +323,6 @@ def set_om_costs_per_kw(self, pv_om_per_kw=None, wind_om_per_kw=None, # om_lengths = {tech + "_om_per_kw" : om_val for om_val, tech in zip(om_vals, techs)} # if len(set(om_lengths.values())) != 1 and len(set(om_lengths.values())) is not None: # raise ValueError(f"Length of yearly om cost per kw arrays must be equal. Some lengths of om_per_kw values are different from others: {om_lengths}") - if pv_om_per_kw and self.pv: self.pv.om_capacity = pv_om_per_kw @@ -336,6 +338,9 @@ def set_om_costs_per_kw(self, pv_om_per_kw=None, wind_om_per_kw=None, if wave_om_per_kw and self.wave: self.wave.om_capacity = wave_om_per_kw + if battery_om_per_kw and self.battery: + self.battery.om_capacity = battery_om_per_kw + if hybrid_om_per_kw: self.grid.om_capacity = hybrid_om_per_kw @@ -382,16 +387,21 @@ def calculate_installed_cost(self): battery_mwh = self.battery.system_capacity_kwh / 1000 # TODO: add tower and trough to cost_model functionality - pv_cost, wind_cost, storage_cost, total_cost = self.cost_model.calculate_total_costs(wind_mw, - pv_mw, - battery_mw, - battery_mwh) + # pv_cost, wind_cost, storage_cost, total_cost = self.cost_model.calculate_total_costs(wind_mw, + # pv_mw, + # battery_mw, + # battery_mwh) + total_cost = 0 + if self.pv: - self.pv.total_installed_cost = pv_cost + self.pv.total_installed_cost = self.pv.calculate_total_installed_cost() + total_cost += self.pv.total_installed_cost if self.wind: - self.wind.total_installed_cost = wind_cost + self.wind.total_installed_cost = self.wind.calculate_total_installed_cost() + total_cost += self.wind.total_installed_cost if self.battery: - self.battery.total_installed_cost = storage_cost + self.battery.total_installed_cost = self.battery.calculate_total_installed_cost() + total_cost += self.battery.total_installed_cost if self.wave: self.wave.total_installed_cost = self.wave.calculate_total_installed_cost() total_cost += self.wave.total_installed_cost diff --git a/hopp/simulation/technologies/battery/battery.py b/hopp/simulation/technologies/battery/battery.py index f044d593c..e34c16897 100644 --- a/hopp/simulation/technologies/battery/battery.py +++ b/hopp/simulation/technologies/battery/battery.py @@ -344,6 +344,29 @@ def validate_replacement_inputs(self, project_life): self._financial_model.value('batt_bank_replacement', [0] + list(self._financial_model.value('batt_bank_replacement'))) else: raise ValueError(f"Error in Battery model: `batt_bank_replacement` should be length of project_life {project_life} but is instead {len(self._financial_model.value('batt_bank_replacement'))}") + + def set_overnight_capital_cost(self, energy_capital_cost, power_capital_cost): + """Set overnight capital costs [$/kW]. + + This method calculates and sets the overnight capital cost based on the given energy and power capital costs. + + Args: + energy_capital_cost (float): The capital cost per unit of energy capacity [$/kWh]. + power_capital_cost (float): The capital cost per unit of power capacity [$/kW]. + + Returns: + None: This method does not return any value. The calculated overnight capital cost is stored internally. + + Note: + The overnight capital cost is calculated using the formula: + `overnight_capital_cost = (energy_capital_cost * hours) + power_capital_cost` + where `hours` is the ratio of energy capacity to power capacity. + + Example: + >>> set_overnight_capital_cost(1500, 500) + """ + hours = self.system_capacity_kwh/self.system_capacity_kw + self._overnight_capital_cost = (energy_capital_cost * hours) + power_capital_cost def simulate_financials( self, diff --git a/hopp/simulation/technologies/power_source.py b/hopp/simulation/technologies/power_source.py index 96ece7a9f..13c0f9c04 100644 --- a/hopp/simulation/technologies/power_source.py +++ b/hopp/simulation/technologies/power_source.py @@ -332,6 +332,16 @@ def simulate(self, interconnect_kw: float, project_life: int = 25, lifetime_sim= self.simulate_financials(interconnect_kw, project_life) logger.info(f"{self.name} simulation executed with AEP {self.annual_energy_kwh}") + def set_overnight_capital_cost(self, overnight_capital_cost): + """Set overnight capital costs [$/kW].""" + self._overnight_capital_cost = overnight_capital_cost + + def calculate_total_installed_cost(self) -> float: + if isinstance(self._financial_model, Singleowner.Singleowner): + return self._financial_model.SystemCosts.total_installed_cost + else: + total_installed_cost = self.system_capacity_kw * self._overnight_capital_cost + return self._financial_model.value("total_installed_cost", total_installed_cost) # # Inputs # diff --git a/tests/hopp/test_hybrid.py b/tests/hopp/test_hybrid.py index 737f6e939..a820010af 100644 --- a/tests/hopp/test_hybrid.py +++ b/tests/hopp/test_hybrid.py @@ -267,6 +267,102 @@ def test_hybrid_pv_only(hybrid_config): assert npvs.pv == approx(-5121293, 1e3) assert npvs.hybrid == approx(-5121293, 1e3) +def test_hybrid_pv_only_custom_fin(hybrid_config, subtests): + solar_only = { + 'pv': { + 'system_capacity_kw': 5000, + 'layout_params': { + 'x_position': 0.5, + 'y_position': 0.5, + 'aspect_power': 0, + 'gcr': 0.5, + 's_buffer': 2, + 'x_buffer': 2, + }, + 'dc_degradation': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'fin_model': DEFAULT_FIN_CONFIG + }, + 'grid':{ + 'interconnect_kw': interconnection_size_kw, + 'fin_model': DEFAULT_FIN_CONFIG, + } + } + hybrid_config["technologies"] = solar_only + hi = HoppInterface(hybrid_config) + + hybrid_plant = hi.system + hybrid_plant.pv.set_overnight_capital_cost(400) + hybrid_plant.set_om_costs_per_kw(pv_om_per_kw=20) + + hi.simulate() + + aeps = hybrid_plant.annual_energies + npvs = hybrid_plant.net_present_values + cf = hybrid_plant.capacity_factors + + with subtests.test("total installed cost"): + assert hybrid_plant.pv.total_installed_cost == approx(2000000,1e-3) + + with subtests.test("om cost"): + assert hybrid_plant.pv.om_capacity == (20,) + + with subtests.test("capacity factor"): + assert cf.hybrid == approx(cf.pv) + + with subtests.test("aep"): + assert aeps.pv == approx(9884106.55, 1e-3) + assert aeps.hybrid == aeps.pv + +def test_hybrid_pv_battery_custom_fin(hybrid_config, subtests): + tech = { + 'pv': { + 'system_capacity_kw': 5000, + 'layout_params': { + 'x_position': 0.5, + 'y_position': 0.5, + 'aspect_power': 0, + 'gcr': 0.5, + 's_buffer': 2, + 'x_buffer': 2, + }, + 'dc_degradation': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + 'fin_model': DEFAULT_FIN_CONFIG + }, + 'battery': { + 'system_capacity_kw': 5000, + 'system_capacity_kwh': 20000, + 'fin_model': DEFAULT_FIN_CONFIG + }, + 'grid':{ + 'interconnect_kw': interconnection_size_kw, + 'fin_model': DEFAULT_FIN_CONFIG, + } + } + hybrid_config["technologies"] = tech + hi = HoppInterface(hybrid_config) + + hybrid_plant = hi.system + hybrid_plant.pv.set_overnight_capital_cost(400) + hybrid_plant.battery.set_overnight_capital_cost(300,200) + hybrid_plant.set_om_costs_per_kw(pv_om_per_kw=20,battery_om_per_kw=30) + + hi.simulate() + + aeps = hybrid_plant.annual_energies + npvs = hybrid_plant.net_present_values + cf = hybrid_plant.capacity_factors + + with subtests.test("pv total installed cost"): + assert hybrid_plant.pv.total_installed_cost == approx(2000000,1e-3) + + with subtests.test("pv om cost"): + assert hybrid_plant.pv.om_capacity == (20,) + + with subtests.test("battery total installed cost"): + assert hybrid_plant.battery.total_installed_cost == approx(7000000,1e-3) + + with subtests.test("battery om cost"): + assert hybrid_plant.battery.om_capacity == (30,) 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"):