diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 68e69811..4f34623f 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.11.17 +current_version = 3.11.19 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index ee51f30f..6135ebea 100644 --- a/.cookiecutterrc +++ b/.cookiecutterrc @@ -54,7 +54,7 @@ default_context: sphinx_doctest: "no" sphinx_theme: "sphinx-py3doc-enhanced-theme" test_matrix_separate_coverage: "no" - version: 3.11.17 + version: 3.11.19 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/README.rst b/README.rst index b40c65c0..d8f30dd6 100644 --- a/README.rst +++ b/README.rst @@ -58,9 +58,9 @@ Free software: `MIT license `__ :alt: Supported implementations :target: https://pypi.org/project/geophires-x -.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.11.17.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/softwareengineerprogrammer/GEOPHIRES-X/v3.11.19.svg :alt: Commits since latest release - :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.11.17...main + :target: https://github.com/softwareengineerprogrammer/GEOPHIRES-X/compare/v3.11.19...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://softwareengineerprogrammer.github.io/GEOPHIRES diff --git a/docs/_images/fervo_project_cape-5-power-production.png b/docs/_images/fervo_project_cape-5-power-production.png index 3b67cd2b..6b5b4990 100644 Binary files a/docs/_images/fervo_project_cape-5-power-production.png and b/docs/_images/fervo_project_cape-5-power-production.png differ diff --git a/docs/_images/fervo_project_cape-5-production-temperature.png b/docs/_images/fervo_project_cape-5-production-temperature.png index 219ae07b..75123868 100644 Binary files a/docs/_images/fervo_project_cape-5-production-temperature.png and b/docs/_images/fervo_project_cape-5-production-temperature.png differ diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg index 1de52409..0ae3c0d9 100644 --- a/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-irr.svg @@ -6,7 +6,7 @@ - 2026-02-27T09:53:20.879903 + 2026-02-27T13:19:01.174125 image/svg+xml @@ -40,23 +40,23 @@ z +" clip-path="url(#p24d86b54d1)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: #003c4d; stroke-width: 2"/> +" clip-path="url(#p24d86b54d1)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - - + @@ -116,11 +116,11 @@ z +" clip-path="url(#p24d86b54d1)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -134,11 +134,11 @@ L 421.635935 97.55355 +" clip-path="url(#p24d86b54d1)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -153,11 +153,11 @@ L 553.962493 97.55355 +" clip-path="url(#p24d86b54d1)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -1528,17 +1528,17 @@ L 777.594375 97.55355 " style="fill: none; stroke: #aaaaaa; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1550,7 +1550,7 @@ z - + @@ -1657,7 +1657,7 @@ z - + @@ -1669,17 +1669,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1747,7 +1747,7 @@ z - + @@ -1758,17 +1758,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1779,7 +1779,7 @@ z - + @@ -1833,7 +1833,7 @@ z - + @@ -1845,17 +1845,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1867,7 +1867,7 @@ z - + @@ -1881,17 +1881,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1905,7 +1905,7 @@ z - + @@ -1973,7 +1973,7 @@ z - + @@ -1984,17 +1984,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2057,7 +2057,7 @@ z - + @@ -2072,17 +2072,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2097,7 +2097,7 @@ z - + @@ -2109,7 +2109,7 @@ z - + @@ -2121,27 +2121,27 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p24d86b54d1)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2159,7 +2159,7 @@ z - + @@ -2171,7 +2171,7 @@ z - + @@ -2183,17 +2183,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2209,7 +2209,7 @@ z - + @@ -2221,27 +2221,27 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p24d86b54d1)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2259,7 +2259,7 @@ z - + @@ -2271,7 +2271,7 @@ z - + @@ -2283,17 +2283,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2309,7 +2309,7 @@ z - + @@ -2321,27 +2321,27 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p24d86b54d1)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2369,7 +2369,7 @@ z - + @@ -2382,7 +2382,7 @@ z - + @@ -2393,17 +2393,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2447,7 +2447,7 @@ z - + @@ -2461,17 +2461,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2485,7 +2485,7 @@ z - + @@ -2498,7 +2498,7 @@ z - + @@ -2510,17 +2510,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2532,7 +2532,7 @@ z - + @@ -2542,17 +2542,17 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2562,7 +2562,7 @@ z - + @@ -2574,7 +2574,7 @@ z - + @@ -2586,24 +2586,24 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + - + @@ -2615,27 +2615,27 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p24d86b54d1)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2654,7 +2654,7 @@ z - + @@ -2666,13 +2666,13 @@ z - + +" clip-path="url(#p24d86b54d1)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> @@ -3052,7 +3052,7 @@ z - + diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg index bb301d97..062b70f9 100644 --- a/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-lcoe.svg @@ -6,7 +6,7 @@ - 2026-02-27T09:53:20.934933 + 2026-02-27T13:19:06.576303 image/svg+xml @@ -40,23 +40,23 @@ z +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: #003c4d; stroke-width: 2"/> +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - - + @@ -104,11 +104,11 @@ z +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -139,11 +139,11 @@ z +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -180,11 +180,11 @@ z +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -198,11 +198,11 @@ L 534.621117 97.55355 +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -216,11 +216,11 @@ L 636.390021 97.55355 +" clip-path="url(#p30608253dc)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -1618,17 +1618,17 @@ L 777.594375 97.55355 " style="fill: none; stroke: #aaaaaa; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1803,7 +1803,7 @@ z - + @@ -1814,17 +1814,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1833,7 +1833,7 @@ z - + @@ -1881,17 +1881,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1903,7 +1903,7 @@ z - + @@ -1982,7 +1982,7 @@ z - + @@ -1997,27 +1997,27 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2029,7 +2029,7 @@ z - + @@ -2044,27 +2044,27 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2077,7 +2077,7 @@ z - + @@ -2092,17 +2092,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2118,7 +2118,7 @@ z - + @@ -2133,27 +2133,27 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2171,7 +2171,7 @@ z - + @@ -2187,7 +2187,7 @@ z - + @@ -2203,17 +2203,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2229,7 +2229,7 @@ z - + @@ -2244,27 +2244,27 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2282,7 +2282,7 @@ z - + @@ -2325,7 +2325,7 @@ z - + @@ -2341,27 +2341,27 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2409,7 +2409,7 @@ z - + @@ -2438,17 +2438,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2462,7 +2462,7 @@ z - + @@ -2519,7 +2519,7 @@ z - + @@ -2534,17 +2534,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2557,7 +2557,7 @@ z - + @@ -2607,7 +2607,7 @@ z - + @@ -2623,24 +2623,24 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + - + @@ -2655,17 +2655,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2681,7 +2681,7 @@ z - + @@ -2691,17 +2691,17 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2711,7 +2711,7 @@ z - + @@ -2727,7 +2727,7 @@ z - + @@ -2743,37 +2743,37 @@ z - + +" clip-path="url(#p30608253dc)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#p30608253dc)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2793,7 +2793,7 @@ z - + @@ -3248,7 +3248,7 @@ z - + diff --git a/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg index 671506ff..a9bfae9a 100644 --- a/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg +++ b/docs/_images/fervo_project_cape-5-sensitivity-analysis-project_npv.svg @@ -6,7 +6,7 @@ - 2026-02-27T09:53:21.774454 + 2026-02-27T13:19:00.325568 image/svg+xml @@ -40,23 +40,23 @@ z +" clip-path="url(#pbd60da1527)" style="fill: none; stroke-dasharray: 7.4,3.2; stroke-dashoffset: 0; stroke: #003c4d; stroke-width: 2"/> +" clip-path="url(#pbd60da1527)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - - + @@ -122,11 +122,11 @@ z +" clip-path="url(#pbd60da1527)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -169,11 +169,11 @@ z +" clip-path="url(#pbd60da1527)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -187,11 +187,11 @@ L 478.476269 97.55355 +" clip-path="url(#pbd60da1527)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -207,11 +207,11 @@ L 595.65686 97.55355 +" clip-path="url(#pbd60da1527)" style="fill: none; stroke-dasharray: 0.8,1.32; stroke-dashoffset: 0; stroke: #aaaaaa; stroke-opacity: 0.7; stroke-width: 0.8"/> - + @@ -1620,17 +1620,17 @@ L 777.594375 97.55355 " style="fill: none; stroke: #aaaaaa; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1642,7 +1642,7 @@ z - + @@ -1838,7 +1838,7 @@ z - + @@ -1860,17 +1860,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1934,7 +1934,7 @@ z - + @@ -1945,17 +1945,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -1966,7 +1966,7 @@ z - + @@ -2002,7 +2002,7 @@ z - + @@ -2016,17 +2016,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2041,7 +2041,7 @@ z - + @@ -2055,17 +2055,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2077,7 +2077,7 @@ z - + @@ -2092,17 +2092,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2149,7 +2149,7 @@ z - + @@ -2164,17 +2164,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2189,7 +2189,7 @@ z - + @@ -2238,7 +2238,7 @@ z - + @@ -2252,27 +2252,27 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#pbd60da1527)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2290,7 +2290,7 @@ z - + @@ -2332,7 +2332,7 @@ z - + @@ -2346,17 +2346,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2372,7 +2372,7 @@ z - + @@ -2386,27 +2386,27 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#pbd60da1527)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2424,7 +2424,7 @@ z - + @@ -2439,7 +2439,7 @@ z - + @@ -2453,17 +2453,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2479,7 +2479,7 @@ z - + @@ -2493,27 +2493,27 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#pbd60da1527)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2541,7 +2541,7 @@ z - + @@ -2556,7 +2556,7 @@ z - + @@ -2571,17 +2571,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2596,7 +2596,7 @@ z - + @@ -2610,17 +2610,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #84d1a8; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2634,7 +2634,7 @@ z - + @@ -2649,7 +2649,7 @@ z - + @@ -2663,17 +2663,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2688,7 +2688,7 @@ z - + @@ -2698,17 +2698,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2716,7 +2716,7 @@ z - + @@ -2730,24 +2730,24 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + - + @@ -2761,27 +2761,27 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #808080; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + +" clip-path="url(#pbd60da1527)" style="fill: #b0b0b0; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2800,7 +2800,7 @@ z - + @@ -2814,17 +2814,17 @@ z - + +" clip-path="url(#pbd60da1527)" style="fill: #4ab67d; stroke: #ffffff; stroke-width: 0.5; stroke-linejoin: miter"/> - + @@ -2844,7 +2844,7 @@ z - + @@ -3367,7 +3367,7 @@ z - + diff --git a/docs/_images/singh_et_al_base_simulation-production-temperature.png b/docs/_images/singh_et_al_base_simulation-production-temperature.png index bd28187b..92368c74 100644 Binary files a/docs/_images/singh_et_al_base_simulation-production-temperature.png and b/docs/_images/singh_et_al_base_simulation-production-temperature.png differ diff --git a/docs/conf.py b/docs/conf.py index 282915ec..a69b9ed5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2025' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.11.17' +version = release = '3.11.19' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/setup.py b/setup.py index 37b67477..8bf84893 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.11.17', + version='3.11.19', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_x/Economics.py b/src/geophires_x/Economics.py index 720632b8..444e7927 100644 --- a/src/geophires_x/Economics.py +++ b/src/geophires_x/Economics.py @@ -15,7 +15,7 @@ project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \ interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \ overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \ - _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter + _YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, expand_schedule from geophires_x.GeoPHIRESUtils import quantity from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \ _WellDrillingCostCorrelationCitation @@ -991,6 +991,8 @@ def __init__(self, model: Model): UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, + # TODO clarify relation to supplemental payments + # TODO document mutual incompatibility with Royalty Rate Schedule ToolTipText="The fraction of the project's gross annual revenue paid to the royalty holder. " "This is modeled as a variable production-based operating expense, reducing the developer's " "taxable income." @@ -1004,6 +1006,7 @@ def __init__(self, model: Model): UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, + # TODO clarify applies to Royalty Rate and not schedule ToolTipText="The additive amount the royalty rate increases each year. For example, a value of 0.001 " "increases a 4% rate (0.04) to 4.1% (0.041) in the next year." ) @@ -1015,6 +1018,7 @@ def __init__(self, model: Model): UnitType=Units.NONE, PreferredUnits=TimeUnit.YEAR, CurrentUnits=TimeUnit.YEAR, + # TODO clarify applies to Royalty Rate and not schedule ToolTipText=f'The first year that the {self.royalty_escalation_rate.Name} is applied. ' f'{_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET}.' ) @@ -1028,12 +1032,40 @@ def __init__(self, model: Model): UnitType=Units.PERCENT, PreferredUnits=PercentUnit.TENTH, CurrentUnits=PercentUnit.TENTH, + # TODO clarify applies to Royalty Rate and not schedule ToolTipText=f"The maximum royalty rate after escalation, expressed as a fraction (e.g., 0.06 for a 6% cap)." f"{' Defaults to 100% (no effective cap).' if maximum_royalty_rate_default_val == 1.0 else ''}" ) - # TODO support custom royalty rate schedule as a list parameter - # (as an alternative to specifying rate/escalation/max) + self.royalty_rate_schedule = self.ParameterDict[self.royalty_rate_schedule.Name] = listParameter( + 'Royalty Rate Schedule', + Min=0.0, + Max=1.0, + UnitType=Units.PERCENT, + PreferredUnits=PercentUnit.TENTH, + CurrentUnits=PercentUnit.TENTH, + ToolTipText='A schedule DSL string defining the royalty rate for each year of the project, ' + 'starting at Year 1. ' # TODO clarify this means operational phase/COD + 'Syntax: " * , * , ..., ". ' + 'For example "0.0175 * 10, 0.035" means 1.75% for 10 years then 3.5% thereafter. ' + # TODO document mutual exclusivity with Royalty Rate + # 'If provided, this overrides Royalty Rate, Royalty Rate Escalation, ' + # 'and Royalty Rate Maximum.' + ) + + self.royalty_supplemental_payments = self.ParameterDict[self.royalty_supplemental_payments.Name] = listParameter( + 'Royalty Supplemental Payments', + Min=0.0, + Max=1.0, + UnitType=Units.CURRENCYFREQUENCY, + PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + # TODO improve phrasing, contrast with Royalty Rate Schedule beginning at Year 1 + ToolTipText='A schedule DSL string defining the royalty supplemental payments for each year of the ' + 'project, starting at the first construction year. ' + 'Syntax: " * , * , ..., ". ' + 'For example "1 * 2, 0.25" means $1M for 2 years then $250k/year thereafter. ' + ) self.royalty_holder_discount_rate = self.ParameterDict[self.royalty_holder_discount_rate.Name] = floatParameter( 'Royalty Holder Discount Rate', @@ -1265,8 +1297,9 @@ def __init__(self, model: Model): DefaultValue=False, UnitType=Units.NONE, Required=False, - ErrMessage="assume default: no economics calculations", - ToolTipText="Set to true if you want the add-on economics calculations to be made" + ToolTipText="By default, add-on calculations are automatically enabled if add-ons parameters are provided. " + "Set this value to false to disable add-on economics calculations. " + "(If, for example, you wish to quickly compare between economics with and without add-ons.)" ) self.DoCarbonCalculations = self.ParameterDict[self.DoCarbonCalculations.Name] = boolParameter( "Do Carbon Price Calculations", @@ -2108,7 +2141,8 @@ def __init__(self, model: Model): PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, ToolTipText=f'GEOPHIRES estimates the annual O&M costs as the sum of the annual surface plant, wellfield, ' - f'make-up water, and pumping O&M costs.' + f'make-up water, pumping O&M costs, ' + f'and average royalty costs (both production-based and supplemental payments).' ) self.averageannualpumpingcosts = OutputParameter( Name="Average Annual Pumping Costs", @@ -2131,6 +2165,7 @@ def __init__(self, model: Model): CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, ToolTipText='The average annual cost paid to a royalty holder, calculated as a percentage of the ' 'project\'s gross annual revenue. This is modeled as a variable operating expense.' + # TODO adjust for Royalty Supplemental Payments, including explaining construction vs. operational years ) @@ -2262,6 +2297,15 @@ def __init__(self, model: Model): self.inflation_cost_during_construction = self.OutputParameterDict[ self.inflation_cost_during_construction.Name] = inflation_cost_during_construction_output_parameter() + self.royalty_supplemental_payments_cost_during_construction = self.OutputParameterDict[ + self.royalty_supplemental_payments_cost_during_construction.Name] = OutputParameter( + Name='Royalty supplemental payments during construction', + UnitType=Units.CURRENCY, + PreferredUnits=CurrencyUnit.MDOLLARS, + CurrentUnits=CurrencyUnit.MDOLLARS, + ToolTipText='The sum of royalty supplemental payments during the construction period.', + ) + self.interest_during_construction = self.OutputParameterDict[ self.interest_during_construction.Name] = interest_during_construction_output_parameter() @@ -2368,6 +2412,7 @@ def __init__(self, model: Model): UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.MDOLLARSPERYEAR, + # TODO clarify that this includes construction years (production-based vs. supplemental payments) ToolTipText="The royalty holder's gross (pre-tax) annual revenue stream from the royalty agreement." ) self.royalty_holder_total_revenue = self.OutputParameterDict[ @@ -2636,7 +2681,11 @@ def _warn(_msg: str) -> None: else: sam_em_only_params: list[Parameter] = [ self.royalty_rate, - # TODO other royalty params + self.royalty_escalation_rate, + self.royalty_escalation_rate_start_year, + self.maximum_royalty_rate, + self.royalty_rate_schedule, + self.royalty_supplemental_payments, self.construction_capex_schedule, self.bond_financing_start_year ] @@ -3401,11 +3450,20 @@ def build_price_models(self, model: Model) -> None: def get_royalty_rate_schedule(self, model: Model) -> list[float]: """ - Builds a year-by-year schedule of royalty rates based on escalation and cap. + Build the royalty rate schedule for each operational year. - :type model: :class:`~geophires_x.Model.Model` - :return: schedule: A list of rates as fractions (e.g., 0.05 for 5%). + If ``royalty_rate_schedule`` was provided via the DSL, it is expanded using + :func:`expand_schedule` and takes precedence. Otherwise the legacy + ``royalty_rate`` + ``royalty_escalation_rate`` + ``maximum_royalty_rate`` logic + is used. + + :returns: A list of royalty rates (fractional, e.g. 0.035 for 3.5%) with + one entry per operational year (length == ``plant_lifetime``). """ + plant_lifetime: int = model.surfaceplant.plant_lifetime.value + + if self.royalty_rate_schedule.Provided and self.royalty_rate_schedule.value: + return expand_schedule(self.royalty_rate_schedule.value, plant_lifetime) def r(x: float) -> float: """Ignore apparent float precision issue""" @@ -3427,6 +3485,20 @@ def r(x: float) -> float: return schedule + def get_royalty_supplemental_payments_schedule_usd(self, model: Model) -> list[float]: + construction_years: int = model.surfaceplant.construction_years.value + operational_years: int = model.surfaceplant.plant_lifetime.value + + royalty_supplemental_payments_schedule_expanded = expand_schedule( + self.royalty_supplemental_payments.value, construction_years + operational_years) + + royalty_supplemental_payments_schedule_usd = [ + PlainQuantity(it, self.royalty_supplemental_payments.CurrentUnits).to('USD/yr').magnitude + for it in royalty_supplemental_payments_schedule_expanded + ] + + return royalty_supplemental_payments_schedule_usd + def calculate_cashflow(self, model: Model) -> None: """ @@ -3532,7 +3604,6 @@ def _calculate_sam_economics(self, model: Model) -> None: # since SAM Economic Model doesn't subtract ITC from this value. self.capex_total.value = (self.sam_economics_calculations.capex.quantity() .to(self.capex_total.CurrentUnits.value).magnitude) - # self.capex_total_per_kw.value = PlainQuantity(self.capex_total.value, f'{self.capex_total.CurrentUnits}' # TODO define this as an output of SurfacePlant rather than calculating it on-demand here and elsewhere max_net_electricity_generation_kw = quantity( @@ -3554,7 +3625,7 @@ def _calculate_sam_economics(self, model: Model) -> None: ).to(self.interest_during_construction.CurrentUnits.value).magnitude - if self.royalty_rate.Provided: + if self.has_royalties: # ignore pre-revenue year(s) (e.g. Year 0) pre_revenue_years_slice_index = model.surfaceplant.construction_years.value @@ -3569,25 +3640,39 @@ def _calculate_sam_economics(self, model: Model) -> None: self.Coam.value += (self.royalties_average_annual_cost.quantity() .to(self.Coam.CurrentUnits.value).magnitude) + # Note that updating Coam's value here does not affect already-calculated cash flow/result OPEX self.royalty_holder_npv.value = quantity( calculate_npv( self.royalty_holder_discount_rate.value, - self.sam_economics_calculations.royalties_opex.value, + self.sam_economics_calculations.royalties_opex.value, # Includes construction years self.discount_initial_year_cashflow.value ), self.sam_economics_calculations.royalties_opex.CurrentUnits.get_currency_unit_str() ).to(self.royalty_holder_npv.CurrentUnits).magnitude - self.royalty_holder_annual_revenue.value = self.royalties_average_annual_cost.value + + self.royalty_holder_annual_revenue.value = (quantity( + np.average( + self.sam_economics_calculations.royalties_opex.value # Includes construction years + ), + self.sam_economics_calculations.royalties_opex.CurrentUnits + ).to(self.royalty_holder_annual_revenue.CurrentUnits).magnitude) self.royalty_holder_total_revenue.value = quantity( np.sum( - self.sam_economics_calculations.royalties_opex.value[pre_revenue_years_slice_index:] + self.sam_economics_calculations.royalties_opex.value # Includes construction years ), self.sam_economics_calculations.royalties_opex.CurrentUnits.get_currency_unit_str() ).to(self.royalty_holder_total_revenue.CurrentUnits).magnitude + self.royalty_supplemental_payments_cost_during_construction.value = quantity( + np.sum( + self.sam_economics_calculations.royalties_opex.value[:pre_revenue_years_slice_index] + ), + self.sam_economics_calculations.royalties_opex.CurrentUnits.get_currency_unit_str() + ).to(self.royalty_supplemental_payments_cost_during_construction.CurrentUnits).magnitude + self.wacc.value = self.sam_economics_calculations.wacc.value self.nominal_discount_rate.value = self.sam_economics_calculations.nominal_discount_rate.value @@ -3629,6 +3714,13 @@ def _calculate_derived_outputs(self, model: Model) -> None: (model.wellbores.nprod.value + model.wellbores.ninj.value) ) + @property + def has_production_based_royalties(self): + return self.royalty_rate.Provided or self.royalty_rate_schedule.Provided + + @property + def has_royalties(self): + return self.has_production_based_royalties or self.royalty_supplemental_payments.Provided def __str__(self): diff --git a/src/geophires_x/EconomicsSam.py b/src/geophires_x/EconomicsSam.py index 3bb98419..0a5238c8 100644 --- a/src/geophires_x/EconomicsSam.py +++ b/src/geophires_x/EconomicsSam.py @@ -433,7 +433,7 @@ def _inv_msg(param_name: str, invalid_value: Any, supported_description: str) -> f'{eir.Name} provided value ({eir.value}) will be ignored. (SAM Economics does not support {eir.Name}.)' ) - econ = model.economics + econ: 'Economics' = model.economics econ.construction_capex_schedule.value = _validate_construction_capex_schedule( econ.construction_capex_schedule, @@ -448,6 +448,11 @@ def _inv_msg(param_name: str, invalid_value: Any, supported_description: str) -> f'first {model.surfaceplant.construction_years.Name[:-1]} ({-1 * (construction_years - 1)}). (OK)' ) + if econ.royalty_rate.Provided and econ.royalty_rate_schedule.Provided: + raise ValueError(f'Only one of {econ.royalty_rate.Name} and {econ.royalty_rate_schedule.Name} may be provided.') + + # TODO validate that other rate-style params are not provided when schedule is provided + def _validate_construction_capex_schedule( econ_capex_schedule: listParameter, construction_years: int, model: Model @@ -540,18 +545,39 @@ def sf(_v: float, num_sig_figs: int = 5) -> float: sam_economics.project_npv.value = sf(_get_project_npv_musd(single_owner, cash_flow_operational_years, model)) sam_economics.capex.value = single_owner.Outputs.adjusted_installed_cost * 1e-6 - if model.economics.royalty_rate.Provided: - # Assumes that royalties opex is the only possible O&M production-based expense - this logic will need to be - # updated if more O&M production-based expenses are added to SAM-EM - sam_economics.royalties_opex.value = [ + if model.economics.has_royalties: + combined_royalties_usd = [ *_pre_revenue_years_vector(model), - *[ - quantity(it, 'USD / year').to(sam_economics.royalties_opex.CurrentUnits).magnitude - for it in _cash_flow_profile_row(cash_flow_operational_years, ROYALTIES_OPEX_CASH_FLOW_LINE_ITEM_KEY) - ], + *([0] * (model.surfaceplant.plant_lifetime.value + 1)), ] - sam_economics._royalties_rate_schedule = model.economics.get_royalty_rate_schedule(model) + if model.economics.has_production_based_royalties: + # Assumes that royalties opex is the only possible O&M production-based expense - this logic will need to be + # updated if more O&M production-based expenses are added to SAM-EM + production_based_royalties_usd = [ + *_pre_revenue_years_vector(model), + *[ + quantity(it, 'USD / year').to(sam_economics.royalties_opex.CurrentUnits).magnitude + for it in _cash_flow_profile_row( + cash_flow_operational_years, ROYALTIES_OPEX_CASH_FLOW_LINE_ITEM_KEY + ) + ], + ] + + for i, annual_production_based_royalties_usd in enumerate(production_based_royalties_usd): + combined_royalties_usd[i] += annual_production_based_royalties_usd + + sam_economics._royalties_rate_schedule = model.economics.get_royalty_rate_schedule(model) + + if model.economics.royalty_supplemental_payments.Provided: + royalty_supplemental_payments_schedule_usd = model.economics.get_royalty_supplemental_payments_schedule_usd( + model + ) + + for i, annual_royalty_supplemental_payment_usd in enumerate(royalty_supplemental_payments_schedule_usd): + combined_royalties_usd[i] += annual_royalty_supplemental_payment_usd + + sam_economics.royalties_opex.value = combined_royalties_usd sam_economics.nominal_discount_rate.value, sam_economics.wacc.value = _calculate_nominal_discount_rate_and_wacc_pct( model, single_owner @@ -920,8 +946,15 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: # Pass the final, correct values to SAM ret['total_installed_cost'] = total_installed_cost_usd - opex_musd = econ.Coam.value - ret['om_fixed'] = [opex_musd * 1e6] * model.surfaceplant.plant_lifetime.value + opex_base_usd = econ.Coam.quantity().to('USD/yr').magnitude + opex_by_year_usd = [] + royalty_supplemental_payments_by_year_usd = econ.get_royalty_supplemental_payments_schedule_usd(model)[ + _pre_revenue_years_count(model) : + ] + for year_index in range(model.surfaceplant.plant_lifetime.value): + opex_by_year_usd.append(opex_base_usd + royalty_supplemental_payments_by_year_usd[year_index]) + + ret['om_fixed'] = opex_by_year_usd # GEOPHIRES assumes O&M fixed costs are not affected by inflation ret['om_fixed_escal'] = -1.0 * _pct(econ.RINFL) @@ -949,7 +982,7 @@ def _get_single_owner_parameters(model: Model) -> dict[str, Any]: ppa_price_schedule_per_kWh = _get_ppa_price_schedule_per_kWh(model) ret['ppa_price_input'] = ppa_price_schedule_per_kWh - if model.economics.royalty_rate.Provided: + if model.economics.has_production_based_royalties: ret['om_production'] = _get_royalties_variable_om_USD_per_MWh_schedule(model) # Debt/equity ratio @@ -1036,6 +1069,10 @@ def _ppa_pricing_model( def _get_royalty_rate_schedule(model: Model) -> list[float]: + """ + Delegates to the Economics instance which now supports the DSL-based + royalty_rate_schedule parameter with automatic fallback. + """ return model.economics.get_royalty_rate_schedule(model) diff --git a/src/geophires_x/EconomicsSamPreRevenue.py b/src/geophires_x/EconomicsSamPreRevenue.py index 5cc030cd..047a5bac 100644 --- a/src/geophires_x/EconomicsSamPreRevenue.py +++ b/src/geophires_x/EconomicsSamPreRevenue.py @@ -57,7 +57,7 @@ def interest_during_construction_usd(self) -> float: def calculate_pre_revenue_costs_and_cashflow(model: 'Model') -> PreRevenueCostsAndCashflow: - econ = model.economics + econ: 'Economics' = model.economics if econ.inflrateconstruction.Provided: pre_revenue_inflation_rate = econ.inflrateconstruction.quantity().to('dimensionless').magnitude else: @@ -76,6 +76,13 @@ def calculate_pre_revenue_costs_and_cashflow(model: 'Model') -> PreRevenueCostsA 0, # Treat bond financing years prior to construction as starting in the first year of construction ) + additional_kw_args = {} + + if econ.royalty_supplemental_payments.Provided: + additional_kw_args['additional_payments_schedule_usd'] = econ.get_royalty_supplemental_payments_schedule_usd( + model + )[:construction_years] + return _calculate_pre_revenue_costs_and_cashflow( total_overnight_capex_usd=econ.CCap.quantity().to('USD').magnitude, pre_revenue_years_count=construction_years, @@ -85,6 +92,7 @@ def calculate_pre_revenue_costs_and_cashflow(model: 'Model') -> PreRevenueCostsA debt_fraction=econ.FIB.quantity().to('dimensionless').magnitude, debt_financing_start_year=debt_financing_start_year, logger=model.logger, + **additional_kw_args, ) @@ -100,20 +108,30 @@ def _calculate_pre_revenue_costs_and_cashflow( debt_fraction: float, debt_financing_start_year: int, logger: logging.Logger, + additional_payments_schedule_usd: list[float] | None = None, ) -> PreRevenueCostsAndCashflow: """ Calculates the true capitalized cost and interest during pre-revenue years (exploration/permitting/appraisal, construction) by simulating a year-by-year phased expenditure with inflation. Also builds a pre-revenue cash flow profile for construction revenue years. - - :param include_summary_line_items: Include cash flow from investment and financing activities and pre-tax returns - in the summary line items. Disabled by default since they are redundant with other construction line items and - confusing to reconcile with their non-construction equivalents. """ logger.info(f"Using Phased CAPEX Schedule: {phased_capex_schedule}") + if additional_payments_schedule_usd is None: + additional_payments_schedule_usd = [0] * pre_revenue_years_count + elif len(additional_payments_schedule_usd) < pre_revenue_years_count: + additional_payments_schedule_usd = [ + *additional_payments_schedule_usd, + *[0] * (pre_revenue_years_count - len(additional_payments_schedule_usd)), + ] + elif len(additional_payments_schedule_usd) > pre_revenue_years_count: + raise ValueError( + f'Additional payments schedule is longer ({len(additional_payments_schedule_usd)} years) ' + f'than pre-revenue years ({pre_revenue_years_count}).' + ) + current_debt_balance_usd = 0.0 total_capitalized_cost_usd = 0.0 total_interest_accrued_usd = 0.0 @@ -138,7 +156,9 @@ def _calculate_pre_revenue_costs_and_cashflow( inflation_cost_vec.append(inflation_cost_this_year_usd) - capex_this_year_usd = base_capex_this_year_usd + inflation_cost_this_year_usd + capex_this_year_usd = ( + base_capex_this_year_usd + inflation_cost_this_year_usd + additional_payments_schedule_usd[year_index] + ) # Interest is calculated on the opening balance (from previous years' draws) interest_this_year_usd = current_debt_balance_usd * pre_revenue_bond_interest_rate @@ -196,6 +216,8 @@ def _append_row(row_name: str, row_vals: list[float | str]) -> None: _append_row(f'Overnight capital expenditure ($)', [round(-it) for it in base_capex_vec]) _append_row(f'plus:', []) _append_row(f'Inflation cost ($)', [round(-it) for it in inflation_cost_vec]) + _append_row(f'plus:', []) + _append_row(f'Royalty supplemental payments ($)', [round(-it) for it in additional_payments_schedule_usd]) _append_row(f'equals:', []) _append_row(f'Nominal capital expenditure ($)', [-x for x in capex_spend_vec]) diff --git a/src/geophires_x/EconomicsUtils.py b/src/geophires_x/EconomicsUtils.py index 9d494b86..ae8f4d9c 100644 --- a/src/geophires_x/EconomicsUtils.py +++ b/src/geophires_x/EconomicsUtils.py @@ -203,6 +203,7 @@ def royalty_cost_output_parameter() -> OutputParameter: UnitType=Units.CURRENCYFREQUENCY, PreferredUnits=CurrencyFrequencyUnit.DOLLARSPERYEAR, CurrentUnits=CurrencyFrequencyUnit.DOLLARSPERYEAR, + # TODO/WIP - clarify production-based vs. supplemental payments ToolTipText='The annual costs paid to a royalty holder, calculated as a percentage of the ' 'project\'s gross annual revenue. This is modeled as a variable operating expense.', ) @@ -219,3 +220,77 @@ def investment_tax_credit_output_parameter() -> OutputParameter: 'For SAM Economic Models, this accounts for the standard Year 1 Federal ITC as well as any ' 'applicable State ITCs or multi-year credit schedules.', ) + + +def expand_schedule(schedule_strings: list[str | float], total_years: int) -> list[float]: + """ + Parse a duration-based scheduling DSL and expand it into a fixed-length time-series array. + + Syntax: ``[Value] * [Years], [Value] * [Years], ..., [Terminal Value]`` + + The terminal (last) value is repeated to fill ``total_years``. A bare scalar + (e.g. ``['2.5']``) is treated as a terminal value and broadcast across all years. + + Examples:: + + expand_schedule(['1.0 * 3', '0.1'], total_years=6) + # => [1.0, 1.0, 1.0, 0.1, 0.1, 0.1] + + expand_schedule(['2.5'], total_years=4) + # => [2.5, 2.5, 2.5, 2.5] + + :param schedule_strings: list of DSL segment strings. Each element is either + ``" * "`` (a run-length segment) or ``""`` (a scalar, + which becomes the terminal value when it is the last element, or a 1-year + segment otherwise). + :param total_years: The total number of years the expanded array must span + (typically ``construction_years + plant_lifetime``). + :returns: A ``list[float]`` of length ``total_years``. + :raises ValueError: On malformed DSL strings or when explicit segments exceed + ``total_years``. + """ + if total_years <= 0: + return [] + + if not schedule_strings: + return [0.0] * total_years + + segments: list[tuple[float, int | None]] = [] + for raw in schedule_strings: + raw = str(raw).strip() + if '*' in raw: + parts = raw.split('*') + if len(parts) != 2: + raise ValueError(f'Invalid schedule segment "{raw}": expected " * ".') + value = float(parts[0].strip()) + years = int(parts[1].strip()) + if years < 0: + raise ValueError(f'Invalid schedule segment "{raw}": year count must be non-negative.') + segments.append((value, years)) + else: + value = float(raw) + segments.append((value, None)) + + result: list[float] = [] + terminal_value = 0.0 + + for idx, (value, years) in enumerate(segments): + is_last = idx == len(segments) - 1 + if years is not None: + result.extend([value] * years) + terminal_value = value + else: + if is_last: + terminal_value = value + else: + result.append(value) + terminal_value = value + + if len(result) > total_years: + raise ValueError(f'Schedule expands to {len(result)} years which exceeds total_years={total_years}.') + + remaining = total_years - len(result) + if remaining > 0: + result.extend([terminal_value] * remaining) + + return result diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 3b5e4dab..1f53da28 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -513,6 +513,15 @@ def PrintOutputs(self, model: Model): f.write( f' {occ_label}{econ.overnight_capital_cost.value:10.2f} {econ.overnight_capital_cost.CurrentUnits.value}\n') + if econ.royalty_supplemental_payments.Provided: + rsp_label = Outputs._field_label(econ.royalty_supplemental_payments_cost_during_construction.display_name, 41) + f.write( + f' {rsp_label} {econ.royalty_supplemental_payments_cost_during_construction.value:.2f} {econ.royalty_supplemental_payments_cost_during_construction.CurrentUnits.value}\n') + + if display_occ_and_inflation_during_construction_in_capital_costs: + icc_label = Outputs._field_label(econ.inflation_cost_during_construction.display_name, 47) + f.write(f' {icc_label}{econ.inflation_cost_during_construction.value:10.2f} {econ.inflation_cost_during_construction.CurrentUnits.value}\n') + display_idc_in_capital_costs = is_sam_econ_model \ and model.surfaceplant.construction_years.value > 1 if display_idc_in_capital_costs: @@ -520,10 +529,6 @@ def PrintOutputs(self, model: Model): f.write( f' {idc_label}{econ.interest_during_construction.value:10.2f} {econ.interest_during_construction.CurrentUnits.value}\n') - if display_occ_and_inflation_during_construction_in_capital_costs: - icc_label = Outputs._field_label(econ.inflation_cost_during_construction.display_name, 47) - f.write(f' {icc_label}{econ.inflation_cost_during_construction.value:10.2f} {econ.inflation_cost_during_construction.CurrentUnits.value}\n') - if is_sam_econ_model and econ.DoAddOnCalculations.value: # Non-SAM econ models print this in Extended Economics profile aoc_label = Outputs._field_label(model.addeconomics.AddOnCAPEXTotal.display_name, 47) @@ -563,7 +568,7 @@ def PrintOutputs(self, model: Model): aoc_label = Outputs._field_label(model.addeconomics.AddOnOPEXTotalPerYear.display_name, 47) f.write(f' {aoc_label}{model.addeconomics.AddOnOPEXTotalPerYear.value:10.2f} {model.addeconomics.AddOnOPEXTotalPerYear.CurrentUnits.value}\n') - if econ.royalty_rate.Provided: + if econ.has_production_based_royalties: royalties_label = Outputs._field_label(econ.royalties_average_annual_cost.display_name, 47) f.write(f' {royalties_label}{econ.royalties_average_annual_cost.value:10.2f} {econ.royalties_average_annual_cost.CurrentUnits.value}\n') @@ -805,7 +810,7 @@ def PrintOutputs(self, model: Model): addon_df, addon_results = model.addoutputs.PrintOutputs(model) extended_economics_header_printed = True - if econ.royalty_rate.Provided: + if econ.has_royalties: with open(self.output_file, 'a', encoding='UTF-8') as f_: if not extended_economics_header_printed: self._print_extended_economics_header(f_) diff --git a/src/geophires_x/Parameter.py b/src/geophires_x/Parameter.py index f187a8a3..ff89cb6a 100644 --- a/src/geophires_x/Parameter.py +++ b/src/geophires_x/Parameter.py @@ -29,6 +29,7 @@ _JSON_PARAMETER_TYPE_BOOLEAN = 'boolean' _JSON_PARAMETER_TYPE_OBJECT = 'object' + class HasQuantity(ABC): def quantity(self) -> PlainQuantity: @@ -36,7 +37,16 @@ def quantity(self) -> PlainQuantity: :rtype: pint.registry.Quantity - note type annotation uses PlainQuantity due to issues with python 3.8 failing to import the Quantity TypeAlias """ - return _ureg.Quantity(self.value, str(self.CurrentUnits.value)) + + quant_val = self.value + + if isinstance(quant_val, str): + quant_val = float(quant_val) + + if isinstance(quant_val, Iterable): + quant_val = [float(it) for it in quant_val] + + return _ureg.Quantity(quant_val, str(self.CurrentUnits.value)) @dataclass @@ -315,16 +325,17 @@ def ReadParameter(ParameterReadIn: ParameterEntry, ParamToModify, model) -> None return # deal with the case where the value has a unit involved - that will be indicated by a space in it - if ' ' in ParameterReadIn.sValue: + if ' ' in ParameterReadIn.sValue and '*' not in ParameterReadIn.sValue: new_str = ConvertUnits(ParamToModify, ParameterReadIn.sValue, model) if len(new_str) > 0: ParameterReadIn.sValue = new_str - #else: + else: # The value came in without any units # TODO: determine the proper action in this case # (previously, it was assumed that the value must be # using the default PreferredUnits, which was not always # valid and led to incorrect units in the output) + pass def default_parameter_value_message(new_val: Any, param_to_modify_name: str, default_value: Any) -> str: return ( @@ -422,20 +433,13 @@ def default_parameter_value_message(new_val: Any, param_to_modify_name: str, def model.logger.info(f'Complete {str(__name__)}: {sys._getframe().f_code.co_name}') -def _read_list_parameter(ParameterReadIn: ParameterEntry, ParamToModify, model) -> None: +def _read_list_parameter(ParameterReadIn: ParameterEntry, ParamToModify: listParameter, model) -> None: """ :type ParamToModify: :class:`~geophires_x.Parameter.Parameter` :type model: :class:`~geophires_x.Model.Model` """ - def _is_int(o: Any) -> bool: - try: - float_n = float(o) - int_n = int(float_n) - except ValueError: - return False - else: - return float_n == int_n + from geophires_x.GeoPHIRESUtils import is_float, is_int as _is_int # avoid circular imports is_positional_parameter = ' ' in ParameterReadIn.Name and _is_int(ParamToModify.Name.split(' ')[-1]) if is_positional_parameter: @@ -453,22 +457,47 @@ def _is_int(o: Any) -> bool: # In an ideal world this would be handled in ParameterEntry such that its sValue and Comment are # correct; however that would only be practical if ParameterEntry had typing information to know # whether to treat text after a second comma as a comment or list entry. - ParamToModify.value = [float(x.strip()) for x in ParameterReadIn.raw_entry.split('--')[0].split(',')[1:] - if x.strip() != ''] + + ParamToModify.value = [float(x.strip()) if is_float(x.strip()) else x.strip() for x in + ParameterReadIn.raw_entry.split('--')[0].split(',')[1:] if x.strip() != ''] ParamToModify.Provided = True + # TODO make this a property of listParameter + is_boolean_type = (ParamToModify.DefaultValue is not None and + (isinstance(ParamToModify.DefaultValue, Iterable) and + len(ParamToModify.DefaultValue) > 0 and + isinstance(ParamToModify.DefaultValue[0], bool)) + or (isinstance(ParamToModify.DefaultValue, bool))) + + + def _is_bool_val(_val: Any) -> bool: + return isinstance(_val, bool) or (str(_val).strip().lower() in ['true', 'false', '1', '0', 'yes', 'no']) + valid = True for i in range(len(ParamToModify.value)): New_val = ParamToModify.value[i] + if is_boolean_type: + if _is_bool_val(New_val): + continue + msg = f'Value given ({str(New_val)}) for {ParamToModify.Name} is not boolean.' + valid = False + + if isinstance(New_val, str): + if '*' in New_val: + New_val = New_val.split('*')[0] + New_val = float(New_val.strip()) + if (New_val < float(ParamToModify.Min)) or (New_val > float(ParamToModify.Max)): msg = ( f'Value given ({str(New_val)}) for {ParamToModify.Name} outside of valid range ' f'({ParamToModify.Min}–{ParamToModify.Max}).' ) + valid = False + + if not valid: print(f'Warning: {msg}') model.logger.warning(msg) - valid = False ParamToModify.Valid = valid diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index d5e925ea..7255145a 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.11.17' +__version__ = '3.11.19' diff --git a/src/geophires_x_schema_generator/geophires-request.json b/src/geophires_x_schema_generator/geophires-request.json index 932f3548..f09115b8 100644 --- a/src/geophires_x_schema_generator/geophires-request.json +++ b/src/geophires_x_schema_generator/geophires-request.json @@ -1719,6 +1719,24 @@ "minimum": 0.0, "maximum": 1.0 }, + "Royalty Rate Schedule": { + "description": "A schedule DSL string defining the royalty rate for each year of the project, starting at Year 1. Syntax: \" * , * , ..., \". For example \"0.0175 * 10, 0.035\" means 1.75% for 10 years then 3.5% thereafter. ", + "type": "array", + "units": "", + "category": "Economics", + "default": [], + "minimum": 0.0, + "maximum": 1.0 + }, + "Royalty Supplemental Payments": { + "description": "A schedule DSL string defining the royalty supplemental payments for each year of the project, starting at the first construction year. Syntax: \" * , * , ..., \". For example \"1 * 2, 0.25\" means $1M for 2 years then $250k/year thereafter. ", + "type": "array", + "units": "MUSD/yr", + "category": "Economics", + "default": [], + "minimum": 0.0, + "maximum": 1.0 + }, "Royalty Holder Discount Rate": { "description": "The discount rate used to calculate the Net Present Value (NPV) of the royalty holder's income stream. This rate should reflect the royalty holder's specific risk profile and is separate from the main project discount rate.", "type": "number", @@ -1953,7 +1971,7 @@ ] }, "Do AddOn Calculations": { - "description": "Set to true if you want the add-on economics calculations to be made", + "description": "By default, add-on calculations are automatically enabled if add-ons parameters are provided. Set this value to false to disable add-on economics calculations. (If, for example, you wish to quickly compare between economics with and without add-ons.)", "type": "boolean", "units": null, "category": "Economics", diff --git a/src/geophires_x_schema_generator/geophires-result.json b/src/geophires_x_schema_generator/geophires-result.json index 9a01c92f..3e4d1101 100644 --- a/src/geophires_x_schema_generator/geophires-result.json +++ b/src/geophires_x_schema_generator/geophires-result.json @@ -543,7 +543,7 @@ "Total average annual O&M costs": {}, "Total operating and maintenance costs": { "type": "number", - "description": "Total O&M Cost. GEOPHIRES estimates the annual O&M costs as the sum of the annual surface plant, wellfield, make-up water, and pumping O&M costs.", + "description": "Total O&M Cost. GEOPHIRES estimates the annual O&M costs as the sum of the annual surface plant, wellfield, make-up water, pumping O&M costs, and average royalty costs (both production-based and supplemental payments).", "units": "MUSD/yr" }, "OPEX": {} diff --git a/tests/examples/Fervo_Project_Cape-4.out b/tests/examples/Fervo_Project_Cape-4.out index 25a7258b..886d1848 100644 --- a/tests/examples/Fervo_Project_Cape-4.out +++ b/tests/examples/Fervo_Project_Cape-4.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:47 - Calculation Time: 1.795 sec + Simulation Time: 13:15 + Calculation Time: 1.787 sec ***SUMMARY OF RESULTS*** @@ -221,6 +221,8 @@ Capital expenditure schedule [construction] (%) 100.0 Overnight capital expenditure [construction] ($) -2,601,042,401 plus: Inflation cost [construction] ($) -59,823,975 +plus: +Royalty supplemental payments [construction] ($) 0 equals: Nominal capital expenditure [construction] ($) -2,660,866,376 diff --git a/tests/examples/Fervo_Project_Cape-5.out b/tests/examples/Fervo_Project_Cape-5.out index 8d03fe8d..8c48f3ae 100644 --- a/tests/examples/Fervo_Project_Cape-5.out +++ b/tests/examples/Fervo_Project_Cape-5.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:49 - Calculation Time: 1.881 sec + Simulation Time: 13:15 + Calculation Time: 1.785 sec ***SUMMARY OF RESULTS*** @@ -116,8 +116,8 @@ Simulation Metadata Total surface equipment costs: 1516.11 MUSD Exploration costs: 30.00 MUSD Overnight Capital Cost: 2437.12 MUSD - Interest during construction: 142.66 MUSD Inflation costs during construction: 285.92 MUSD + Interest during construction: 142.66 MUSD Total CAPEX: 2865.69 MUSD @@ -233,6 +233,8 @@ Capital expenditure schedule [construction] (%) 1.40 2.70 13. Overnight capital expenditure [construction] ($) -34,119,619 -65,802,123 -338,759,075 -1,050,396,845 -948,037,988 plus: Inflation cost [construction] ($) -921,230 -3,601,284 -28,187,019 -118,120,553 -135,085,460 +plus: +Royalty supplemental payments [construction] ($) 0 0 0 0 0 equals: Nominal capital expenditure [construction] ($) -35,040,849 -69,403,407 -366,946,094 -1,168,517,398 -1,083,123,448 @@ -468,5 +470,5 @@ Interest earned on reserves ($) ***EXTENDED ECONOMICS*** Royalty Holder NPV: 136.02 MUSD - Royalty Holder Average Annual Revenue: 12.19 MUSD/yr + Royalty Holder Average Annual Revenue: 10.44 MUSD/yr Royalty Holder Total Revenue: 365.56 MUSD diff --git a/tests/examples/Fervo_Project_Cape-6.out b/tests/examples/Fervo_Project_Cape-6.out index 07843d34..05f33a62 100644 --- a/tests/examples/Fervo_Project_Cape-6.out +++ b/tests/examples/Fervo_Project_Cape-6.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:53 - Calculation Time: 1.834 sec + Simulation Time: 13:19 + Calculation Time: 1.826 sec ***SUMMARY OF RESULTS*** @@ -116,8 +116,8 @@ Simulation Metadata Total surface equipment costs: 303.37 MUSD Exploration costs: 30.00 MUSD Overnight Capital Cost: 522.94 MUSD - Interest during construction: 21.28 MUSD Inflation costs during construction: 33.49 MUSD + Interest during construction: 21.28 MUSD Total CAPEX: 577.72 MUSD @@ -233,6 +233,8 @@ Capital expenditure schedule [construction] (%) 7.50 52.50 40. Overnight capital expenditure [construction] ($) -39,220,743 -274,545,204 -209,177,298 plus: Inflation cost [construction] ($) -1,058,960 -15,025,584 -17,404,949 +plus: +Royalty supplemental payments [construction] ($) 0 0 0 equals: Nominal capital expenditure [construction] ($) -40,279,704 -289,570,789 -226,582,247 @@ -468,5 +470,5 @@ Interest earned on reserves ($) 0 ***EXTENDED ECONOMICS*** Royalty Holder NPV: 30.35 MUSD - Royalty Holder Average Annual Revenue: 2.47 MUSD/yr + Royalty Holder Average Annual Revenue: 2.24 MUSD/yr Royalty Holder Total Revenue: 73.97 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA-2.out b/tests/examples/example_SAM-single-owner-PPA-2.out index 73a47239..75c94e00 100644 --- a/tests/examples/example_SAM-single-owner-PPA-2.out +++ b/tests/examples/example_SAM-single-owner-PPA-2.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:53 - Calculation Time: 1.050 sec + Simulation Time: 13:19 + Calculation Time: 1.006 sec ***SUMMARY OF RESULTS*** @@ -201,6 +201,8 @@ Capital expenditure schedule [construction] (%) 100.0 Overnight capital expenditure [construction] ($) -1,532,782,686 plus: Inflation cost [construction] ($) -76,639,134 +plus: +Royalty supplemental payments [construction] ($) 0 equals: Nominal capital expenditure [construction] ($) -1,609,421,820 diff --git a/tests/examples/example_SAM-single-owner-PPA-3.out b/tests/examples/example_SAM-single-owner-PPA-3.out index 6d530815..87eefbea 100644 --- a/tests/examples/example_SAM-single-owner-PPA-3.out +++ b/tests/examples/example_SAM-single-owner-PPA-3.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:53 - Calculation Time: 1.239 sec + Simulation Time: 13:19 + Calculation Time: 1.206 sec ***SUMMARY OF RESULTS*** @@ -203,6 +203,8 @@ Capital expenditure schedule [construction] (%) 100.0 Overnight capital expenditure [construction] ($) -262,355,642 plus: Inflation cost [construction] ($) -13,117,782 +plus: +Royalty supplemental payments [construction] ($) 0 equals: Nominal capital expenditure [construction] ($) -275,473,424 diff --git a/tests/examples/example_SAM-single-owner-PPA-4.out b/tests/examples/example_SAM-single-owner-PPA-4.out index d516b362..f8506fab 100644 --- a/tests/examples/example_SAM-single-owner-PPA-4.out +++ b/tests/examples/example_SAM-single-owner-PPA-4.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:53 - Calculation Time: 1.326 sec + Simulation Time: 13:19 + Calculation Time: 1.238 sec ***SUMMARY OF RESULTS*** @@ -204,6 +204,8 @@ Capital expenditure schedule [construction] (%) 100.0 Overnight capital expenditure [construction] ($) -215,055,748 plus: Inflation cost [construction] ($) -10,752,787 +plus: +Royalty supplemental payments [construction] ($) 0 equals: Nominal capital expenditure [construction] ($) -225,808,536 @@ -439,5 +441,5 @@ Interest earned on reserves ($) 0 0 0 ***EXTENDED ECONOMICS*** Royalty Holder NPV: 50.59 MUSD - Royalty Holder Average Annual Revenue: 4.40 MUSD/yr + Royalty Holder Average Annual Revenue: 4.19 MUSD/yr Royalty Holder Total Revenue: 87.93 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA-4b.out b/tests/examples/example_SAM-single-owner-PPA-4b.out new file mode 100644 index 00000000..62df7b29 --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-4b.out @@ -0,0 +1,447 @@ + ***************** + ***CASE REPORT*** + ***************** + +Simulation Metadata +---------------------- + GEOPHIRES Version: 3.11.18 + Simulation Date: 2026-02-27 + Simulation Time: 13:08 + Calculation Time: 1.296 sec + + ***SUMMARY OF RESULTS*** + + End-Use Option: Electricity + Average Net Electricity Production: 54.96 MW + Electricity breakeven price: 7.30 cents/kWh + Total CAPEX: 244.26 MUSD + Total CAPEX ($/kW): 4432 USD/kW + Number of production wells: 6 + Number of injection wells: 6 + Flowrate per production well: 100.0 kg/sec + Well depth: 2.6 kilometer + Geothermal gradient: 74 degC/km + + + ***ECONOMIC PARAMETERS*** + + Economic Model = SAM Single Owner PPA + Real Discount Rate: 8.00 % + Nominal Discount Rate: 10.16 % + WACC: 7.49 % + Investment Tax Credit: 73.28 MUSD + Project lifetime: 20 yr + Capacity factor: 90.0 % + Project NPV: 74.94 MUSD + After-tax IRR: 17.48 % + Project VIR=PI=PIR: 1.57 + Project MOIC: 3.29 + Project Payback Period: 7.34 yr + Estimated Jobs Created: 125 + + ***ENGINEERING PARAMETERS*** + + Number of Production Wells: 6 + Number of Injection Wells: 6 + Well depth: 2.6 kilometer + Water loss rate: 10.0 % + Pump efficiency: 80.0 % + Injection temperature: 56.7 degC + Production Wellbore heat transmission calculated with Ramey's model + Average production well temperature drop: 2.2 degC + Flowrate per production well: 100.0 kg/sec + Injection well casing ID: 9.625 in + Production well casing ID: 9.625 in + Number of times redrilling: 0 + Power plant type: Supercritical ORC + + + ***RESOURCE CHARACTERISTICS*** + + Maximum reservoir temperature: 500.0 degC + Number of segments: 1 + Geothermal gradient: 74 degC/km + + + ***RESERVOIR PARAMETERS*** + + Reservoir Model = Multiple Parallel Fractures Model (Gringarten) + Bottom-hole temperature: 202.40 degC + Fracture model = Square + Well separation: fracture height: 165.00 meter + Fracture area: 27225.00 m**2 + Number of fractures calculated with reservoir volume and fracture separation as input + Number of fractures: 4083 + Fracture separation: 18.00 meter + Reservoir volume: 2000000000 m**3 + Reservoir hydrostatic pressure: 24578.69 kPa + Plant outlet pressure: 6894.76 kPa + Production wellhead pressure: 2240.80 kPa + Productivity Index: 2.47 kg/sec/bar + Injectivity Index: 3.00 kg/sec/bar + Reservoir density: 2800.00 kg/m**3 + Reservoir thermal conductivity: 3.05 W/m/K + Reservoir heat capacity: 790.00 J/kg/K + + + ***RESERVOIR SIMULATION RESULTS*** + + Maximum Production Temperature: 200.4 degC + Average Production Temperature: 200.2 degC + Minimum Production Temperature: 198.6 degC + Initial Production Temperature: 198.6 degC + Average Reservoir Heat Extraction: 360.65 MW + Production Wellbore Heat Transmission Model = Ramey Model + Average Production Well Temperature Drop: 2.2 degC + Average Injection Well Pump Pressure Drop: -3478.9 kPa + Average Production Well Pump Pressure Drop: 4583.0 kPa + + + ***CAPITAL COSTS (M$)*** + + Drilling and completion costs: 49.18 MUSD + Drilling and completion costs per vertical production well: 3.37 MUSD + Drilling and completion costs per vertical injection well: 3.37 MUSD + Drilling and completion costs per non-vertical section: 2.14 MUSD + Stimulation costs: 9.06 MUSD + Surface power plant costs: 144.44 MUSD + Field gathering system costs: 8.50 MUSD + Total surface equipment costs: 152.93 MUSD + Exploration costs: 3.89 MUSD + Overnight Capital Cost: 215.06 MUSD + Royalty supplemental payments during construction: 2.25 MUSD + Inflation costs during construction: 22.23 MUSD + Interest during construction: 4.73 MUSD + Total CAPEX: 244.26 MUSD + + + ***OPERATING AND MAINTENANCE COSTS (M$/yr)*** + + Wellfield maintenance costs: 1.15 MUSD/yr + Power plant maintenance costs: 3.90 MUSD/yr + Water costs: 1.58 MUSD/yr + Average Annual Royalty Cost: 2.91 MUSD/yr + Total operating and maintenance costs: 9.54 MUSD/yr + + + ***SURFACE EQUIPMENT SIMULATION RESULTS*** + + Initial geofluid availability: 0.19 MW/(kg/s) + Maximum Total Electricity Generation: 59.02 MW + Average Total Electricity Generation: 58.87 MW + Minimum Total Electricity Generation: 57.74 MW + Initial Total Electricity Generation: 57.74 MW + Maximum Net Electricity Generation: 55.11 MW + Average Net Electricity Generation: 54.96 MW + Minimum Net Electricity Generation: 53.82 MW + Initial Net Electricity Generation: 53.82 MW + Average Annual Total Electricity Generation: 464.13 GWh + Average Annual Net Electricity Generation: 433.32 GWh + Initial pumping power/net installed power: 7.27 % + Average Pumping Power: 3.91 MW + Heat to Power Conversion Efficiency: 15.24 % + + ************************************************************ + * HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ************************************************************ + YEAR THERMAL GEOFLUID PUMP NET FIRST LAW + DRAWDOWN TEMPERATURE POWER POWER EFFICIENCY + (degC) (MW) (MW) (%) + 1 1.0000 198.64 3.9147 53.8242 15.0894 + 2 1.0055 199.73 3.9097 54.6130 15.1937 + 3 1.0065 199.93 3.9088 54.7578 15.2128 + 4 1.0070 200.03 3.9083 54.8329 15.2226 + 5 1.0074 200.10 3.9080 54.8826 15.2292 + 6 1.0076 200.15 3.9077 54.9191 15.2339 + 7 1.0078 200.19 3.9075 54.9478 15.2377 + 8 1.0080 200.22 3.9074 54.9713 15.2408 + 9 1.0081 200.25 3.9073 54.9911 15.2434 + 10 1.0082 200.27 3.9072 55.0082 15.2456 + 11 1.0083 200.30 3.9071 55.0231 15.2476 + 12 1.0084 200.31 3.9070 55.0364 15.2493 + 13 1.0085 200.33 3.9069 55.0483 15.2509 + 14 1.0086 200.34 3.9068 55.0591 15.2523 + 15 1.0087 200.36 3.9068 55.0690 15.2536 + 16 1.0087 200.37 3.9067 55.0781 15.2547 + 17 1.0088 200.38 3.9067 55.0864 15.2558 + 18 1.0088 200.39 3.9066 55.0942 15.2569 + 19 1.0089 200.40 3.9066 55.1015 15.2578 + 20 1.0089 200.41 3.9065 55.1083 15.2587 + + + ******************************************************************* + * ANNUAL HEATING, COOLING AND/OR ELECTRICITY PRODUCTION PROFILE * + ******************************************************************* + YEAR ELECTRICITY HEAT RESERVOIR PERCENTAGE OF + PROVIDED EXTRACTED HEAT CONTENT TOTAL HEAT MINED + (GWh/year) (GWh/year) (10^15 J) (%) + 1 428.5 2826.8 606.53 1.65 + 2 431.2 2836.1 596.32 3.31 + 3 432.0 2838.9 586.10 4.96 + 4 432.5 2840.6 575.87 6.62 + 5 432.8 2841.7 565.64 8.28 + 6 433.1 2842.6 555.41 9.94 + 7 433.3 2843.3 545.17 11.60 + 8 433.5 2843.9 534.94 13.26 + 9 433.6 2844.4 524.70 14.92 + 10 433.7 2844.9 514.45 16.58 + 11 433.9 2845.2 504.21 18.24 + 12 434.0 2845.6 493.97 19.90 + 13 434.0 2845.9 483.72 21.56 + 14 434.1 2846.2 473.48 23.23 + 15 434.2 2846.4 463.23 24.89 + 16 434.3 2846.7 452.98 26.55 + 17 434.3 2846.9 442.73 28.21 + 18 434.4 2847.1 432.48 29.87 + 19 434.4 2847.3 422.23 31.53 + 20 434.5 2847.5 411.98 33.20 + + *************************** + * SAM CASH FLOW PROFILE * + *************************** +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + Year -2 Year -1 Year 0 Year 1 Year 2 Year 3 Year 4 Year 5 Year 6 Year 7 Year 8 Year 9 Year 10 Year 11 Year 12 Year 13 Year 14 Year 15 Year 16 Year 17 Year 18 Year 19 Year 20 +CONSTRUCTION +Capital expenditure schedule [construction] (%) 33.30 33.30 33.30 +Overnight capital expenditure [construction] ($) -71,685,249 -71,685,249 -71,685,249 +plus: +Inflation cost [construction] ($) -3,584,262 -7,347,738 -11,299,387 +plus: +Royalty supplemental payments [construction] ($) -1,000,000 -1,000,000 -250,000 +equals: +Nominal capital expenditure [construction] ($) -76,269,512 -80,032,988 -83,234,637 + +Issuance of equity [construction] ($) 45,761,707 48,019,793 49,940,782 +Issuance of debt [construction] ($) 30,507,805 32,013,195 33,293,855 +Debt balance [construction] ($) 30,507,805 64,046,390 100,542,564 +Debt interest payment [construction] ($) 0 1,525,390 3,202,320 + +Installed cost [construction] ($) -76,269,512 -81,558,378 -86,436,956 +After-tax net cash flow [construction] ($) -45,761,707 -48,019,793 -49,940,782 + +ENERGY +Electricity to grid (kWh) 0.0 428,554,957 431,240,089 432,051,822 432,530,792 432,865,667 433,120,646 433,325,167 433,495,101 433,639,945 433,765,810 433,876,853 433,976,022 434,065,480 434,146,861 434,221,417 434,290,137 434,353,843 434,413,216 434,468,735 434,518,614 +Electricity from grid (kWh) 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.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 +Electricity to grid net (kWh) 0.0 428,554,957 431,240,089 432,051,822 432,530,792 432,865,667 433,120,646 433,325,167 433,495,101 433,639,945 433,765,810 433,876,853 433,976,022 434,065,480 434,146,861 434,221,417 434,290,137 434,353,843 434,413,216 434,468,735 434,518,614 + +REVENUE +PPA price (cents/kWh) 0.0 8.0 8.0 8.32 8.64 8.97 9.29 9.61 9.93 10.25 10.58 10.90 11.22 11.54 11.86 12.19 12.51 12.83 13.15 13.47 13.80 +PPA revenue ($) 0 34,284,397 34,499,207 35,955,353 37,387,962 38,810,736 40,228,246 41,642,549 43,054,733 44,465,440 45,875,072 47,283,899 48,692,110 50,099,838 51,507,184 52,914,222 54,321,010 55,727,598 57,134,026 58,540,317 59,946,188 +Curtailment payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Capacity payment revenue ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Salvage value ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 122,132,423 +Total revenue ($) 0 34,284,397 34,499,207 35,955,353 37,387,962 38,810,736 40,228,246 41,642,549 43,054,733 44,465,440 45,875,072 47,283,899 48,692,110 50,099,838 51,507,184 52,914,222 54,321,010 55,727,598 57,134,026 58,540,317 182,078,611 + +Property tax net assessed value ($) 0 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 244,264,846 + +OPERATING EXPENSES +O&M fixed expense ($) 0 6,876,143 6,876,143 6,876,143 6,876,143 6,876,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 6,726,143 +Royalty rate (%) 2.50 2.50 2.50 2.50 2.50 5.0 5.0 5.0 5.0 5.0 7.50 7.50 7.50 7.50 7.50 7.50 7.50 7.50 7.50 7.50 +O&M production-based expense ($) 0 857,110 862,480 898,884 934,699 970,268 2,011,412 2,082,127 2,152,737 2,223,272 2,293,754 3,546,292 3,651,908 3,757,488 3,863,039 3,968,567 4,074,076 4,179,570 4,285,052 4,390,524 4,495,964 +O&M capacity-based expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Fuel expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Electricity purchase ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Property tax expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Insurance expense ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total operating expenses ($) 0 7,733,253 7,738,624 7,775,027 7,810,842 7,846,412 8,737,556 8,808,271 8,878,880 8,949,415 9,019,897 10,272,436 10,378,052 10,483,631 10,589,182 10,694,710 10,800,219 10,905,713 11,011,195 11,116,667 11,222,108 + +EBITDA ($) 0 26,551,143 26,760,584 28,180,325 29,577,119 30,964,324 31,490,690 32,834,278 34,175,853 35,516,025 36,855,175 37,011,464 38,314,058 39,616,206 40,918,001 42,219,512 43,520,791 44,821,885 46,122,831 47,423,650 170,856,504 + +OPERATING ACTIVITIES +EBITDA ($) 0 26,551,143 26,760,584 28,180,325 29,577,119 30,964,324 31,490,690 32,834,278 34,175,853 35,516,025 36,855,175 37,011,464 38,314,058 39,616,206 40,918,001 42,219,512 43,520,791 44,821,885 46,122,831 47,423,650 170,856,504 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +plus PBI if not available for debt service: +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Debt interest payment ($) 0 5,027,128 4,875,095 4,715,460 4,547,843 4,371,845 4,187,048 3,993,011 3,789,271 3,575,345 3,350,723 3,114,869 2,867,223 2,607,194 2,334,164 2,047,482 1,746,467 1,430,400 1,098,530 750,067 384,181 +Cash flow from operating activities ($) 0 21,524,015 21,885,489 23,464,866 25,029,276 26,592,479 27,303,642 28,841,267 30,386,582 31,940,679 33,504,453 33,896,595 35,446,835 37,009,013 38,583,838 40,172,030 41,774,325 43,391,485 45,024,300 46,673,583 170,472,323 + +INVESTING ACTIVITIES +Total installed cost ($) -244,264,846 +Debt closing costs ($) 0 +Debt up-front fee ($) 0 +minus: +Total IBI income ($) 0 +Total CBI income ($) 0 +equals: +Purchase of property ($) -244,264,846 +plus: +Reserve (increase)/decrease debt service ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease working capital ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease receivables ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve (increase)/decrease major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 1 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 2 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserve capital spending major equipment 3 ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash flow from investing activities ($) -244,264,846 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +FINANCING ACTIVITIES +Issuance of equity ($) 143,722,282 +Size of debt ($) 100,542,564 +minus: +Debt principal payment ($) 0 3,040,667 3,192,701 3,352,336 3,519,952 3,695,950 3,880,748 4,074,785 4,278,524 4,492,450 4,717,073 4,952,927 5,200,573 5,460,602 5,733,632 6,020,313 6,321,329 6,637,395 6,969,265 7,317,728 7,683,615 +equals: +Cash flow from financing activities ($) 244,264,846 -3,040,667 -3,192,701 -3,352,336 -3,519,952 -3,695,950 -3,880,748 -4,074,785 -4,278,524 -4,492,450 -4,717,073 -4,952,927 -5,200,573 -5,460,602 -5,733,632 -6,020,313 -6,321,329 -6,637,395 -6,969,265 -7,317,728 -7,683,615 + +PROJECT RETURNS +Pre-tax Cash Flow: +Cash flow from operating activities ($) 0 21,524,015 21,885,489 23,464,866 25,029,276 26,592,479 27,303,642 28,841,267 30,386,582 31,940,679 33,504,453 33,896,595 35,446,835 37,009,013 38,583,838 40,172,030 41,774,325 43,391,485 45,024,300 46,673,583 170,472,323 +Cash flow from investing activities ($) -244,264,846 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Cash flow from financing activities ($) 244,264,846 -3,040,667 -3,192,701 -3,352,336 -3,519,952 -3,695,950 -3,880,748 -4,074,785 -4,278,524 -4,492,450 -4,717,073 -4,952,927 -5,200,573 -5,460,602 -5,733,632 -6,020,313 -6,321,329 -6,637,395 -6,969,265 -7,317,728 -7,683,615 +Total pre-tax cash flow ($) 0 18,483,348 18,692,788 20,112,530 21,509,324 22,896,528 23,422,894 24,766,482 26,108,058 27,448,229 28,787,380 28,943,668 30,246,263 31,548,411 32,850,206 34,151,716 35,452,996 36,754,089 38,055,035 39,355,855 162,788,708 + +Pre-tax Returns: +Issuance of equity ($) 143,722,282 +Total pre-tax cash flow ($) 0 18,483,348 18,692,788 20,112,530 21,509,324 22,896,528 23,422,894 24,766,482 26,108,058 27,448,229 28,787,380 28,943,668 30,246,263 31,548,411 32,850,206 34,151,716 35,452,996 36,754,089 38,055,035 39,355,855 162,788,708 +Total pre-tax returns ($) -143,722,282 18,483,348 18,692,788 20,112,530 21,509,324 22,896,528 23,422,894 24,766,482 26,108,058 27,448,229 28,787,380 28,943,668 30,246,263 31,548,411 32,850,206 34,151,716 35,452,996 36,754,089 38,055,035 39,355,855 162,788,708 + +After-tax Returns: +Total pre-tax returns ($) -143,722,282 18,483,348 18,692,788 20,112,530 21,509,324 22,896,528 23,422,894 24,766,482 26,108,058 27,448,229 28,787,380 28,943,668 30,246,263 31,548,411 32,850,206 34,151,716 35,452,996 36,754,089 38,055,035 39,355,855 162,788,708 +Federal ITC total income ($) 0 73,279,454 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal tax benefit (liability) ($) 0 -3,189,910 -2,246,777 -2,555,229 -2,860,758 -3,166,052 -3,304,942 -3,605,240 -3,907,040 -4,210,555 -4,515,960 -4,592,546 -4,895,308 -5,200,401 -5,507,964 -5,818,138 -6,131,066 -6,446,898 -6,765,787 -7,087,891 -31,265,785 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -1,143,337 -805,296 -915,853 -1,025,361 -1,134,786 -1,184,567 -1,292,201 -1,400,373 -1,509,160 -1,618,624 -1,646,074 -1,754,591 -1,863,943 -1,974,181 -2,085,354 -2,197,515 -2,310,716 -2,425,013 -2,540,463 -11,206,375 +Total after-tax returns ($) -143,722,282 87,429,554 15,640,715 16,641,448 17,623,204 18,595,691 18,933,385 19,869,041 20,800,645 21,728,514 22,652,796 22,705,049 23,596,364 24,484,067 25,368,061 26,248,224 27,124,415 27,996,476 28,864,236 29,727,500 120,316,548 + +After-tax net cash flow ($) -45,761,707 -48,019,793 -49,940,782 87,429,554 15,640,715 16,641,448 17,623,204 18,595,691 18,933,385 19,869,041 20,800,645 21,728,514 22,652,796 22,705,049 23,596,364 24,484,067 25,368,061 26,248,224 27,124,415 27,996,476 28,864,236 29,727,500 120,316,548 +After-tax cumulative IRR (%) NaN NaN NaN -23.19 -14.77 -7.41 -1.66 2.68 5.86 8.29 10.17 11.63 12.79 13.68 14.40 14.99 15.47 15.87 16.19 16.47 16.69 16.88 17.48 +After-tax cumulative NPV ($) -45,761,707 -89,352,659 -130,506,226 -65,104,911 -54,484,022 -44,225,814 -34,364,352 -24,918,420 -16,187,965 -7,871,064 32,765 7,527,682 14,620,759 21,074,498 27,162,995 32,897,879 38,291,799 43,358,125 48,110,708 52,563,667 56,731,224 60,627,556 74,942,806 + +AFTER-TAX LCOE AND PPA PRICE +Annual costs ($) -45,761,707 -48,019,793 -49,940,782 53,145,157 -18,858,492 -19,313,904 -19,764,758 -20,215,045 -21,294,860 -21,773,507 -22,254,089 -22,736,926 -23,222,277 -24,578,851 -25,095,745 -25,615,771 -26,139,123 -26,665,998 -27,196,596 -27,731,122 -28,269,791 -28,812,817 60,370,360 +PPA revenue ($) 0 0 0 34,284,397 34,499,207 35,955,353 37,387,962 38,810,736 40,228,246 41,642,549 43,054,733 44,465,440 45,875,072 47,283,899 48,692,110 50,099,838 51,507,184 52,914,222 54,321,010 55,727,598 57,134,026 58,540,317 59,946,188 +Electricity to grid (kWh) 0 0 0 428,554,957 431,240,089 432,051,822 432,530,792 432,865,667 433,120,646 433,325,167 433,495,101 433,639,945 433,765,810 433,876,853 433,976,022 434,065,480 434,146,861 434,221,417 434,290,137 434,353,843 434,413,216 434,468,735 434,518,614 + +Present value of annual costs ($) 219,312,570 +Present value of annual energy nominal (kWh) 3,002,517,885 +LCOE Levelized cost of energy nominal (cents/kWh) 7.30 + +Present value of PPA revenue ($) 294,255,377 +Present value of annual energy nominal (kWh) 3,002,517,885 +LPPA Levelized PPA price nominal (cents/kWh) 9.80 + +PROJECT STATE INCOME TAXES +EBITDA ($) 0 26,551,143 26,760,584 28,180,325 29,577,119 30,964,324 31,490,690 32,834,278 34,175,853 35,516,025 36,855,175 37,011,464 38,314,058 39,616,206 40,918,001 42,219,512 43,520,791 44,821,885 46,122,831 47,423,650 170,856,504 +State taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State taxable IBI income ($) 0 +State taxable CBI income ($) 0 +minus: +Debt interest payment ($) 0 5,027,128 4,875,095 4,715,460 4,547,843 4,371,845 4,187,048 3,993,011 3,789,271 3,575,345 3,350,723 3,114,869 2,867,223 2,607,194 2,334,164 2,047,482 1,746,467 1,430,400 1,098,530 750,067 384,181 +Total state tax depreciation ($) 0 5,190,628 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 +equals: +State taxable income ($) 0 16,333,387 11,504,233 13,083,610 14,648,020 16,211,223 16,922,386 18,460,011 20,005,326 21,559,424 23,123,197 23,515,339 25,065,579 26,627,757 28,202,582 29,790,774 31,393,069 33,010,229 34,643,044 36,292,327 160,091,067 + +State income tax rate (frac) 0.0 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 0.07 +State tax benefit (liability) ($) 0 -1,143,337 -805,296 -915,853 -1,025,361 -1,134,786 -1,184,567 -1,292,201 -1,400,373 -1,509,160 -1,618,624 -1,646,074 -1,754,591 -1,863,943 -1,974,181 -2,085,354 -2,197,515 -2,310,716 -2,425,013 -2,540,463 -11,206,375 + +PROJECT FEDERAL INCOME TAXES +EBITDA ($) 0 26,551,143 26,760,584 28,180,325 29,577,119 30,964,324 31,490,690 32,834,278 34,175,853 35,516,025 36,855,175 37,011,464 38,314,058 39,616,206 40,918,001 42,219,512 43,520,791 44,821,885 46,122,831 47,423,650 170,856,504 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State tax benefit (liability) ($) 0 -1,143,337 -805,296 -915,853 -1,025,361 -1,134,786 -1,184,567 -1,292,201 -1,400,373 -1,509,160 -1,618,624 -1,646,074 -1,754,591 -1,863,943 -1,974,181 -2,085,354 -2,197,515 -2,310,716 -2,425,013 -2,540,463 -11,206,375 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal taxable IBI income ($) 0 +Federal taxable CBI income ($) 0 +Federal taxable PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +minus: +Debt interest payment ($) 0 5,027,128 4,875,095 4,715,460 4,547,843 4,371,845 4,187,048 3,993,011 3,789,271 3,575,345 3,350,723 3,114,869 2,867,223 2,607,194 2,334,164 2,047,482 1,746,467 1,430,400 1,098,530 750,067 384,181 +Total federal tax depreciation ($) 0 5,190,628 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 10,381,256 +equals: +Federal taxable income ($) 0 15,190,050 10,698,936 12,167,757 13,622,659 15,076,437 15,737,819 17,167,810 18,604,953 20,050,264 21,504,573 21,869,265 23,310,989 24,763,814 26,228,401 27,705,419 29,195,554 30,699,513 32,218,031 33,751,864 148,884,692 + +Federal income tax rate (frac) 0.0 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 0.21 +Federal tax benefit (liability) ($) 0 -3,189,910 -2,246,777 -2,555,229 -2,860,758 -3,166,052 -3,304,942 -3,605,240 -3,907,040 -4,210,555 -4,515,960 -4,592,546 -4,895,308 -5,200,401 -5,507,964 -5,818,138 -6,131,066 -6,446,898 -6,765,787 -7,087,891 -31,265,785 + +CASH INCENTIVES +Federal IBI income ($) 0 +State IBI income ($) 0 +Utility IBI income ($) 0 +Other IBI income ($) 0 +Total IBI income ($) 0 + +Federal CBI income ($) 0 +State CBI income ($) 0 +Utility CBI income ($) 0 +Other CBI income ($) 0 +Total CBI income ($) 0 + +Federal PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Utility PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Other PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Total PBI income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +TAX CREDITS +Federal PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State PTC income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Federal ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC percent income ($) 0 73,279,454 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Federal ITC total income ($) 0 73,279,454 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +State ITC amount income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC percent income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +State ITC total income ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +DEBT REPAYMENT +Debt balance ($) 100,542,564 97,501,897 94,309,196 90,956,861 87,436,908 83,740,958 79,860,211 75,785,426 71,506,901 67,014,451 62,297,378 57,344,452 52,143,879 46,683,277 40,949,646 34,929,332 28,608,003 21,970,608 15,001,343 7,683,615 0 +Debt interest payment ($) 0 5,027,128 4,875,095 4,715,460 4,547,843 4,371,845 4,187,048 3,993,011 3,789,271 3,575,345 3,350,723 3,114,869 2,867,223 2,607,194 2,334,164 2,047,482 1,746,467 1,430,400 1,098,530 750,067 384,181 +Debt principal payment ($) 0 3,040,667 3,192,701 3,352,336 3,519,952 3,695,950 3,880,748 4,074,785 4,278,524 4,492,450 4,717,073 4,952,927 5,200,573 5,460,602 5,733,632 6,020,313 6,321,329 6,637,395 6,969,265 7,317,728 7,683,615 +Debt total payment ($) 0 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 + +DSCR (DEBT FRACTION) +EBITDA ($) 0 26,551,143 26,760,584 28,180,325 29,577,119 30,964,324 31,490,690 32,834,278 34,175,853 35,516,025 36,855,175 37,011,464 38,314,058 39,616,206 40,918,001 42,219,512 43,520,791 44,821,885 46,122,831 47,423,650 170,856,504 +minus: +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +equals: +Cash available for debt service (CAFDS) ($) 0 26,551,143 26,760,584 28,180,325 29,577,119 30,964,324 31,490,690 32,834,278 34,175,853 35,516,025 36,855,175 37,011,464 38,314,058 39,616,206 40,918,001 42,219,512 43,520,791 44,821,885 46,122,831 47,423,650 170,856,504 +Debt total payment ($) 0 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 8,067,795 +DSCR (pre-tax) 0.0 3.29 3.32 3.49 3.67 3.84 3.90 4.07 4.24 4.40 4.57 4.59 4.75 4.91 5.07 5.23 5.39 5.56 5.72 5.88 21.18 + +RESERVES +Reserves working capital funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves working capital balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves debt service funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves debt service balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves receivables funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves receivables balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 1 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 1 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 2 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 2 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves major equipment 3 funding ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 disbursement ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Reserves major equipment 3 balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 + +Reserves total reserves balance ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +Interest on reserves (%/year) 1.75 +Interest earned on reserves ($) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 +--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + + ***EXTENDED ECONOMICS*** + + Royalty Holder NPV: 31.15 MUSD + Royalty Holder Average Annual Revenue: 2.63 MUSD/yr + Royalty Holder Total Revenue: 60.50 MUSD diff --git a/tests/examples/example_SAM-single-owner-PPA-4b.txt b/tests/examples/example_SAM-single-owner-PPA-4b.txt new file mode 100644 index 00000000..153555d3 --- /dev/null +++ b/tests/examples/example_SAM-single-owner-PPA-4b.txt @@ -0,0 +1,89 @@ +# Example: SAM Single Owner PPA Economic Model: 50 MWe with Royalties Schedule and Royalty Supplemental Payments +# This example models example_SAM-single-owner with a royalties schedule and royalty supplemental payments +# See "SAM Economic Models" in GEOPHIRES documentation: https://softwareengineerprogrammer.github.io/GEOPHIRES/SAM-Economic-Models.html#royalties + + +# *** ECONOMIC/FINANCIAL PARAMETERS *** +# ************************************* +Economic Model, 5, -- SAM Single Owner PPA + +Royalty Rate Schedule, 0.025 * 5, 0.05 * 5, 0.075 +Royalty Supplemental Payments, 1*2, 0.25 * 6, 0.1 + +Starting Electricity Sale Price, 0.08 +Ending Electricity Sale Price, 1.00 +Electricity Escalation Rate Per Year, 0.00322 +Electricity Escalation Start Year, 1 + +Fraction of Investment in Bonds, .4 +Inflated Bond Interest Rate, .05 +Discount Rate, 0.08 +Inflation Rate, .02 +Inflation Rate During Construction, 0.05 + +Combined Income Tax Rate, .28 +Investment Tax Credit Rate, 0.3 +Property Tax Rate, 0 + +Capital Cost for Power Plant for Electricity Generation, 1900 + +# *** SURFACE & SUBSURFACE TECHNICAL PARAMETERS *** +# ************************************************* +End-Use Option, 1, -- Electricity +Power Plant Type, 2, -- Supercritical ORC +Plant Lifetime, 20 +Construction Years, 3 + +Reservoir Model, 1 + +Reservoir Volume Option, 2, -- RES_VOL_FRAC_SEP (Specify reservoir volume and fracture separation) +Reservoir Volume, 2000000000, -- m**3 +Fracture Shape, 3, -- Square +Fracture Separation, 18 +Fracture Height, 165 + +Reservoir Density, 2800 +Reservoir Depth, 2.6, -- km +Reservoir Heat Capacity, 790 +Reservoir Thermal Conductivity, 3.05 +Reservoir Porosity, 0.0118 + +Injectivity Index, 3, -- [kg/s/bar] NREL ATB conservative scenario (https://atb.nrel.gov/electricity/2024/geothermal) +Productivity Index, 2.4742, -- [kg/s/bar] NREL ATB conservative scenario (https://atb.nrel.gov/electricity/2024/geothermal) + +Number of Segments, 1 +Gradient 1, 74 + +Number of Injection Wells, 6 +Number of Production Wells, 6 + +Production Flow Rate per Well, 100 + +Production Well Diameter, 9.625 +Injection Well Diameter, 9.625 + +Well Separation, 365 feet + +Ramey Production Wellbore Model, 1 +Injection Temperature, 60 degC +Injection Wellbore Temperature Gain, 3 +Plant Outlet Pressure, 1000 psi +Production Wellhead Pressure, 325 psi + +Utilization Factor, .9 +Water Loss Fraction, 0.10 +Maximum Drawdown, 0.0066 +Ambient Temperature, 10, -- degC +Surface Temperature, 10, -- degC +Circulation Pump Efficiency, 0.80 + +Well Geometry Configuration, 4 +Has Nonvertical Section, True +Multilaterals Cased, True +Number of Multilateral Sections, 3 +Nonvertical Length per Multilateral Section, 1433, -- meters + +# *** SIMULATION PARAMETERS *** +# ***************************** +Maximum Temperature, 500 +Time steps per year, 12 diff --git a/tests/examples/example_SAM-single-owner-PPA-5.out b/tests/examples/example_SAM-single-owner-PPA-5.out index 7fec0283..15b5367f 100644 --- a/tests/examples/example_SAM-single-owner-PPA-5.out +++ b/tests/examples/example_SAM-single-owner-PPA-5.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:53 - Calculation Time: 1.854 sec + Simulation Time: 13:19 + Calculation Time: 1.784 sec ***SUMMARY OF RESULTS*** @@ -107,8 +107,8 @@ Simulation Metadata Total surface equipment costs: 298.30 MUSD Exploration costs: 120.00 MUSD Overnight Capital Cost: 599.42 MUSD - Interest during construction: 28.51 MUSD Inflation costs during construction: 82.70 MUSD + Interest during construction: 28.51 MUSD Total CAPEX: 710.63 MUSD @@ -222,6 +222,8 @@ Capital expenditure schedule [construction] (%) 1.0 2.0 7.0 Overnight capital expenditure [construction] ($) -5,994,215 -11,988,430 -41,959,506 -59,942,151 -119,884,303 -119,884,303 -239,768,605 plus: Inflation cost [construction] ($) -137,867 -557,810 -2,962,306 -5,707,868 -14,435,638 -17,524,996 -41,370,820 +plus: +Royalty supplemental payments [construction] ($) 0 0 0 0 0 0 0 equals: Nominal capital expenditure [construction] ($) -6,132,082 -12,546,240 -44,921,812 -65,650,020 -134,319,940 -137,409,299 -281,139,426 diff --git a/tests/examples/example_SAM-single-owner-PPA-5_cash-flow.csv b/tests/examples/example_SAM-single-owner-PPA-5_cash-flow.csv index 5efdbe62..2690ec25 100644 --- a/tests/examples/example_SAM-single-owner-PPA-5_cash-flow.csv +++ b/tests/examples/example_SAM-single-owner-PPA-5_cash-flow.csv @@ -4,6 +4,8 @@ Capital expenditure schedule [construction] (%),1,2,7,10,20,20,40,,,,,,,,,,,,,,, Overnight capital expenditure [construction] ($),-5994215,-11988430,-41959506,-59942151,-119884303,-119884303,-239768605,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, plus:,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Inflation cost [construction] ($),-137867,-557810,-2962306,-5707868,-14435638,-17524996,-41370820,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +plus:,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +Royalty supplemental payments [construction] ($),0,0,0,0,0,0,0,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, equals:,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, Nominal capital expenditure [construction] ($),-6132082,-12546240,-44921812,-65650020,-134319940,-137409299,-281139426,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, ,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/tests/examples/example_SAM-single-owner-PPA.out b/tests/examples/example_SAM-single-owner-PPA.out index a9cd2585..e5ecde31 100644 --- a/tests/examples/example_SAM-single-owner-PPA.out +++ b/tests/examples/example_SAM-single-owner-PPA.out @@ -4,10 +4,10 @@ Simulation Metadata ---------------------- - GEOPHIRES Version: 3.11.16 + GEOPHIRES Version: 3.11.18 Simulation Date: 2026-02-27 - Simulation Time: 09:49 - Calculation Time: 1.227 sec + Simulation Time: 13:15 + Calculation Time: 1.201 sec ***SUMMARY OF RESULTS*** @@ -203,6 +203,8 @@ Capital expenditure schedule [construction] (%) 100.0 Overnight capital expenditure [construction] ($) -215,055,748 plus: Inflation cost [construction] ($) -10,752,787 +plus: +Royalty supplemental payments [construction] ($) 0 equals: Nominal capital expenditure [construction] ($) -225,808,536 diff --git a/tests/geophires_x_tests/test_economics.py b/tests/geophires_x_tests/test_economics.py index 02b41be2..99e373fa 100644 --- a/tests/geophires_x_tests/test_economics.py +++ b/tests/geophires_x_tests/test_economics.py @@ -3,13 +3,18 @@ import os import sys from pathlib import Path +from typing import Any import numpy_financial as npf # ruff: noqa: I001 # Successful module initialization is dependent on this specific import order. from geophires_x.Model import Model from geophires_x.Economics import CalculateFinancialPerformance -from geophires_x_client import GeophiresXResult, GeophiresXClient, GeophiresInputParameters +from geophires_x_client import ( + GeophiresXResult, + GeophiresXClient, + GeophiresInputParameters, +) from tests.base_test_case import BaseTestCase @@ -109,20 +114,6 @@ def test_well_drilling_cost_correlation_tooltip_text(self): def test_indirect_cost_factor(self) -> None: self.assertEqual(1.12, self._new_model().economics._indirect_cost_factor) - # noinspection PyMethodMayBeStatic - def _new_model(self) -> Model: - stash_cwd = Path.cwd() - stash_sys_argv = sys.argv - - sys.argv = [''] - - m = Model(enable_geophires_logging_config=False) - - sys.argv = stash_sys_argv - os.chdir(stash_cwd) - - return m - def test_peaking_boiler_cost(self): def _get_result(peaking_boiler_cost_: int) -> GeophiresXResult: return GeophiresXClient().get_geophires_result( @@ -143,3 +134,32 @@ def _lcoh_pbc(r: GeophiresXResult) -> tuple[float, float]: lcoh, peaking_boiler_cost = _lcoh_pbc(_get_result(0)) self.assertLess(lcoh, 13.19) self.assertEqual(0, peaking_boiler_cost) + + # noinspection PyMethodMayBeStatic + def _new_model( + self, input_file: Path | None = None, additional_params: dict[str, Any] | None = None, read_and_calculate=False + ) -> Model: + model_args = {'enable_geophires_logging_config': False} + + if input_file is not None: + if additional_params is not None: + params = GeophiresInputParameters(from_file_path=input_file, params=additional_params) + input_file = params.as_file_path() + + model_args['input_file'] = input_file + + stash_cwd = Path.cwd() + stash_sys_argv = sys.argv + + sys.argv = [''] + + m = Model(**model_args) + + sys.argv = stash_sys_argv + os.chdir(stash_cwd) + + if read_and_calculate: + m.read_parameters() + m.Calculate() + + return m diff --git a/tests/geophires_x_tests/test_economics_sam.py b/tests/geophires_x_tests/test_economics_sam.py index 1ff070a3..45c30619 100644 --- a/tests/geophires_x_tests/test_economics_sam.py +++ b/tests/geophires_x_tests/test_economics_sam.py @@ -10,6 +10,7 @@ import numpy as np import numpy_financial as npf import pandas as pd +from pint.facets.plain import PlainQuantity from geophires_x.Parameter import listParameter @@ -1019,22 +1020,101 @@ def test_royalty_rate_schedule(self): royalty_rate = 0.1 escalation_rate = 0.01 max_rate = royalty_rate + 5 * escalation_rate - m: Model = EconomicsSamTestCase._new_model( - self._egs_test_file_path(), - additional_params={ - 'Royalty Rate': royalty_rate, - 'Royalty Rate Escalation': escalation_rate, - 'Royalty Rate Maximum': max_rate, - }, - ) + rate_based_params = { + 'Royalty Rate': royalty_rate, + 'Royalty Rate Escalation': escalation_rate, + 'Royalty Rate Maximum': max_rate, + } - schedule: list[float] = _get_royalty_rate_schedule(m) + expected_schedule_1 = [0.1, 0.11, 0.12, 0.13, 0.14, *[0.15] * 20] + expected_schedule_2 = [*[0.03] * 10, *[0.04] * 10, *[0.05] * 5] + + rate_based_case_name = 'Rate-based' + + base_additional_params = {'Plant Lifetime': 25} + + royalty_rate_schedule_param_name = 'Royalty Rate Schedule' + + for params_type_case in [ + (rate_based_case_name, rate_based_params, expected_schedule_1), + ( + 'Schedule-based 1', + {royalty_rate_schedule_param_name: '0.1, 0.11, 0.12, 0.13, 0.14, 0.15 * 15'}, + expected_schedule_1, + ), + ( + 'Schedule-based 2', + {royalty_rate_schedule_param_name: '0.03, 0.03 * 9, 0.04 * 10, 0.05'}, + expected_schedule_2, + ), + ( + 'Schedule-based 3', + {royalty_rate_schedule_param_name: '0.03 * 10, 0.04 * 10, 0.05'}, + expected_schedule_2, + ), + ]: + case_name = params_type_case[0] + with self.subTest(case_name): + m: Model = EconomicsSamTestCase._new_model( + self._egs_test_file_path(), + additional_params={**params_type_case[1], **base_additional_params}, + ) - self.assertListAlmostEqual( - [0.1, 0.11, 0.12, 0.13, 0.14, *[0.15] * 15], - schedule, - places=3, - ) + schedule: list[float] = _get_royalty_rate_schedule(m) + expected_schedule = params_type_case[2] + + self.assertListAlmostEqual( + expected_schedule, + schedule, + places=3, + ) + + case_result: GeophiresXResult = EconomicsSamTestCase._get_result_from_model(m) + + royalty_rate_row = self._get_cash_flow_row( + case_result.result['SAM CASH FLOW PROFILE'], 'Royalty rate (%)' + )[1:] + self.assertIsNotNone(royalty_rate_row) + + if case_name == rate_based_case_name: + equivalent_schedule_param = ', '.join([str(float(it) / 100.0) for it in royalty_rate_row]) + equivalent_schedule_based_result: GeophiresXResult = GeophiresXClient().get_geophires_result( + ImmutableGeophiresInputParameters( + from_file_path=self._egs_test_file_path(), + params={ + royalty_rate_schedule_param_name: equivalent_schedule_param, + **base_additional_params, + }, + ) + ) + + equivalent_schedule_based_result_royalty_rate_row = self._get_cash_flow_row( + case_result.result['SAM CASH FLOW PROFILE'], 'Royalty rate (%)' + )[1:] + + self.assertListEqual( + [round(it * 100) for it in expected_schedule], equivalent_schedule_based_result_royalty_rate_row + ) + + def _d_san(d: dict[str, Any]) -> dict[str, Any]: + for k in ['metadata', 'Simulation Metadata']: + d.pop(k, None) + + return d + + try: + self.assertDictAlmostEqual( + _d_san(case_result.result), + _d_san(equivalent_schedule_based_result.result), + percent=0.04698, + ) + except AssertionError as dict_almost_equal_error: + try: + self.assertDictEqual( + _d_san(case_result.result), _d_san(equivalent_schedule_based_result.result) + ) + except AssertionError as dict_exactly_equal_error: + raise dict_exactly_equal_error from dict_almost_equal_error def test_royalty_rate_escalation_start_year(self) -> None: construction_years: int = 5 @@ -1052,7 +1132,7 @@ def _get_result(start_year: int) -> tuple[str, dict[str, Any], GeophiresXResult] 'Royalty Rate Escalation Start Year': start_year, 'Print Output to Console': 1, } - input_params: GeophiresInputParameters = GeophiresInputParameters( + input_params: GeophiresInputParameters = ImmutableGeophiresInputParameters( from_file_path=_input_file_path, params=_additional_params, ) @@ -1097,6 +1177,41 @@ def _royalty_rates_from_cash_flow(r: GeophiresXResult) -> list[float]: expected_royalties_based_on_cash_flow_ppa_revenue, result_4_royalty_cash_flow_usd, percent=0.0001 ) + def test_royalty_supplemental_payments(self): + plant_lifetime = 25 + construction_years = 5 + + m: Model = EconomicsSamTestCase._new_model( + self._egs_test_file_path(), + additional_params={ + 'Royalty Supplemental Payments': '1 * 3, 0.25 * 5, 0.1', + 'Plant Lifetime': plant_lifetime, + 'Construction Years': construction_years, + }, + ) + + schedule_usd: list[float] = m.economics.get_royalty_supplemental_payments_schedule_usd(m) + expected_schedule = [1e6, 1e6, 1e6, 0.25e6, 0.25e6, *[0.25e6] * 3, *[0.1e6] * (plant_lifetime - 3)] + + self.assertListAlmostEqual( + expected_schedule, + schedule_usd, + places=3, + ) + + result: GeophiresXResult = EconomicsSamTestCase._get_result_from_model(m) + + opex_cashflow = self._get_cash_flow_row(result.result['SAM CASH FLOW PROFILE'], 'O&M fixed expense ($)') + operational_years_opex_cashflow_usd = opex_cashflow[construction_years:] + self.assertEqual(150_000, operational_years_opex_cashflow_usd[2] - operational_years_opex_cashflow_usd[3]) + + royalty_holder_total_revenue_vu = result.result['EXTENDED ECONOMICS']['Royalty Holder Total Revenue'] + self.assertAlmostEqual( + royalty_holder_total_revenue_vu['value'], + PlainQuantity(sum(expected_schedule), 'USD').to(royalty_holder_total_revenue_vu['unit']).magnitude, + places=2, + ) + def test_sam_cash_flow_total_after_tax_returns_all_years(self): input_file = self._egs_test_file_path() additional_params = {'Construction Years': 2} @@ -1154,3 +1269,11 @@ def _new_model(input_file: Path, additional_params: dict[str, Any] | None = None m.Calculate() return m + + @staticmethod + def _get_result_from_model(m: Model) -> GeophiresXResult: + m.outputs.PrintOutputs(m) + result: GeophiresXResult = GeophiresXResult(m.outputs.output_file) + # Ideally we'd provide a temporary output file path, but the default (HDR.out) is fine for now... + + return result diff --git a/tests/geophires_x_tests/test_economics_utils.py b/tests/geophires_x_tests/test_economics_utils.py new file mode 100644 index 00000000..442196ef --- /dev/null +++ b/tests/geophires_x_tests/test_economics_utils.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from geophires_x.EconomicsUtils import expand_schedule +from tests.base_test_case import BaseTestCase + + +class EconomicsUtilsTestCase(BaseTestCase): + + def test_expand_schedule(self): + expanded = expand_schedule(['0.03', '0.03 * 9', '0.04 * 10', '0.05'], 25) + self.assertListEqual([*[0.03] * 10, *[0.04] * 10, *[0.05] * 5], expanded) + + self.assertEqual(len(expanded), 25)