Skip to content

Commit 4bd6833

Browse files
Merge pull request #166 from softwareengineerprogrammer/drawdown-parameter-schedule
Drawdown Parameter Schedule [v.3.13.12]
2 parents a03de95 + 465c94f commit 4bd6833

18 files changed

Lines changed: 535 additions & 113 deletions

.bumpversion.cfg

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

.cookiecutterrc

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

.pre-commit-config.yaml

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

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ GEOPHIRES v3 (2023-2026)
88
3.13
99
^^^^
1010

11+
3.13.12: `Drawdown Parameter Schedule <https://github.com/softwareengineerprogrammer/GEOPHIRES/pull/166>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.13.12>`__
12+
1113
3.13.10: `Well integrity parameterization to trigger redrilling <https://github.com/softwareengineerprogrammer/GEOPHIRES/pull/163>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.13.10>`__
1214

1315
3.13.9: `Add hip-ra-x-result.json schema and HipRaXResult <https://github.com/NatLabRockies/GEOPHIRES-X/pull/499>`__ | `release <https://github.com/NREL/GEOPHIRES-X/releases/tag/v3.13.9>`__

README.rst

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

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

6565
.. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat
6666
:target: https://softwareengineerprogrammer.github.io/GEOPHIRES

docs/conf.py

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

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

setup.py

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

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

src/geophires_x/Economics.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@
1616
project_payback_period_parameter, inflation_cost_during_construction_output_parameter, \
1717
interest_during_construction_output_parameter, total_capex_parameter_output_parameter, \
1818
overnight_capital_cost_output_parameter, CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME, \
19-
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, expand_schedule_dsl, \
20-
lcoh_output_parameter, lcoc_output_parameter
19+
_YEAR_INDEX_VALUE_EXPLANATION_SNIPPET, investment_tax_credit_output_parameter, lcoh_output_parameter, lcoc_output_parameter
20+
from geophires_x.ParameterUtils import expand_schedule_dsl
2121
from geophires_x.GeoPHIRESUtils import quantity
2222
from geophires_x.OptionList import Configuration, WellDrillingCostCorrelation, EconomicModel, EndUseOptions, PlantType, \
2323
_WellDrillingCostCorrelationCitation

src/geophires_x/EconomicsUtils.py

Lines changed: 5 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

3-
from geophires_x.GeoPHIRESUtils import is_float, is_int
4-
from geophires_x.Parameter import OutputParameter, SCHEDULE_DSL_MULTIPLIER_SYMBOL
3+
from geophires_x.Parameter import OutputParameter
54
from geophires_x.Units import Units, PercentUnit, TimeUnit, CurrencyUnit, CurrencyFrequencyUnit, EnergyCostUnit
65

76
CONSTRUCTION_CAPEX_SCHEDULE_PARAMETER_NAME = 'Construction CAPEX Schedule'
@@ -248,90 +247,9 @@ def investment_tax_credit_output_parameter() -> OutputParameter:
248247

249248
def expand_schedule_dsl(schedule_strings: list[str | float], total_years: int) -> list[float]:
250249
"""
251-
Parse a duration-based scheduling DSL and expand it into a fixed-length time-series array.
252-
253-
Syntax: `[Value] * [Years], [Value] * [Years], ..., [Terminal Value]`
254-
255-
The terminal (last) value is repeated to fill `total_years`. A bare scalar
256-
(e.g. `['2.5']`) is treated as a terminal value and broadcast across all years.
257-
258-
Examples::
259-
260-
expand_schedule_dsl(['1.0 * 3', '0.1'], total_years=6)
261-
# => [1.0, 1.0, 1.0, 0.1, 0.1, 0.1]
262-
263-
expand_schedule_dsl(['2.5'], total_years=4)
264-
# => [2.5, 2.5, 2.5, 2.5]
265-
266-
:param schedule_strings: list of DSL segment strings. Each element is either
267-
`"<value> * <years>"` (a run-length segment) or `"<value>"` (a scalar,
268-
which becomes the terminal value when it is the last element, or a 1-year
269-
segment otherwise).
270-
:param total_years: The total number of years the expanded array must span
271-
(typically `construction_years + plant_lifetime`).
272-
:returns: A `list[float]` of length `total_years`.
273-
:raises ValueError: On malformed DSL strings or when explicit segments exceed
274-
`total_years`.
250+
Deprecated, call ParameterUtils.expand_schedule_dsl
275251
"""
276252

277-
if total_years <= 0:
278-
return []
279-
280-
if not schedule_strings:
281-
return [0.0] * total_years
282-
283-
segments: list[tuple[float, int | None]] = []
284-
for raw in schedule_strings:
285-
raw = str(raw).strip()
286-
if SCHEDULE_DSL_MULTIPLIER_SYMBOL in raw:
287-
parts = raw.split(SCHEDULE_DSL_MULTIPLIER_SYMBOL)
288-
if len(parts) != 2:
289-
raise ValueError(f'Invalid schedule segment "{raw}": expected "<value> * <years>".')
290-
291-
val_raw = parts[0].strip()
292-
if not is_float(val_raw):
293-
raise ValueError(f'Invalid schedule segment "{raw}": "{val_raw}" is not a float.')
294-
value = float(val_raw)
295-
if value < 0:
296-
raise ValueError(f'Invalid schedule segment "{raw}": {val_raw} is negative.')
297-
298-
years_raw = parts[1].strip()
299-
if not is_int(years_raw):
300-
raise ValueError(f'Invalid schedule segment "{raw}": "{years_raw}" is not an int.')
301-
302-
years = int(years_raw)
303-
if years < 0:
304-
raise ValueError(f'Invalid schedule segment "{raw}": year count must be non-negative.')
305-
segments.append((value, years))
306-
else:
307-
if not is_float(raw):
308-
raise ValueError(f'Invalid schedule segment "{raw}": "{raw}" is not a float.')
309-
310-
value = float(raw)
311-
segments.append((value, None))
312-
313-
result: list[float] = []
314-
terminal_value = 0.0
315-
316-
for idx, (value, years) in enumerate(segments):
317-
is_last = idx == len(segments) - 1
318-
if years is not None:
319-
result.extend([value] * years)
320-
terminal_value = value
321-
else:
322-
if is_last:
323-
terminal_value = value
324-
else:
325-
result.append(value)
326-
terminal_value = value
327-
328-
if len(result) > total_years:
329-
raise ValueError(
330-
f'Invalid schedule: Schedule expands to {len(result)} years ' f'which exceeds total_years={total_years}.'
331-
)
332-
333-
remaining = total_years - len(result)
334-
if remaining > 0:
335-
result.extend([terminal_value] * remaining)
336-
337-
return result
253+
from geophires_x.ParameterUtils import expand_schedule_dsl
254+
255+
return expand_schedule_dsl(schedule_strings, total_years)

