Skip to content

Commit d74ef75

Browse files
Merge pull request #112 from softwareengineerprogrammer/sam-em-multi-year-construction_v3
SAM-EM Multiple Construction Years [v3.10]
2 parents 8cbf801 + fc7b3d3 commit d74ef75

40 files changed

+2529
-389
lines changed

.bumpversion.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[bumpversion]
2-
current_version = 3.9.65
2+
current_version = 3.10.22
33
commit = True
44
tag = True
55

.cookiecutterrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ default_context:
5454
sphinx_doctest: "no"
5555
sphinx_theme: "sphinx-py3doc-enhanced-theme"
5656
test_matrix_separate_coverage: "no"
57-
version: 3.9.65
57+
version: 3.10.22
5858
version_manager: "bump2version"
5959
website: "https://github.com/NREL"
6060
year_from: "2023"

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# pre-commit install --install-hooks
33
# To update the versions:
44
# pre-commit autoupdate
5-
exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|src/geophires_x(?!/(GEOPHIRESv3|EconomicsSam|EconomicsSamCashFlow)\.py))(/|$)'
5+
exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg|src/geophires_x(?!/(GEOPHIRESv3|EconomicsSam|EconomicsSamCashFlow|EconomicsUtils|EconomicsSamPreRevenue|SurfacePlantUtils)\.py))(/|$)'
66
# Note the order is intentional to avoid multiple passes of the hooks
77
repos:
88
- repo: https://github.com/astral-sh/ruff-pre-commit

README.rst

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ Free software: `MIT license <LICENSE>`__
5858
:alt: Supported implementations
5959
:target: https://pypi.org/project/geophires-x
6060

61-
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.9.65.svg
61+
.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.10.22.svg
6262
:alt: Commits since latest release
63-
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.9.65...main
63+
:target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.10.22...main
6464

6565
.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
6666
:target: https://nrel.github.io/GEOPHIRES-X
@@ -316,6 +316,10 @@ Example-specific web interface deeplinks are listed in the Link column.
316316
- `example_SAM-single-owner-PPA-4.txt <tests/examples/example_SAM-single-owner-PPA-4.txt>`__
317317
- `.out <tests/examples/example_SAM-single-owner-PPA-4.out>`__
318318
- `link <https://gtp.scientificwebservices.com/geophires?geophires-example-id=example_SAM-single-owner-PPA-4>`__
319+
* - SAM Single Owner PPA: Multiple Construction Years
320+
- `example_SAM-single-owner-PPA-5.txt <tests/examples/example_SAM-single-owner-PPA-5.txt>`__
321+
- `.out <tests/examples/example_SAM-single-owner-PPA-5.out>`__
322+
- `link <https://gtp.scientificwebservices.com/geophires?geophires-example-id=example_SAM-single-owner-PPA-5>`__
319323
.. raw:: html
320324

321325
<embed>

docs/SAM-Economic-Models.md

Lines changed: 36 additions & 26 deletions
Large diffs are not rendered by default.

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
year = '2025'
1919
author = 'NREL'
2020
copyright = f'{year}, {author}'
21-
version = release = '3.9.65'
21+
version = release = '3.10.22'
2222

2323
pygments_style = 'trac'
2424
templates_path = ['./templates']

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def read(*names, **kwargs):
1313

