Skip to content

Commit da41556

Browse files
Merge branch 'client-result-multicategory-fields-fix' into sam-em-add-ons
2 parents 6e56734 + 762564b commit da41556

File tree

7 files changed

+105
-22
lines changed

7 files changed

+105
-22
lines changed

src/geophires_x_client/geophires_x_result.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,9 @@ def __init__(self, output_file_path, logger_name=None):
381381
self._logger = _get_logger(logger_name)
382382
self.output_file_path = output_file_path
383383

384-
f = open(self.output_file_path)
385-
self._lines = list(f.readlines())
386-
f.close()
384+
with open(self.output_file_path, encoding='utf-8') as f:
385+
self._lines = list(f.readlines())
386+
self._lines_by_category = self._get_lines_by_category()
387387

388388
# TODO generic-er result value map
389389

@@ -393,19 +393,26 @@ def __init__(self, output_file_path, logger_name=None):
393393
fields = category_fields[1]
394394

395395
self.result[category] = {}
396+
category_lines = self._lines_by_category.get(category, [])
397+
396398
for field in fields:
397399
if isinstance(field, _EqualSignDelimitedField):
398-
self.result[category][field.field_name] = self._get_equal_sign_delimited_field(field.field_name)
400+
self.result[category][field.field_name] = self._get_equal_sign_delimited_field(
401+
field.field_name, search_lines=category_lines
402+
)
399403
elif isinstance(field, _UnlabeledStringField):
400404
self.result[category][field.field_name] = self._get_unlabeled_string_field(
401-
field.field_name, field.marker_prefixes
405+
field.field_name, field.marker_prefixes, search_lines=category_lines
402406
)
403407
else:
404408
is_string_field = isinstance(field, _StringValueField)
405409
field_name = field.field_name if is_string_field else field
406410
indent = 4 if category != 'Simulation Metadata' else 1
407411
self.result[category][field_name] = self._get_result_field(
408-
field_name, is_string_value_field=is_string_field, min_indentation_spaces=indent
412+
field_name,
413+
is_string_value_field=is_string_field,
414+
min_indentation_spaces=indent,
415+
search_lines=category_lines,
409416
)
410417

411418
try:
@@ -446,6 +453,35 @@ def __init__(self, output_file_path, logger_name=None):
446453
if self._get_end_use_option() is not None:
447454
self.result['metadata']['End-Use Option'] = self._get_end_use_option().name
448455

456+
def _get_lines_by_category(self) -> dict[str, list[str]]:
457+
"""
458+
Parses the raw output file lines into a dictionary where keys are
459+
category headers and values are the lines belonging to that category.
460+
"""
461+
lines_by_category = {}
462+
current_category = None
463+
known_headers = list(self._RESULT_FIELDS_BY_CATEGORY.keys())
464+
465+
for line in self._lines:
466+
467+
def get_header_content(h_: str) -> str:
468+
if h_ == 'Simulation Metadata':
469+
return h_
470+
return f'***{h_}***'
471+
472+
# Check if the line is a category header
473+
found_header = next((h for h in known_headers if get_header_content(h) == line.strip()), None)
474+
475+
if found_header:
476+
current_category = found_header
477+
if current_category not in lines_by_category:
478+
lines_by_category[current_category] = []
479+
elif current_category:
480+
# Append the line to the current category if one has been found
481+
lines_by_category[current_category].append(line)
482+
483+
return lines_by_category
484+
449485
@property
450486
def direct_use_heat_breakeven_price_USD_per_MMBTU(self):
451487
summary = self.result['SUMMARY OF RESULTS']
@@ -552,9 +588,18 @@ def _json_fields(self) -> MappingProxyType:
552588
except FileNotFoundError:
553589
return {}
554590

555-
def _get_result_field(self, field_name: str, is_string_value_field: bool = False, min_indentation_spaces: int = 4):
591+
def _get_result_field(
592+
self,
593+
field_name: str,
594+
is_string_value_field: bool = False,
595+
min_indentation_spaces: int = 4,
596+
search_lines: list[str] | None = None,
597+
):
598+
if search_lines is None:
599+
search_lines = self._lines
600+
556601
# TODO make this less fragile with proper regex
557-
matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, self._lines))
602+
matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, search_lines))
558603

