From 960cb39e8b54ac0c59fc8b5a60829f6d912fc612 Mon Sep 17 00:00:00 2001 From: dmulash Date: Tue, 10 Sep 2024 13:51:46 -0600 Subject: [PATCH 1/3] initial manager.py soft capex changes --- ORBIT/manager.py | 296 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 264 insertions(+), 32 deletions(-) diff --git a/ORBIT/manager.py b/ORBIT/manager.py index 84232f63..889c23c1 100644 --- a/ORBIT/manager.py +++ b/ORBIT/manager.py @@ -323,11 +323,21 @@ def compile_input_dict(cls, phases): "project_lifetime": "yrs (optional, default: 25)", "discount_rate": "yearly (optional, default: .025)", "opex_rate": "$/kW/year (optional, default: 150)", - "construction_insurance": "$/kW (optional, default: 44)", - "construction_financing": "$/kW (optional, default: 183)", - "contingency": "$/kW (optional, default: 316)", - "commissioning": "$/kW (optional, default: 44)", - "decommissioning": "$/kW (optional, default: 58)", + "construction_insurance": "$/kW (optional, default: value calculated using construction_insurance_factor)", + "construction_insurance_factor": "float (optional, default: 0.0115)", + "decommissioning": "$/kW (optional, default: value calculated using decommissioning_factor)", + "decommissioning_factor": "float (optional, default: 0.1725)", + "project_completion": "$/kW (optional, default: value calculated using project_completion_factor)", + "project_completion_factor": "float (optional, default: 0.0115)", + "procurement_contingency": "$/kW (optional, default: value calculated using procurement_contingency_factor)", + "procurement_contingency_factor": "float (optional, default: 0.0575)", + "installation_contingency": "$/kW (optional, default: value calculated using installation_contingency_factor)", + "installation_contingency_factor": "float (optional, default: 0.345)", + "construction_financing": "$/kW (optional, default: value calculated using construction_financing_factor))", + "construction_financing_factor": "$/kW (optional, default: value calculated using spend_schedule, tax_rate and interest_during_construction))", + "spend_schedule": "dict (optional, default: {0: 0.25, 1: 0.25, 2: 0.3, 3: 0.1, 4: 0.1, 5: 0.0}", + "tax_rate": "float (optional, default: 0.26", + "interest_during_construction": "float (optional, default: 0.044", "site_auction_price": "$ (optional, default: 100e6)", "site_assessment_cost": "$ (optional, default: 50e6)", "construction_plan_cost": "$ (optional, default: 1e6)", @@ -974,8 +984,8 @@ def outputs(self, include_logs=False, npv_detailed=False): "system_capex_per_kw": self.system_capex_per_kw, "overnight_capex": self.overnight_capex, "overnight_capex_per_kw": self.overnight_capex_per_kw, - "soft_capex": self.soft_capex, - "soft_capex_per_kw": self.soft_capex_per_kw, + "soft_capex": sum(self.soft_capex.values()), + "soft_capex_per_kw": sum(self.soft_capex_per_kw.values()), "bos_capex": self.bos_capex, "bos_capex_per_kw": self.bos_capex_per_kw, "project_capex": self.project_capex, @@ -1372,7 +1382,9 @@ def capex_breakdown(self): outputs[name] = cost outputs["Turbine"] = self.turbine_capex - outputs["Soft"] = self.soft_capex + + outputs["Soft"] = sum(self.soft_capex.values()) + outputs["Project"] = self.project_capex return outputs @@ -1386,6 +1398,65 @@ def capex_breakdown_per_kw(self): for k, v in self.capex_breakdown.items() } + @property + def capex_detailed_soft_capex_breakdown(self): + """Returns CapEx breakdown by category with a detailed soft capex breakdown.""" + + unique = np.unique( + [*self.system_costs.keys(), *self.installation_costs.keys()] + ) + categories = {} + + for phase in unique: + for base, cat in self._capex_categories.items(): + if base in phase: + categories[phase] = cat + break + + missing = list(set(unique).difference([*categories])) + if missing: + print( + f"Warning: CapEx category not found for {missing}. " + f"Added to 'Misc.'" + ) + + for phase in missing: + categories[phase] = "Misc." + + outputs = {} + for phase, cost in self.system_costs.items(): + name = categories[phase] + if name in outputs: + outputs[name] += cost + + else: + outputs[name] = cost + + for phase, cost in self.installation_costs.items(): + name = categories[phase] + " Installation" + if name in outputs: + outputs[name] += cost + + else: + outputs[name] = cost + + outputs["Turbine"] = self.turbine_capex + + outputs = {**outputs, **self.soft_capex} + + outputs["Project"] = self.project_capex + + return outputs + + @property + def capex_detailed_soft_capex_breakdown_per_kw(self): + """Returns CapEx per kW breakdown by category with a detailed soft capex breakdown.""" + + return { + k: v / (self.capacity * 1000) + for k, v in self.capex_detailed_soft_capex_breakdown.items() + } + @property def bos_capex(self): """Returns total balance of system CapEx.""" @@ -1437,39 +1508,200 @@ def overnight_capex(self): return self.system_capex + self.turbine_capex @property - def soft_capex(self): - """Returns total project cost costs.""" + def construction_insurance_capex(self): + """ + Returns the construction insurance capital cost of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review. + """ try: - capex = self.soft_capex_per_kw * self.capacity * 1000 + construction_insurance_per_kW = self.config["project_parameters"]["construction_insurance"] + num_turbines = self.config["plant"]["num_turbines"] + rating = self.config["turbine"]["turbine_rating"] + construction_insurance = construction_insurance_per_kW * num_turbines * rating * 1000 + + except: + contruction_insurance_factor = self.project_params.get("construction_insurance_factor", 0.0115) + construction_insurance = (self.turbine_capex + self.bos_capex + self.project_capex) *\ + contruction_insurance_factor - except TypeError: - capex = None + return construction_insurance - return capex + @property + def decommissioning_capex(self): + """ + Returns the decommissioning capital cost of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review. + """ + + try: + decommissioning_per_kW = self.config["project_parameters"]["decommissioning"] + num_turbines = self.config["plant"]["num_turbines"] + rating = self.config["turbine"]["turbine_rating"] + decommissioning = decommissioning_per_kW * num_turbines * rating * 1000 + + except: + decommissioning_factor = self.project_params.get("decommissioning_factor", 0.175) + decommissioning = self.installation_capex * decommissioning_factor + + return decommissioning @property - def soft_capex_per_kw(self): + def project_completion_capex(self): """ - Returns project soft costs per kW. Default numbers are based on the - Cost of Energy Review (Stehly and Beiter 2018). + Returns the project completion capital cost of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review. """ - insurance = self.project_params.get("construction_insurance", 44) - financing = self.project_params.get("construction_financing", 183) - contingency = self.project_params.get("contingency", 316) - commissioning = self.project_params.get("commissioning", 44) - decommissioning = self.project_params.get("decommissioning", 58) + try: + project_completion_per_kW = self.config["project_parameters"]["project_completion"] + num_turbines = self.config["plant"]["num_turbines"] + rating = self.config["turbine"]["turbine_rating"] + project_completion = project_completion_per_kW * num_turbines * rating * 1000 + + except: + project_completion_factor = self.project_params.get("project_completion_factor", 0.0115) + project_completion = (self.turbine_capex + self.bos_capex + self.project_capex) *\ + project_completion_factor - return sum( - [ - insurance, - financing, - contingency, - commissioning, - decommissioning, - ], - ) + return project_completion + + @property + def procurement_contingency_capex(self): + """ + Returns the procurement contingency capital cost of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review. + """ + + try: + procurement_contingency_per_kW = self.config["project_parameters"]["procurement_contingency"] + num_turbines = self.config["plant"]["num_turbines"] + rating = self.config["turbine"]["turbine_rating"] + procurement_contingency = procurement_contingency_per_kW * num_turbines * rating * 1000 + + except: + procurement_contingency_factor = self.project_params.get("procurement_contingency_factor", 0.0575) + procurement_contingency = (self.turbine_capex + self.bos_capex + self.project_capex - self.installation_capex) *\ + procurement_contingency_factor + + return procurement_contingency + + @property + def installation_contingency_capex(self): + """ + Returns the installation contingency capital cost of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review. + """ + + try: + installation_contingency_per_kW = self.config["project_parameters"]["installation_contingency"] + num_turbines = self.config["plant"]["num_turbines"] + rating = self.config["turbine"]["turbine_rating"] + installation_contingency = installation_contingency_per_kW * num_turbines * rating * 1000 + + except: + installation_contingency_factor = self.project_params.get("installation_contingency_factor", 0.345) + installation_contingency = self.installation_capex * installation_contingency_factor + + return installation_contingency + + @property + def construction_financing_factor(self): + """ + Returns the construction finaning factor of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review, + except the spend schedule, which is sourced from collaborations with industry. + """ + + try: + spend_schedule = self.config["project_parameters"]["spend_schedule"] + except: + spend_schedule = self.project_params.get("spend_schedule", {0: 0.25, + 1: 0.25, + 2: 0.3, + 3: 0.1, + 4: 0.1, + 5: 0.0 + }) + + try: + tax_rate = self.config["project_parameters"]["tax_rate"] + except: + tax_rate = self.project_params.get("tax_rate", 0.26) + + try: + interest_during_construction = self.config["project_parameters"]["interest_during_construction"] + except: + interest_during_construction = self.project_params.get("interest_during_construction", 0.044) + + + _check = 0 + _construction_financing_factor = 0 + + for key, val in spend_schedule.items(): + _check += val + + _construction_financing_factor += val *\ + (1 + (1 - tax_rate) * ((1 + interest_during_construction) **\ + (key + 0.5) - 1)) + if _check != 1.0: + raise Exception("Values in spend_schedule must sum to 1.0") + + return _construction_financing_factor + + @property + def construction_financing_capex(self): + """ + Returns the construction financing capital cost of the project. + Methodology from ORCA model, default values used in 2022 Cost of Wind Energy Review. + """ + + try: + construction_financing_per_kW = self.config["project_parameters"]["construction_financing"] + num_turbines = self.config["plant"]["num_turbines"] + rating = self.config["turbine"]["turbine_rating"] + construction_financing = construction_financing_per_kW * num_turbines * rating * 1000 + + except: + construction_financing_factor = self.project_params.get("construction_financing_factor", self.construction_financing_factor) + construction_financing = (self.construction_insurance_capex +\ + self.decommissioning_capex +\ + self.project_completion_capex +\ + self.procurement_contingency_capex +\ + self.installation_contingency_capex +\ + self.bos_capex + self.turbine_capex) * (construction_financing_factor - 1) + + + return construction_financing + + @property + def soft_capex_breakdown(self): + """Returns soft cost breakdown.""" + + soft_capex = {"Construction Insurance": self.construction_insurance_capex, + "Decommissioning": self.decommissioning_capex, + "Project Completion": self.project_completion_capex, + "Procurement Contingency": self.procurement_contingency_capex, + "Installation Contingency": self.installation_contingency_capex, + "Construction Financing": self.construction_financing_capex + } + + return soft_capex + + @property + def soft_capex(self): + """Returns soft cost breakdown.""" + + return soft_capex + + @property + def soft_capex_per_kw(self): + """Returns Soft Cost CapEx per kW breakdown by category.""" + + return { + k: v / (self.capacity * 1000) + for k, v in self.soft_capex.items() + } @property def project_capex(self): @@ -1515,7 +1747,7 @@ def total_capex(self): return ( self.bos_capex + self.turbine_capex - + self.soft_capex + + sum(self.soft_capex.values()) + self.project_capex ) From c921f42d4886011242abc10f16654f24f03b9bed Mon Sep 17 00:00:00 2001 From: dmulash Date: Tue, 10 Sep 2024 14:21:19 -0600 Subject: [PATCH 2/3] update manager.py keeping float soft_capex output --- ORBIT/manager.py | 87 ++++++++++++++++++++++++++---------------------- 1 file changed, 47 insertions(+), 40 deletions(-) diff --git a/ORBIT/manager.py b/ORBIT/manager.py index 889c23c1..ae1f2c19 100644 --- a/ORBIT/manager.py +++ b/ORBIT/manager.py @@ -324,20 +324,20 @@ def compile_input_dict(cls, phases): "discount_rate": "yearly (optional, default: .025)", "opex_rate": "$/kW/year (optional, default: 150)", "construction_insurance": "$/kW (optional, default: value calculated using construction_insurance_factor)", - "construction_insurance_factor": "float (optional, default: 0.0115)", - "decommissioning": "$/kW (optional, default: value calculated using decommissioning_factor)", - "decommissioning_factor": "float (optional, default: 0.1725)", - "project_completion": "$/kW (optional, default: value calculated using project_completion_factor)", - "project_completion_factor": "float (optional, default: 0.0115)", + "construction_financing": "$/kW (optional, default: value calculated using construction_financing_factor))", "procurement_contingency": "$/kW (optional, default: value calculated using procurement_contingency_factor)", - "procurement_contingency_factor": "float (optional, default: 0.0575)", "installation_contingency": "$/kW (optional, default: value calculated using installation_contingency_factor)", - "installation_contingency_factor": "float (optional, default: 0.345)", - "construction_financing": "$/kW (optional, default: value calculated using construction_financing_factor))", + "decommissioning": "$/kW (optional, default: value calculated using decommissioning_factor)", + "project_completion": "$/kW (optional, default: value calculated using project_completion_factor)", + "construction_insurance_factor": "float (optional, default: 0.0115)", "construction_financing_factor": "$/kW (optional, default: value calculated using spend_schedule, tax_rate and interest_during_construction))", "spend_schedule": "dict (optional, default: {0: 0.25, 1: 0.25, 2: 0.3, 3: 0.1, 4: 0.1, 5: 0.0}", "tax_rate": "float (optional, default: 0.26", "interest_during_construction": "float (optional, default: 0.044", + "procurement_contingency_factor": "float (optional, default: 0.0575)", + "installation_contingency_factor": "float (optional, default: 0.345)", + "decommissioning_factor": "float (optional, default: 0.1725)", + "project_completion_factor": "float (optional, default: 0.0115)", "site_auction_price": "$ (optional, default: 100e6)", "site_assessment_cost": "$ (optional, default: 50e6)", "construction_plan_cost": "$ (optional, default: 1e6)", @@ -984,8 +984,8 @@ def outputs(self, include_logs=False, npv_detailed=False): "system_capex_per_kw": self.system_capex_per_kw, "overnight_capex": self.overnight_capex, "overnight_capex_per_kw": self.overnight_capex_per_kw, - "soft_capex": sum(self.soft_capex.values()), - "soft_capex_per_kw": sum(self.soft_capex_per_kw.values()), + "soft_capex": self.soft_capex, + "soft_capex_per_kw": self.soft_capex_per_kw, "bos_capex": self.bos_capex, "bos_capex_per_kw": self.bos_capex_per_kw, "project_capex": self.project_capex, @@ -1383,7 +1383,7 @@ def capex_breakdown(self): outputs["Turbine"] = self.turbine_capex - outputs["Soft"] = sum(self.soft_capex.values()) + outputs["Soft"] = self.soft_capex outputs["Project"] = self.project_capex @@ -1442,7 +1442,7 @@ def capex_detailed_soft_capex_breakdown(self): outputs["Turbine"] = self.turbine_capex - outputs = {**outputs, **self.soft_capex} + outputs = {**outputs, **self.soft_capex_breakdown} outputs["Project"] = self.project_capex @@ -1507,6 +1507,40 @@ def overnight_capex(self): return self.system_capex + self.turbine_capex + @property + def soft_capex(self): + """Returns Total Soft CapEx.""" + + return sum(self.soft_capex_breakdown.values()) + + @property + def soft_capex_per_kw(self): + """Returns Total Soft CapEx per kW.""" + + try: + capex = sum(self.soft_capex_breakdown.values()) / (self.capacity * 1000) + + except TypeError: + capex = None + + return capex + + @property + def soft_capex_breakdown(self): + """Returns soft cost breakdown.""" + + soft_capex = {"Construction Insurance": self.construction_insurance_capex, + "Decommissioning": self.decommissioning_capex, + "Project Completion": self.project_completion_capex, + "Procurement Contingency": self.procurement_contingency_capex, + "Installation Contingency": self.installation_contingency_capex, + "Construction Financing": self.construction_financing_capex + } + + return soft_capex + + + @property def construction_insurance_capex(self): """ @@ -1674,34 +1708,7 @@ def construction_financing_capex(self): return construction_financing - @property - def soft_capex_breakdown(self): - """Returns soft cost breakdown.""" - - soft_capex = {"Construction Insurance": self.construction_insurance_capex, - "Decommissioning": self.decommissioning_capex, - "Project Completion": self.project_completion_capex, - "Procurement Contingency": self.procurement_contingency_capex, - "Installation Contingency": self.installation_contingency_capex, - "Construction Financing": self.construction_financing_capex - } - - return soft_capex - - @property - def soft_capex(self): - """Returns soft cost breakdown.""" - return soft_capex - - @property - def soft_capex_per_kw(self): - """Returns Soft Cost CapEx per kW breakdown by category.""" - - return { - k: v / (self.capacity * 1000) - for k, v in self.soft_capex.items() - } @property def project_capex(self): @@ -1747,7 +1754,7 @@ def total_capex(self): return ( self.bos_capex + self.turbine_capex - + sum(self.soft_capex.values()) + + self.soft_capex + self.project_capex ) From 3e3a910146923fbf5c786adb0035333b9da15661 Mon Sep 17 00:00:00 2001 From: Mulas Hernando Date: Wed, 9 Oct 2024 15:31:51 -0600 Subject: [PATCH 3/3] update test soft costs --- .pre-commit-config.yaml | 2 +- ORBIT/manager.py | 2 +- tests/test_project_manager.py | 73 ++++++++++++++++++++++++++++++++--- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f315b7db..7c5f075f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: hooks: - id: black name: black - stages: [commit] + stages: [pre-commit] exclude: ^ORBIT/api/wisdem - repo: https://github.com/pre-commit/pre-commit-hooks diff --git a/ORBIT/manager.py b/ORBIT/manager.py index ae1f2c19..dcc31a70 100644 --- a/ORBIT/manager.py +++ b/ORBIT/manager.py @@ -327,7 +327,7 @@ def compile_input_dict(cls, phases): "construction_financing": "$/kW (optional, default: value calculated using construction_financing_factor))", "procurement_contingency": "$/kW (optional, default: value calculated using procurement_contingency_factor)", "installation_contingency": "$/kW (optional, default: value calculated using installation_contingency_factor)", - "decommissioning": "$/kW (optional, default: value calculated using decommissioning_factor)", + "decommissioning": "$/kW (optional, default: value calculated using decommissioning_factor)", "project_completion": "$/kW (optional, default: value calculated using project_completion_factor)", "construction_insurance_factor": "float (optional, default: 0.0115)", "construction_financing_factor": "$/kW (optional, default: value calculated using spend_schedule, tax_rate and interest_during_construction))", diff --git a/tests/test_project_manager.py b/tests/test_project_manager.py index 140f36f6..575eb187 100644 --- a/tests/test_project_manager.py +++ b/tests/test_project_manager.py @@ -852,32 +852,93 @@ def test_soft_costs(): config = deepcopy(complete_project) config["project_parameters"] = {"construction_insurance": 50} project = ProjectManager(config) + project.run() assert project.soft_capex != baseline config = deepcopy(complete_project) config["project_parameters"] = {"construction_financing": 190} project = ProjectManager(config) + project.run() assert project.soft_capex != baseline config = deepcopy(complete_project) - config["project_parameters"] = {"contingency": 320} + config["project_parameters"] = {"procurement_contingency": 320} project = ProjectManager(config) + project.run() assert project.soft_capex != baseline config = deepcopy(complete_project) - config["project_parameters"] = {"contingency": 320} + config["project_parameters"] = {"installation_contingency": 320} project = ProjectManager(config) + project.run() assert project.soft_capex != baseline config = deepcopy(complete_project) - config["project_parameters"] = {"commissioning": 50} + config["project_parameters"] = {"decommissioning": 50} project = ProjectManager(config) + project.run() assert project.soft_capex != baseline config = deepcopy(complete_project) - config["project_parameters"] = {"decommissioning": 50} + config["project_parameters"] = {"project_completion": 50} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"construction_insurance_factor": 0.02} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"construction_financing_factor": 1.2} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"spend_schedule": {0: 0, 1: 0, 2: 0, 3: 0, 4: 0.5, 5: 0.5}} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"tax_rate": 0.35} project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"interest_during_construction": 0.06} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"procurement_contingency_factor": 0.1} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"project_completion_factor": 0.3} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"installation_contingency_factor": 0.7} + project = ProjectManager(config) + project.run() + assert project.soft_capex != baseline + + config = deepcopy(complete_project) + config["project_parameters"] = {"decommissioning_factor": 0.7} + project = ProjectManager(config) + project.run() assert project.soft_capex != baseline + def test_project_costs(): @@ -932,9 +993,9 @@ def test_total_capex(): fix_project = ProjectManager(complete_project) fix_project.run() - assert fix_project.total_capex == pytest.approx(1207278397.56, abs=1e-1) + assert fix_project.total_capex == pytest.approx(1216449001.7410436, abs=1e-1) flt_project = ProjectManager(complete_floating_project) flt_project.run() - assert flt_project.total_capex == pytest.approx(3284781912.73, abs=1e-1) + assert flt_project.total_capex == pytest.approx(3540761314.148985, abs=1e-1)