1414
setup(
1515
name='geophires-x',
16-
version='3.9.65',
16+
version='3.10.22',
1717
license='MIT',
1818
description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.',
1919
long_description='{}\n{}'.format(

src/geophires_x/Economics.py

Lines changed: 110 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
from geophires_x.EconomicsUtils import BuildPricingModel, wacc_output_parameter, nominal_discount_rate_parameter, \
1414
real_discount_rate_parameter, after_tax_irr_parameter, moic_parameter, project_vir_parameter, \
1515
project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \
16-
total_capex_parameter_output_parameter
16+
interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \
17+
overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \
18+
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET
1719
from geophires_x.GeoPHIRESUtils import quantity
1820
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
1921
_WellDrillingCostCorrelationCitation
2022
from geophires_x.Parameter import intParameter, floatParameter, OutputParameter, ReadParameter, boolParameter, \
21-
coerce_int_params_to_enum_values
23+
coerce_int_params_to_enum_values, listParameter, Parameter
24+
from geophires_x.SurfacePlantUtils import MAX_CONSTRUCTION_YEARS
2225
from geophires_x.Units import *
2326
from geophires_x.WellBores import calculate_total_drilling_lengths_m
2427

@@ -1004,6 +1007,17 @@ def __init__(self, model: Model):
10041007
"increases a 4% rate (0.04) to 4.1% (0.041) in the next year."
10051008
)
10061009

1010+
self.royalty_escalation_rate_start_year = self.ParameterDict[self.royalty_escalation_rate_start_year.Name] = intParameter(
1011+
'Royalty Rate Escalation Start Year',
1012+
DefaultValue=1,
1013+
AllowableRange=list(range(1, model.surfaceplant.plant_lifetime.AllowableRange[-1], 1)),
1014+
UnitType=Units.PERCENT,
1015+
PreferredUnits=PercentUnit.TENTH,
1016+
CurrentUnits=PercentUnit.TENTH,
1017+
ToolTipText=f'The first year that the {self.royalty_escalation_rate.Name} is applied. '
1018+
f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}.'
1019+
)
1020+
10071021
maximum_royalty_rate_default_val = 1.0
10081022
self.maximum_royalty_rate = self.ParameterDict[self.maximum_royalty_rate.Name] = floatParameter(
10091023
'Royalty Rate Maximum',
@@ -1050,28 +1064,43 @@ def __init__(self, model: Model):
10501064
'See https://github.com/NREL/GEOPHIRES-X/discussions/344 for further details.'
10511065
)
10521066

1067+
default_fraction_in_bonds = 0.5
10531068
self.FIB = self.ParameterDict[self.FIB.Name] = floatParameter(
10541069
"Fraction of Investment in Bonds",
1055-
DefaultValue=0.5,
1070+
DefaultValue=default_fraction_in_bonds,
10561071
Min=0.0,
10571072
Max=1.0,
10581073
UnitType=Units.PERCENT,
10591074
PreferredUnits=PercentUnit.TENTH,
10601075
CurrentUnits=PercentUnit.TENTH,
1061-
ErrMessage="assume default fraction of investment in bonds (0.5)",
1062-
ToolTipText="Fraction of geothermal project financing through bonds (debt)."
1076+
ErrMessage=f"assume default fraction of investment in bonds ({default_fraction_in_bonds})",
1077+
ToolTipText="Fraction of geothermal project financing through bonds (debt/loans)."
10631078
)
1079+
1080+
default_bond_interest_rate = 0.05
10641081
self.BIR = self.ParameterDict[self.BIR.Name] = floatParameter(
10651082
"Inflated Bond Interest Rate",
1066-
DefaultValue=0.05,
1083+
DefaultValue=default_bond_interest_rate,
1084+
Min=0.0,
1085+
Max=1.0,
1086+
UnitType=Units.PERCENT,
1087+
PreferredUnits=PercentUnit.TENTH,
1088+
CurrentUnits=PercentUnit.TENTH,
1089+
ErrMessage=f"assume default inflated bond interest rate ({default_bond_interest_rate})",
1090+
ToolTipText="Inflated bond interest rate (for debt/loans)"
1091+
)
1092+
1093+
self.bond_interest_rate_during_construction = self.ParameterDict[self.bond_interest_rate_during_construction.Name] = floatParameter(
1094+
'Inflated Bond Interest Rate During Construction',
1095+
DefaultValue=self.BIR.DefaultValue,
10671096
Min=0.0,
10681097
Max=1.0,
10691098
UnitType=Units.PERCENT,
10701099
PreferredUnits=PercentUnit.TENTH,
10711100
CurrentUnits=PercentUnit.TENTH,
1072-
ErrMessage="assume default inflated bond interest rate (0.05)",
1073-
ToolTipText="Inflated bond interest rate (see docs)"
1101+
ToolTipText='Inflated bond interest rate during construction (for debt/loans)'
10741102
)
1103+
10751104
self.EIR = self.ParameterDict[self.EIR.Name] = floatParameter(
10761105
"Inflated Equity Interest Rate",
10771106
DefaultValue=0.1,
@@ -1155,6 +1184,50 @@ def __init__(self, model: Model):
11551184
'calculated automatically by compounding Inflation Rate over Construction Years.'
11561185
)
11571186