559604
if len(matching_lines) == 0:
560605
self._logger.debug(f'Field not found: {field_name}')
@@ -592,14 +637,17 @@ def normalize_spaces(matched_line):
592637

593638
return {'value': self._parse_number(str_val, field=f'field "{field_name}"'), 'unit': unit}
594639

595-
def _get_equal_sign_delimited_field(self, field_name):
640+
def _get_equal_sign_delimited_field(self, field_name, search_lines: list[str] | None = None):
641+
if search_lines is None:
642+
search_lines = self._lines
643+
596644
metadata_markers = (
597645
f' {field_name} = ',
598646
# Previous versions of GEOPHIRES erroneously included an extra space after the field name so we include
599647
# the pattern for it for backwards compatibility with existing .out files.
600648
f' {field_name} = ',
601649
)
602-
matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), self._lines))
650+
matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), search_lines))
603651

604652
if len(matching_lines) == 0:
605653
self._logger.debug(f'Equal sign-delimited field not found: {field_name}')
@@ -619,8 +667,13 @@ def _get_equal_sign_delimited_field(self, field_name):
619667
self._logger.error(f'Unexpected error extracting equal sign-delimited field {field_name}') # Shouldn't happen
620668
return None
621669

622-
def _get_unlabeled_string_field(self, field_name: str, marker_prefixes: list[str]):
623-
matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), self._lines))
670+
def _get_unlabeled_string_field(
671+
self, field_name: str, marker_prefixes: list[str], search_lines: list[str] | None = None
672+
):
673+
if search_lines is None:
674+
search_lines = self._lines
675+
676+
matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), search_lines))
624677

625678
if len(matching_lines) == 0:
626679
self._logger.debug(f'Unlabeled string field not found: {field_name}')

tests/example1_addons.csv

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,18 @@ RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey
7171
RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.0,degC
7272
RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,217.9,kPa
7373
RESERVOIR SIMULATION RESULTS,Average Production Well Pump Pressure Drop,,1112.0,kPa
74-
RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,5.39,MW
7574
CAPITAL COSTS (M$),Drilling and completion costs,,17.38,MUSD
7675
CAPITAL COSTS (M$),Drilling and completion costs per well,,4.35,MUSD
7776
CAPITAL COSTS (M$),Stimulation costs,,3.02,MUSD
7877
CAPITAL COSTS (M$),Surface power plant costs,,20.8,MUSD
7978
CAPITAL COSTS (M$),Field gathering system costs,,2.3,MUSD
8079
CAPITAL COSTS (M$),Total surface equipment costs,,23.1,MUSD
8180
CAPITAL COSTS (M$),Exploration costs,,4.49,MUSD
82-
CAPITAL COSTS (M$),Total Add-on CAPEX,,70.0,MUSD
8381
CAPITAL COSTS (M$),Total capital costs,,25.67,MUSD
8482
CAPITAL COSTS (M$),Annualized capital costs,,1.28,MUSD
8583
OPERATING AND MAINTENANCE COSTS (M$/yr),Wellfield maintenance costs,,0.39,MUSD/yr
8684
OPERATING AND MAINTENANCE COSTS (M$/yr),Power plant maintenance costs,,0.9,MUSD/yr
8785
OPERATING AND MAINTENANCE COSTS (M$/yr),Water costs,,0.06,MUSD/yr
88-
OPERATING AND MAINTENANCE COSTS (M$/yr),Total Add-on OPEX,,1.7,MUSD/yr
8986
OPERATING AND MAINTENANCE COSTS (M$/yr),Total operating and maintenance costs,,-0.86,MUSD/yr
9087
SURFACE EQUIPMENT SIMULATION RESULTS,Initial geofluid availability,,0.11,MW/(kg/s)
9188
SURFACE EQUIPMENT SIMULATION RESULTS,Maximum Total Electricity Generation,,5.62,MW

