Skip to content

Client result multicategory fields fix #92

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 65 additions & 12 deletions src/geophires_x_client/geophires_x_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,9 @@ def __init__(self, output_file_path, logger_name=None):
self._logger = _get_logger(logger_name)
self.output_file_path = output_file_path

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

# TODO generic-er result value map

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

self.result[category] = {}
category_lines = self._lines_by_category.get(category, [])

for field in fields:
if isinstance(field, _EqualSignDelimitedField):
self.result[category][field.field_name] = self._get_equal_sign_delimited_field(field.field_name)
self.result[category][field.field_name] = self._get_equal_sign_delimited_field(
field.field_name, search_lines=category_lines
)
elif isinstance(field, _UnlabeledStringField):
self.result[category][field.field_name] = self._get_unlabeled_string_field(
field.field_name, field.marker_prefixes
field.field_name, field.marker_prefixes, search_lines=category_lines
)
else:
is_string_field = isinstance(field, _StringValueField)
field_name = field.field_name if is_string_field else field
indent = 4 if category != 'Simulation Metadata' else 1
self.result[category][field_name] = self._get_result_field(
field_name, is_string_value_field=is_string_field, min_indentation_spaces=indent
field_name,
is_string_value_field=is_string_field,
min_indentation_spaces=indent,
search_lines=category_lines,
)

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

def _get_lines_by_category(self) -> dict[str, list[str]]:
"""
Parses the raw output file lines into a dictionary where keys are
category headers and values are the lines belonging to that category.
"""
lines_by_category = {}
current_category = None
known_headers = list(self._RESULT_FIELDS_BY_CATEGORY.keys())

for line in self._lines:

def get_header_content(h_: str) -> str:
if h_ == 'Simulation Metadata':
return h_
return f'***{h_}***'

# Check if the line is a category header
found_header = next((h for h in known_headers if get_header_content(h) == line.strip()), None)

if found_header:
current_category = found_header
if current_category not in lines_by_category:
lines_by_category[current_category] = []
elif current_category:
# Append the line to the current category if one has been found
lines_by_category[current_category].append(line)

return lines_by_category

@property
def direct_use_heat_breakeven_price_USD_per_MMBTU(self):
summary = self.result['SUMMARY OF RESULTS']
Expand Down Expand Up @@ -552,9 +588,18 @@ def _json_fields(self) -> MappingProxyType:
except FileNotFoundError:
return {}

def _get_result_field(self, field_name: str, is_string_value_field: bool = False, min_indentation_spaces: int = 4):
def _get_result_field(
self,
field_name: str,
is_string_value_field: bool = False,
min_indentation_spaces: int = 4,
search_lines: list[str] | None = None,
):
if search_lines is None:
search_lines = self._lines

# TODO make this less fragile with proper regex
matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, self._lines))
matching_lines = set(filter(lambda line: f'{min_indentation_spaces * " "}{field_name}: ' in line, search_lines))

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

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

def _get_equal_sign_delimited_field(self, field_name):
def _get_equal_sign_delimited_field(self, field_name, search_lines: list[str] | None = None):
if search_lines is None:
search_lines = self._lines

metadata_markers = (
f' {field_name} = ',
# Previous versions of GEOPHIRES erroneously included an extra space after the field name so we include
# the pattern for it for backwards compatibility with existing .out files.
f' {field_name} = ',
)
matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), self._lines))
matching_lines = set(filter(lambda line: any(m in line for m in metadata_markers), search_lines))

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

def _get_unlabeled_string_field(self, field_name: str, marker_prefixes: list[str]):
matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), self._lines))
def _get_unlabeled_string_field(
self, field_name: str, marker_prefixes: list[str], search_lines: list[str] | None = None
):
if search_lines is None:
search_lines = self._lines

matching_lines = set(filter(lambda line: any(m in line for m in marker_prefixes), search_lines))

if len(matching_lines) == 0:
self._logger.debug(f'Unlabeled string field not found: {field_name}')
Expand Down
3 changes: 0 additions & 3 deletions tests/example1_addons.csv
Original file line number Diff line number Diff line change
Expand Up @@ -71,21 +71,18 @@ RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey
RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.0,degC
RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,217.9,kPa
RESERVOIR SIMULATION RESULTS,Average Production Well Pump Pressure Drop,,1112.0,kPa
RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,5.39,MW
CAPITAL COSTS (M$),Drilling and completion costs,,17.38,MUSD
CAPITAL COSTS (M$),Drilling and completion costs per well,,4.35,MUSD
CAPITAL COSTS (M$),Stimulation costs,,3.02,MUSD
CAPITAL COSTS (M$),Surface power plant costs,,20.8,MUSD
CAPITAL COSTS (M$),Field gathering system costs,,2.3,MUSD
CAPITAL COSTS (M$),Total surface equipment costs,,23.1,MUSD
CAPITAL COSTS (M$),Exploration costs,,4.49,MUSD
CAPITAL COSTS (M$),Total Add-on CAPEX,,70.0,MUSD
CAPITAL COSTS (M$),Total capital costs,,25.67,MUSD
CAPITAL COSTS (M$),Annualized capital costs,,1.28,MUSD
OPERATING AND MAINTENANCE COSTS (M$/yr),Wellfield maintenance costs,,0.39,MUSD/yr
OPERATING AND MAINTENANCE COSTS (M$/yr),Power plant maintenance costs,,0.9,MUSD/yr
OPERATING AND MAINTENANCE COSTS (M$/yr),Water costs,,0.06,MUSD/yr
OPERATING AND MAINTENANCE COSTS (M$/yr),Total Add-on OPEX,,1.7,MUSD/yr
OPERATING AND MAINTENANCE COSTS (M$/yr),Total operating and maintenance costs,,-0.86,MUSD/yr
SURFACE EQUIPMENT SIMULATION RESULTS,Initial geofluid availability,,0.11,MW/(kg/s)
SURFACE EQUIPMENT SIMULATION RESULTS,Maximum Total Electricity Generation,,5.62,MW
Expand Down
6 changes: 3 additions & 3 deletions tests/examples/example1_addons.out
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
Simulation Metadata
----------------------
GEOPHIRES Version: 3.9.44
Simulation Date: 2025-07-28
Simulation Time: 14:30
Calculation Time: 0.858 sec
Simulation Date: 2025-07-29
Simulation Time: 06:46
Calculation Time: 0.898 sec