1187+
self.construction_capex_schedule = self.ParameterDict[self.construction_capex_schedule.Name] = listParameter(
1188+
CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME,
1189+
DefaultValue=[1.],
1190+
Min=0.0,
1191+
Max=1.0,
1192+
ToolTipText=f'A list of fractions of the total overnight CAPEX spent in each construction year. '
1193+
f'For example, for 3 construction years with 10% in the first year, 40% in the second, '
1194+
f'and 50% in the third, provide {CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME} = 0.1,0.4,0.5. '
1195+
f'The schedule will be automatically interpolated to match the number of construction years '
1196+
f'and normalized to sum to 1.0.'
1197+
)
1198+
1199+
bond_financing_start_year_name = 'Bond Financing Start Year'
1200+
min_bond_financing_start_year = -1*(MAX_CONSTRUCTION_YEARS - 1)
1201+
default_bond_financing_start_year = min_bond_financing_start_year
1202+
latest_allowed_bond_financing_start_year_index = 0
1203+
self.bond_financing_start_year = self.ParameterDict[self.bond_financing_start_year.Name] = intParameter(
1204+
bond_financing_start_year_name,
1205+
DefaultValue=default_bond_financing_start_year,
1206+
AllowableRange=list(range(
1207+
min_bond_financing_start_year,
1208+
latest_allowed_bond_financing_start_year_index + 1,
1209+
1)),
1210+
UnitType=Units.TIME,
1211+
PreferredUnits=TimeUnit.YEAR,
1212+
CurrentUnits=TimeUnit.YEAR,
1213+
ToolTipText=f'By default, bond financing (debt/loans) starts during the first construction year '
1214+
f'(if {self.FIB.Name} is >0). '
1215+
f'Provide {bond_financing_start_year_name} to delay the '
1216+
f'start of bond financing during construction; years prior to {bond_financing_start_year_name} '
1217+
f'will be financed with equity only. '
1218+
f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}; the first construction year has the year index '
1219+
f'{{({model.surfaceplant.construction_years.Name} - 1) * -1}})'
1220+
f' and the final construction year index is 0. '
1221+
f'For example, a project with 4 construction years '
1222+
f'where bond financing starts on the third '
1223+
f'{model.surfaceplant.construction_years.Name[:-1].lower()} '
1224+
f'would have a {bond_financing_start_year_name} value of -1; construction starts in Year -3, '
1225+
f'the second year is Year -2, and the final 2 bond-financed construction years are Year -1 '
1226+
f'and Year 0. '
1227+
f'Bond financing will start on the first construction year if the specified year index is '
1228+
f'prior to the first construction year.'
1229+
)
1230+
11581231
self.contingency_percentage = self.ParameterDict[self.contingency_percentage.Name] = floatParameter(
11591232
'Contingency Percentage',
11601233
DefaultValue=15.,
@@ -2160,22 +2233,26 @@ def __init__(self, model: Model):
21602233
PreferredUnits=PercentUnit.PERCENT,
21612234
CurrentUnits=PercentUnit.PERCENT
21622235
)
2236+
2237+
self.overnight_capital_cost = self.OutputParameterDict[
2238+
self.overnight_capital_cost.Name] = overnight_capital_cost_output_parameter()
2239+
21632240
self.accrued_financing_during_construction_percentage = self.OutputParameterDict[
21642241
self.accrued_financing_during_construction_percentage.Name] = OutputParameter(
21652242
Name='Accrued financing during construction',
21662243
UnitType=Units.PERCENT,
21672244
PreferredUnits=PercentUnit.PERCENT,
21682245
CurrentUnits=PercentUnit.PERCENT,
21692246
ToolTipText='The accrued inflation on total capital costs over the construction period, '
2170-
f'as defined by {self.inflrateconstruction.Name}. '
2171-
'For SAM Economic Models, this is calculated automatically by compounding '
2172-
f'{self.RINFL.Name} over Construction Years '
2173-
f'if {self.inflrateconstruction.Name} is not provided.'
2247+
f'as defined by {self.inflrateconstruction.Name}.'
21742248
)
21752249