tests/examples/example1_addons.out

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
Simulation Metadata
66
----------------------
77
GEOPHIRES Version: 3.9.44
8-
Simulation Date: 2025-07-28
9-
Simulation Time: 14:30
10-
Calculation Time: 0.858 sec
8+
Simulation Date: 2025-07-29
9+
Simulation Time: 06:46
10+
Calculation Time: 0.898 sec
1111

1212
***SUMMARY OF RESULTS***
1313

tests/geophires-result_example-3.csv

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,6 @@ RESERVOIR SIMULATION RESULTS,Average Reservoir Heat Extraction,,133.3,MW
5151
RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey Model,
5252
RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.6,degC
5353
RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,650.2,kPa
54-
RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,19.7,MW
5554
CAPITAL COSTS (M$),Drilling and completion costs,,34.45,MUSD
5655
CAPITAL COSTS (M$),Drilling and completion costs per well,,5.74,MUSD
5756
CAPITAL COSTS (M$),Stimulation costs,,4.53,MUSD

tests/geophires_x_client_tests/test_geophires_x_result.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ def test_get_sam_cash_flow_row_name_unit_split(self) -> None:
1616
actual = GeophiresXResult._get_sam_cash_flow_row_name_unit_split(case[0])
1717
self.assertListEqual(actual, case[1])
1818

19+
def test_get_lines_by_category(self) -> None:
20+
r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out'))
21+
lines_by_cat = r._get_lines_by_category()
22+
res_params_lines = lines_by_cat['RESERVOIR PARAMETERS']
23+
self.assertGreater(len(res_params_lines), 0)
24+
1925
def test_reservoir_volume_calculation_note(self) -> None:
2026
r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out'))
2127
field_name = 'Reservoir volume calculation note'
@@ -37,3 +43,17 @@ def test_sam_economic_model_result_csv(self) -> None:
3743
r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('sam-em-csv-test.out'))
3844
as_csv = r.as_csv()
3945
self.assertIsNotNone(as_csv)
46+
47+
def test_multicategory_fields_only_in_case_report_category(self) -> None:
48+
r: GeophiresXResult = GeophiresXResult(
49+
self._get_test_file_path('../examples/example_SAM-single-owner-PPA-3.out')
50+
)
51+
self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on CAPEX'])
52+
self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on OPEX'])
53+
54+
self.assertIn('Total Add-on CAPEX', r.result['CAPITAL COSTS (M$)'])
55+
self.assertIn('Total Add-on OPEX', r.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'])
56+
57+
self.assertIsNone(r.result['RESERVOIR SIMULATION RESULTS']['Average Net Electricity Production'])
58+
self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['Average Net Electricity Production'])
59+
self.assertIsNotNone(r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Average Net Electricity Generation'])

tests/regenerate-example-result.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ rm examples/$1.json
1616
if [[ $1 == "example1_addons" ]]
1717
then
1818
echo "Updating CSV..."
19-
python regenerate_example_result_csv.py
19+
python regenerate_example_result_csv.py example1_addons
2020
fi

tests/regenerate_example_result_csv.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import argparse
12
import os
23

34
from geophires_x_client import GeophiresXResult
@@ -8,5 +9,18 @@ def _get_file_path(file_name: str) -> str:
89

910

1011
if __name__ == '__main__':
11-
with open(_get_file_path('example1_addons.csv'), 'w', encoding='utf-8') as csvfile:
12-
csvfile.write(GeophiresXResult(_get_file_path('examples/example1_addons.out')).as_csv())
12+
parser = argparse.ArgumentParser(description='Regenerate a CSV result file from a GEOPHIRES-X example .out file.')
13+
parser.add_argument(
14+
'example_name',
15+
type=str,
16+
nargs='?', # Makes the argument optional
17+
default='example1_addons',
18+
help='The base name of the example file (e.g., "example1_addons"). Defaults to "example1_addons".',
19+
)
20+
args = parser.parse_args()
21+
22+
example_name = args.example_name
23+
example_relative_path = f'{"examples/" if example_name.startswith("example") else ""}{example_name}.out'
24+
25+
with open(_get_file_path(f'{example_name}.csv'), 'w', encoding='utf-8') as csvfile:
26+
csvfile.write(GeophiresXResult(_get_file_path(example_relative_path)).as_csv())

0 commit comments

Comments
 (0)