***SUMMARY OF RESULTS***

Expand Down
1 change: 0 additions & 1 deletion tests/geophires-result_example-3.csv
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ RESERVOIR SIMULATION RESULTS,Average Reservoir Heat Extraction,,133.3,MW
RESERVOIR SIMULATION RESULTS,Production Wellbore Heat Transmission Model,,Ramey Model,
RESERVOIR SIMULATION RESULTS,Average Production Well Temperature Drop,,3.6,degC
RESERVOIR SIMULATION RESULTS,Average Injection Well Pump Pressure Drop,,650.2,kPa
RESERVOIR SIMULATION RESULTS,Average Net Electricity Production,,19.7,MW
CAPITAL COSTS (M$),Drilling and completion costs,,34.45,MUSD
CAPITAL COSTS (M$),Drilling and completion costs per well,,5.74,MUSD
CAPITAL COSTS (M$),Stimulation costs,,4.53,MUSD
Expand Down
20 changes: 20 additions & 0 deletions tests/geophires_x_client_tests/test_geophires_x_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ def test_get_sam_cash_flow_row_name_unit_split(self) -> None:
actual = GeophiresXResult._get_sam_cash_flow_row_name_unit_split(case[0])
self.assertListEqual(actual, case[1])

def test_get_lines_by_category(self) -> None:
r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out'))
lines_by_cat = r._get_lines_by_category()
res_params_lines = lines_by_cat['RESERVOIR PARAMETERS']
self.assertGreater(len(res_params_lines), 0)

def test_reservoir_volume_calculation_note(self) -> None:
r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('../examples/example2.out'))
field_name = 'Reservoir volume calculation note'
Expand All @@ -37,3 +43,17 @@ def test_sam_economic_model_result_csv(self) -> None:
r: GeophiresXResult = GeophiresXResult(self._get_test_file_path('sam-em-csv-test.out'))
as_csv = r.as_csv()
self.assertIsNotNone(as_csv)

def test_multicategory_fields_only_in_case_report_category(self) -> None:
r: GeophiresXResult = GeophiresXResult(
self._get_test_file_path('../examples/example_SAM-single-owner-PPA-3.out')
)
self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on CAPEX'])
self.assertIsNone(r.result['EXTENDED ECONOMICS']['Total Add-on OPEX'])

self.assertIn('Total Add-on CAPEX', r.result['CAPITAL COSTS (M$)'])
self.assertIn('Total Add-on OPEX', r.result['OPERATING AND MAINTENANCE COSTS (M$/yr)'])

self.assertIsNone(r.result['RESERVOIR SIMULATION RESULTS']['Average Net Electricity Production'])
self.assertIsNotNone(r.result['SUMMARY OF RESULTS']['Average Net Electricity Production'])
self.assertIsNotNone(r.result['SURFACE EQUIPMENT SIMULATION RESULTS']['Average Net Electricity Generation'])
2 changes: 1 addition & 1 deletion tests/regenerate-example-result.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ rm examples/$1.json
if [[ $1 == "example1_addons" ]]
then
echo "Updating CSV..."
python regenerate_example_result_csv.py
python regenerate_example_result_csv.py example1_addons
fi
18 changes: 16 additions & 2 deletions tests/regenerate_example_result_csv.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import argparse
import os

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


if __name__ == '__main__':
with open(_get_file_path('example1_addons.csv'), 'w', encoding='utf-8') as csvfile:
csvfile.write(GeophiresXResult(_get_file_path('examples/example1_addons.out')).as_csv())
parser = argparse.ArgumentParser(description='Regenerate a CSV result file from a GEOPHIRES-X example .out file.')
parser.add_argument(
'example_name',
type=str,
nargs='?', # Makes the argument optional
default='example1_addons',
help='The base name of the example file (e.g., "example1_addons"). Defaults to "example1_addons".',
)
args = parser.parse_args()

example_name = args.example_name
example_relative_path = f'{"examples/" if example_name.startswith("example") else ""}{example_name}.out'

with open(_get_file_path(f'{example_name}.csv'), 'w', encoding='utf-8') as csvfile:
csvfile.write(GeophiresXResult(_get_file_path(example_relative_path)).as_csv())
Loading