21762250
self.inflation_cost_during_construction = self.OutputParameterDict[
21772251
self.inflation_cost_during_construction.Name] = inflation_cost_during_construction_output_parameter()
21782252

2253+
self.interest_during_construction = self.OutputParameterDict[
2254+
self.interest_during_construction.Name] = interest_during_construction_output_parameter()
2255+
21792256
self.after_tax_irr = self.OutputParameterDict[self.after_tax_irr.Name] = (
21802257
after_tax_irr_parameter())
21812258
self.real_discount_rate = self.OutputParameterDict[self.real_discount_rate.Name] = (
@@ -2551,10 +2628,16 @@ def _warn(_msg: str) -> None:
25512628
if self.econmodel.value == EconomicModel.SAM_SINGLE_OWNER_PPA:
25522629
EconomicsSam.validate_read_parameters(model)
25532630
else:
2554-
if self.royalty_rate.Provided:
2555-
raise NotImplementedError('Royalties are only supported for SAM Economic Models')
2631+
sam_em_only_params: list[Parameter] = [
2632+
self.royalty_rate,
2633+
# TODO other royalty params
2634+
self.construction_capex_schedule,
2635+
self.bond_financing_start_year
2636+
]
2637+
for sam_em_only_param in sam_em_only_params:
2638+
if sam_em_only_param.Provided:
2639+
raise NotImplementedError(f'{sam_em_only_param.Name} is only supported for SAM Economic Models')
25562640

2557-
# TODO validate that other SAM-EM-only parameters have not been provided
25582641
else:
25592642
model.logger.info("No parameters read because no content provided")
25602643

@@ -3330,10 +3413,11 @@ def r(x: float) -> float:
33303413

33313414
schedule = []
33323415
current_rate = r(self.royalty_rate.value)
3333-
for _ in range(plant_lifetime):
3416+
for year_index in range(plant_lifetime):
33343417
current_rate = r(current_rate)
33353418
schedule.append(min(current_rate, max_rate))
3336-
current_rate += escalation_rate
3419+
if year_index >= (model.economics.royalty_escalation_rate_start_year.value - 2):
3420+
current_rate += escalation_rate
33373421

33383422
return schedule
33393423

@@ -3436,7 +3520,7 @@ def calculate_cashflow(self, model: Model) -> None:
34363520

34373521
def _calculate_sam_economics(self, model: Model) -> None:
34383522
non_calculated_output_placeholder_val = -1
3439-
self.sam_economics_calculations = calculate_sam_economics(model)
3523+
self.sam_economics_calculations: SamEconomicsCalculations = calculate_sam_economics(model)
34403524

34413525
# Setting capex_total distinguishes capex from CCap's display name of 'Total capital costs',
34423526
# since SAM Economic Model doesn't subtract ITC from this value.
@@ -3445,6 +3529,14 @@ def _calculate_sam_economics(self, model: Model) -> None:
34453529
self.CCap.value = (self.sam_economics_calculations.capex.quantity()
34463530
.to(self.CCap.CurrentUnits.value).magnitude)
34473531

3532+
self.overnight_capital_cost.value = (self.sam_economics_calculations.overnight_capital_cost.quantity()
3533+
.to(self.overnight_capital_cost.CurrentUnits.value).magnitude)
3534+
3535+
self.interest_during_construction.value = quantity(
3536+
self.sam_economics_calculations.pre_revenue_costs_and_cash_flow.interest_during_construction_usd,
3537+
'USD'
3538+
).to(self.interest_during_construction.CurrentUnits.value).magnitude
3539+
34483540

34493541
if self.royalty_rate.Provided:
34503542
# ignore pre-revenue year(s) (e.g. Year 0)

0 commit comments

Comments
 (0)