src/geophires_x/ParameterUtils.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
from __future__ import annotations
2+
3+
import copy
4+
import logging
5+
6+
from geophires_x.GeoPHIRESUtils import is_float, is_int
7+
from geophires_x.Parameter import SCHEDULE_DSL_MULTIPLIER_SYMBOL
8+
9+
10+
_log = logging.getLogger(__name__)
11+
12+
13+
def expand_schedule_dsl(
14+
schedule_strings: list[str | float], total_years: int, allow_schedule_length_to_exceed_total_years: bool = False
15+
) -> list[float]:
16+
"""
17+
Parse a duration-based scheduling DSL and expand it into a fixed-length time-series array.
18+
19+
Syntax: `[Value] * [Years], [Value] * [Years], ..., [Terminal Value]`
20+
21+
The terminal (last) value is repeated to fill `total_years`. A bare scalar
22+
(e.g. `['2.5']`) is treated as a terminal value and broadcast across all years.
23+
24+
Examples::
25+
26+
expand_schedule_dsl(['1.0 * 3', '0.1'], total_years=6)
27+
# => [1.0, 1.0, 1.0, 0.1, 0.1, 0.1]
28+
29+
expand_schedule_dsl(['2.5'], total_years=4)
30+
# => [2.5, 2.5, 2.5, 2.5]
31+
32+
:param schedule_strings: list of DSL segment strings. Each element is either
33+
`"<value> * <years>"` (a run-length segment) or `"<value>"` (a scalar,
34+
which becomes the terminal value when it is the last element, or a 1-year
35+
segment otherwise).
36+
:param total_years: The total number of years the expanded array must span
37+
(typically `construction_years + plant_lifetime`).
38+
:returns: A `list[float]` of length `total_years`.
39+
:raises ValueError: On malformed DSL strings or when explicit segments exceed
40+
`total_years`.
41+
"""
42+
43+
if total_years <= 0:
44+
return []
45+
46+
if not schedule_strings:
47+
return [0.0] * total_years
48+
49+
segments: list[tuple[float, int | None]] = []
50+
for raw in schedule_strings:
51+
raw = str(raw).strip()
52+
if SCHEDULE_DSL_MULTIPLIER_SYMBOL in raw:
53+
parts = raw.split(SCHEDULE_DSL_MULTIPLIER_SYMBOL)
54+
if len(parts) != 2:
55+
raise ValueError(f'Invalid schedule segment "{raw}": expected "<value> * <years>".')
56+
57+
val_raw = parts[0].strip()
58+
if not is_float(val_raw):
59+
raise ValueError(f'Invalid schedule segment "{raw}": "{val_raw}" is not a float.')
60+
value = float(val_raw)
61+
if value < 0:
62+
raise ValueError(f'Invalid schedule segment "{raw}": {val_raw} is negative.')
63+
64+
years_raw = parts[1].strip()
65+
if not is_int(years_raw):
66+
raise ValueError(f'Invalid schedule segment "{raw}": "{years_raw}" is not an int.')
67+
68+
years = int(years_raw)
69+
if years < 0:
70+
raise ValueError(f'Invalid schedule segment "{raw}": year count must be non-negative.')
71+
segments.append((value, years))
72+
else:
73+
if not is_float(raw):
74+
raise ValueError(f'Invalid schedule segment "{raw}": "{raw}" is not a float.')
75+
76+
value = float(raw)
77+
segments.append((value, None))
78+
79+
result: list[float] = []
80+
terminal_value = 0.0
81+
82+
for idx, (value, years) in enumerate(segments):
83+
is_last = idx == len(segments) - 1
84+
if years is not None:
85+
result.extend([value] * years)
86+
terminal_value = value
87+
else:
88+
if is_last:
89+
terminal_value = value
90+
else:
91+
result.append(value)
92+
terminal_value = value
93+
94+
remaining = total_years - len(result)
95+
if remaining > 0:
96+
result.extend([terminal_value] * remaining)
97+
98+
if len(result) > total_years:
99+
if not allow_schedule_length_to_exceed_total_years:
100+
raise ValueError(
101+
f'Invalid schedule: Schedule expands to {len(result)} years '
102+
f'which exceeds total_years={total_years}.'
103+
)
104+
else:
105+
pre_truncation_result = copy.copy(result)
106+
result = result[:total_years]
107+
_log.warning(
108+
f'Schedule expands to {len(pre_truncation_result)} years, which exceeds total_years={total_years}. '
109+
f'Schedule has been truncated to {total_years} years ({result}; from {pre_truncation_result}).'
110+
)
111+
112+
return result

0 commit comments

Comments
 (0)