diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 9e0bfe76..a018e799 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 3.3.4 +current_version = 3.4.0 commit = True tag = True diff --git a/.cookiecutterrc b/.cookiecutterrc index 4c1b0a92..c7a22552 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.3.4 + version: 3.4.0 version_manager: "bump2version" website: "https://github.com/NREL" year_from: "2023" diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 05c28f4a..b8d0839d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,13 @@ Changelog GEOPHIRES-X (2023-2024) ------------------------ +3.4 +^^^ +Monte Carlo moved to dedicated module + +`diff `__ + + 3.3 ^^^ diff --git a/README.rst b/README.rst index 9cbd4f0f..26195fa8 100644 --- a/README.rst +++ b/README.rst @@ -47,9 +47,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/NREL/GEOPHIRES-X/v3.3.4.svg +.. |commits-since| image:: https://img.shields.io/github/commits-since/NREL/GEOPHIRES-X/v3.4.0.svg :alt: Commits since latest release - :target: https://github.com/NREL/GEOPHIRES-X/compare/v3.3.4...main + :target: https://github.com/NREL/GEOPHIRES-X/compare/v3.4.0...main .. |docs| image:: https://readthedocs.org/projects/GEOPHIRES-X/badge/?style=flat :target: https://nrel.github.io/GEOPHIRES-X @@ -281,6 +281,11 @@ Extending GEOPHIRES-X - `Extension example: SUTRA `__ +Monte Carlo +----------- + +`Monte Carlo User Guide `__ + Other Documentation: -------------------- The `GEOPHIRES v2.0 (previous version's) user manual `__ describes GEOPHIRES's high-level software architecture. diff --git a/docs/Monte-Carlo-User-Guide.md b/docs/Monte-Carlo-User-Guide.md new file mode 100644 index 00000000..4c198dba --- /dev/null +++ b/docs/Monte-Carlo-User-Guide.md @@ -0,0 +1,114 @@ +# GEOPHIRES Monte Carlo User Guide + +## Example Setup + +Create a project with the following structure, including GEOPHIRES in `requirements.txt` and setting up `venv` with `virtualenv`: + +``` +├── GEOPHIRES-example1.txt +├── MC_GEOPHIRES_Settings_file.txt +├── main.py +├── requirements.txt +└── venv/ +``` + +In `main.py`: + +```python +from pathlib import Path + +from geophires_monte_carlo import GeophiresMonteCarloClient, MonteCarloResult, MonteCarloRequest, SimulationProgram + + +def monte_carlo(): + client = GeophiresMonteCarloClient() + + result: MonteCarloResult = client.get_monte_carlo_result( + MonteCarloRequest( + SimulationProgram.GEOPHIRES, + + # Files from tests/geophires_monte_carlo_tests - copy these into the same directory as main.py + Path('GEOPHIRES-example1.txt').absolute(), + Path('MC_GEOPHIRES_Settings_file.txt').absolute(), + output_file=Path('MC_GEOPHIRES_Result.txt').absolute() + ) + ) + + with open(result.output_file_path, 'r') as result_output_file: + result_display = result_output_file.read() + print(f'MC result:\n{result_display}') + +if __name__ == '__main__': + monte_carlo() +``` + +To run: +``` +(venv) python main.py + +[...] + +[2024-02-09 07:47:18][INFO] Complete geophires_monte_carlo.MC_GeoPHIRES3: main +MC result: +Electricity breakeven price, Project NPV, Gradient 1, Reservoir Temperature, Utilization Factor, Ambient Temperature +38.81, -42.59, (Gradient 1:30.09736131122952;Reservoir Temperature:320.2888549098197;Utilization Factor:0.9295528406892491;Ambient Temperature:20.684620766378806;) +17.81, -42.76, (Gradient 1:39.47722802709689;Reservoir Temperature:306.71578141214087;Utilization Factor:0.7604874092668568;Ambient Temperature:20.39891267899405;) +9.91, -41.95, (Gradient 1:50.24142993501238;Reservoir Temperature:311.3876336705825;Utilization Factor:0.8657162766204807;Ambient Temperature:20.205604589516913;) +61.37, -43.34, (Gradient 1:28.230745766883796;Reservoir Temperature:324.25115143107104;Utilization Factor:0.8308351836890867;Ambient Temperature:20.615118153663598;) +8.76, -41.76, (Gradient 1:54.66070153603035;Reservoir Temperature:319.6097066730564;Utilization Factor:0.855785650134492;Ambient Temperature:19.359218133245772;) +7.58, -37.63, (Gradient 1:57.53774721757885;Reservoir Temperature:318.6354560118773;Utilization Factor:0.9305717468323405;Ambient Temperature:20.011047903204176;) +9.35, -41.03, (Gradient 1:51.20593175130416;Reservoir Temperature:311.40737612727423;Utilization Factor:0.8876035819161642;Ambient Temperature:19.968497775278948;) +18.11, -42.68, (Gradient 1:38.66016637029756;Reservoir Temperature:310.20708430352124;Utilization Factor:0.7640202889998118;Ambient Temperature:18.805190553693578;) +10.07, -40.41, (Gradient 1:47.876595779164845;Reservoir Temperature:310.4061695000305;Utilization Factor:0.9228147348375185;Ambient Temperature:20.119411582814514;) +23.08, -41.38, (Gradient 1:33.342513206728185;Reservoir Temperature:305.48937077249826;Utilization Factor:0.9361923986407424;Ambient Temperature:18.423715643246567;) +Electricity breakeven price: + minimum: 7.58 + maximum: 61.37 + median: 13.94 + average: 20.48 + mean: 20.48 + standard deviation: 16.36 +bin values (as percentage): [0.09295408 0.18590816 0.18590816 0. 0. 0. + 0. 0. 0. 0.18590816 0. 0. + 0. 0. 0.09295408 0. 0. 0. + 0. 0. 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0.09295408 + 0. 0. 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0. + 0. 0.09295408] +bin edges: [ 7.58 8.6558 9.7316 10.8074 11.8832 12.959 14.0348 15.1106 16.1864 + 17.2622 18.338 19.4138 20.4896 21.5654 22.6412 23.717 24.7928 25.8686 + 26.9444 28.0202 29.096 30.1718 31.2476 32.3234 33.3992 34.475 35.5508 + 36.6266 37.7024 38.7782 39.854 40.9298 42.0056 43.0814 44.1572 45.233 + 46.3088 47.3846 48.4604 49.5362 50.612 51.6878 52.7636 53.8394 54.9152 + 55.991 57.0668 58.1426 59.2184 60.2942 61.37 ] +Project NPV: + minimum: -43.34 + maximum: -37.63 + median: -41.86 + average: -41.55 + mean: -41.55 + standard deviation: 1.56 +bin values (as percentage): [0.87565674 0. 0. 0. 0. 1.75131349 + 0.87565674 0. 0. 0. 0. 0. + 0.87565674 0.87565674 0. 0. 0. 0.87565674 + 0. 0. 0.87565674 0. 0. 0. + 0. 0.87565674 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0. + 0. 0. 0. 0. 0. 0. + 0. 0.87565674] +bin edges: [-43.34 -43.2258 -43.1116 -42.9974 -42.8832 -42.769 -42.6548 -42.5406 + -42.4264 -42.3122 -42.198 -42.0838 -41.9696 -41.8554 -41.7412 -41.627 + -41.5128 -41.3986 -41.2844 -41.1702 -41.056 -40.9418 -40.8276 -40.7134 + -40.5992 -40.485 -40.3708 -40.2566 -40.1424 -40.0282 -39.914 -39.7998 + -39.6856 -39.5714 -39.4572 -39.343 -39.2288 -39.1146 -39.0004 -38.8862 + -38.772 -38.6578 -38.5436 -38.4294 -38.3152 -38.201 -38.0868 -37.9726 + -37.8584 -37.7442 -37.63 ] + +``` + +## Documentation + +See [module documentation](reference/geophires_monte_carlo.html) diff --git a/docs/conf.py b/docs/conf.py index a1c91085..5f91586c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,7 +18,7 @@ year = '2023' author = 'NREL' copyright = f'{year}, {author}' -version = release = '3.3.4' +version = release = '3.4.0' pygments_style = 'trac' templates_path = ['./templates'] diff --git a/docs/index.rst b/docs/index.rst index 3982bbaa..157002af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Contents overview parameters How-to-extend-GEOPHIRES-X + Monte-Carlo-User-Guide .. reference/index Indices and tables diff --git a/docs/reference/geophires_monte_carlo.rst b/docs/reference/geophires_monte_carlo.rst new file mode 100644 index 00000000..65ba9ce2 --- /dev/null +++ b/docs/reference/geophires_monte_carlo.rst @@ -0,0 +1,12 @@ +geophires_monte_carlo +===================== + +.. testsetup:: + + from geophires_monte_carlo import * + +.. automodule:: geophires_monte_carlo + :members: + +.. automodule:: geophires_monte_carlo.MC_GeoPHIRES3 + :members: diff --git a/setup.py b/setup.py index 72501a59..a75c03bf 100755 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ def read(*names, **kwargs): setup( name='geophires-x', - version='3.3.4', + version='3.4.0', license='MIT', description='GEOPHIRES is a free and open-source geothermal techno-economic simulator.', long_description='{}\n{}'.format( diff --git a/src/geophires_monte_carlo/.gitignore b/src/geophires_monte_carlo/.gitignore new file mode 100644 index 00000000..60bc8da3 --- /dev/null +++ b/src/geophires_monte_carlo/.gitignore @@ -0,0 +1,3 @@ +/MC_GEOPHIRES_Result.txt +/MC_HIP_Result.txt +*.png diff --git a/src/geophires_monte_carlo/Examples/MC_GEOPHIRES_Settings_file.txt b/src/geophires_monte_carlo/Examples/MC_GEOPHIRES_Settings_file.txt new file mode 100644 index 00000000..ebdc018b --- /dev/null +++ b/src/geophires_monte_carlo/Examples/MC_GEOPHIRES_Settings_file.txt @@ -0,0 +1,10 @@ +INPUT, Gradient 1, uniform, 25, 60 +INPUT, Reservoir Temperature, normal, 250, 10 +INPUT, Utilization Factor,uniform, 0.7, 0.95 +INPUT, Ambient Temperature,triangular, 15, 20, 25 +OUTPUT, Average Net Electricity Production +OUTPUT, Average Production Temperature +OUTPUT, Average Annual Total Electricity Generation +ITERATIONS, 250 +MC_OUTPUT_FILE, MC_GEOPHIRES_Result.txt +PYTHON_PATH, ..\..\venv\Scripts\python.exe diff --git a/src/geophires_x/MC_Settings_file.txt b/src/geophires_monte_carlo/Examples/MC_GEOPHIRES_Settings_file_2.txt similarity index 85% rename from src/geophires_x/MC_Settings_file.txt rename to src/geophires_monte_carlo/Examples/MC_GEOPHIRES_Settings_file_2.txt index 05c28be6..c6714109 100644 --- a/src/geophires_x/MC_Settings_file.txt +++ b/src/geophires_monte_carlo/Examples/MC_GEOPHIRES_Settings_file_2.txt @@ -4,4 +4,4 @@ INPUT, Ambient Temperature,triangular, 15, #, 25 OUTPUT, Average Net Electricity Production OUTPUT, Average Annual Total Electricity Generation ITERATIONS, 10 -MC_OUTPUT_FILE, MC_Result.txt \ No newline at end of file +MC_OUTPUT_FILE, MC_GEOPHIRES_Result_2.txt diff --git a/src/geophires_monte_carlo/Examples/MC_HIP_Settings_file.txt b/src/geophires_monte_carlo/Examples/MC_HIP_Settings_file.txt new file mode 100644 index 00000000..3a6baae6 --- /dev/null +++ b/src/geophires_monte_carlo/Examples/MC_HIP_Settings_file.txt @@ -0,0 +1,12 @@ +INPUT, Formation Porosity, uniform, 9.0, 28.0 +INPUT, Reservoir Area, uniform, 50.0, 120.0 +INPUT, Reservoir Thickness, uniform, 0.122, 0.299 +INPUT, Reservoir Temperature, uniform, 130, 170 +INPUT, Rejection Temperature, uniform, 20, 33 +OUTPUT, Available Heat (fluid) +OUTPUT, Producible Heat (fluid) +OUTPUT, Producible Heat/Unit Area (fluid) +OUTPUT, Producible Electricity (fluid) +OUTPUT, Producible Electricity/Unit Area (fluid) +ITERATIONS, 250 +MC_OUTPUT_FILE, MC_HIP_Result.txt diff --git a/src/geophires_monte_carlo/MC_GeoPHIRES3.py b/src/geophires_monte_carlo/MC_GeoPHIRES3.py new file mode 100755 index 00000000..d4f63603 --- /dev/null +++ b/src/geophires_monte_carlo/MC_GeoPHIRES3.py @@ -0,0 +1,411 @@ +#! python +""" +Framework for running Monte Carlo simulations using GEOPHIRES v3.0 & HIP-RA 1.0 +build date: September 2023 +Created on Wed November 16 10:43:04 2017 +@author: Malcolm Ross V3 +@author: softwareengineerprogrammer +""" + +import argparse +import concurrent.futures +import os +import shutil +import subprocess +import sys +import tempfile +import time +import uuid +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd + +from geophires_monte_carlo.common import _get_logger +from geophires_x_client import GeophiresInputParameters +from geophires_x_client import GeophiresXClient +from geophires_x_client import GeophiresXResult +from hip_ra import HipRaClient +from hip_ra import HipRaInputParameters +from hip_ra import HipRaResult +from hip_ra_x import HipRaXClient + + +def check_and_replace_mean(input_value, args) -> list: + """ + CheckAndReplaceMean - check to see if the user has requested that a value be replaced by a mean value by specifying + a value as "#" + :param input_value: the value to check + :type input_value: list + :param args: the list of arguments passed in from the command line + :type args: list + :return: the input_value, with the mean value replaced if necessary + :rtype: list + """ + + i = 0 + for input_x in input_value: + if '#' in input_x: + # found one we have to process. + vari_name = input_value[0] + # find it in the Input_file + with open(args.Input_file) as f: + ss = f.readlines() + for s in ss: + if str(s).startswith(vari_name): + s2 = s.split(',') + input_value[i] = s2[1] + break + break + i += 1 + + return input_value + + +def work_package(pass_list: list): + """ + Function that is called by the executor. It does the work of running the simulation. + :param pass_list: the list of arguments passed in from the command line + """ + + log = _get_logger() + + print('#', end='') # TODO Use tdqm library to show progress bar on screen: https://github.com/tqdm/tqdm + + input_values: list[list] = pass_list[0] + outputs: list = pass_list[1] + args: argparse.Namespace = pass_list[2] + output_file: str = pass_list[3] + working_dir: str = pass_list[4] # noqa: F841 + python_path: str = pass_list[5] + + input_file_entries = '' + + for input_value in input_values: + # get random values for each of the INPUTS based on the distributions and boundary values + if input_value[1].strip().startswith('normal'): + rando = np.random.normal(float(input_value[2]), float(input_value[3])) + input_file_entries += input_value[0] + ', ' + str(rando) + '\n' + elif input_value[1].strip().startswith('uniform'): + rando = np.random.uniform(float(input_value[2]), float(input_value[3])) + input_file_entries += input_value[0] + ', ' + str(rando) + '\n' + elif input_value[1].strip().startswith('triangular'): + rando = np.random.triangular(float(input_value[2]), float(input_value[3]), float(input_value[4])) + input_file_entries += input_value[0] + ', ' + str(rando) + '\n' + if input_value[1].strip().startswith('lognormal'): + rando = np.random.lognormal(float(input_value[2]), float(input_value[3])) + input_file_entries += input_value[0] + ', ' + str(rando) + '\n' + if input_value[1].strip().startswith('binomial'): + rando = np.random.binomial(int(input_value[2]), float(input_value[3])) + input_file_entries += input_value[0] + ', ' + str(rando) + '\n' + + # make up a temporary file name that will be shared among files for this iteration + tmp_input_file: str = str(Path(tempfile.gettempdir(), f'{uuid.uuid4()!s}.txt')) + tmp_output_file: str = tmp_input_file.replace('.txt', '_result.txt') + + # copy the contents of the Input_file into a new input file + shutil.copyfile(args.Input_file, tmp_input_file) + + # append those values to the new input file in the format "variable name, new_random_value". + # This will cause GeoPHIRES/HIP-RA to replace the value in the file with this random value in the calculation + # if it exists in that file already, or it will set it to the value as if it was a new value set by the user. + with open(tmp_input_file, 'a') as f: + f.write(input_file_entries) + + if args.Code_File.endswith('GEOPHIRESv3.py'): + # FIXME verify client manipulation of sys.argv is threadsafe + geophires_client: GeophiresXClient = GeophiresXClient() + result: GeophiresXResult = geophires_client.get_geophires_result( + GeophiresInputParameters(from_file_path=Path(tmp_input_file)) + ) + shutil.copyfile(result.output_file_path, tmp_output_file) + elif args.Code_File.endswith('HIP_RA.py'): + # FIXME verify client manipulation of sys.argv is threadsafe + hip_ra_client: HipRaClient = HipRaClient() + result: HipRaResult = hip_ra_client.get_hip_ra_result(HipRaInputParameters(from_file_path=Path(tmp_input_file))) + shutil.copyfile(result.output_file_path, tmp_output_file) + elif args.Code_File.endswith('hip_ra_x.py'): + # FIXME verify client manipulation of sys.argv is threadsafe + hip_ra_x_client: HipRaXClient = HipRaXClient() + result: HipRaResult = hip_ra_x_client.get_hip_ra_result( + HipRaInputParameters(from_file_path=Path(tmp_input_file)) + ) + shutil.copyfile(result.output_file_path, tmp_output_file) + else: + log.warning( + f'Code file from args ({args.Code_File}) is not a known program, ' + f'using subprocess instead of dedicated client...' + ) + + # start the passed in program name (usually GEOPHIRES or HIP-RA) with the supplied input file. + # Capture the output into a filename that is the same as the input file but has the suffix "_result.txt". + # ruff: noqa: S603 + # FIXME re-enable QA and address + sprocess = subprocess.Popen( + [python_path, args.Code_File, tmp_input_file, tmp_output_file], stdout=subprocess.DEVNULL + ) + sprocess.wait() + + # look group "_result.txt" file for the OUTPUT variables that the user asked for. + # For each of them, write them as a column in results file + s1 = '' + s2 = {} + result_s = '' + local_outputs = outputs + + # make sure a key file exists. If not, exit + if not Path(tmp_output_file).exists(): + logger.warning(f'Timed out waiting for: {tmp_output_file}') + exit(-33) + + with open(tmp_output_file) as f: + s1 = f.readline() + i = 0 + while s1: # read until the end of the file + for out in local_outputs: # check for each requested output + if out in s1: # If true, we found the output value that the user requested, so process it + local_outputs.remove(out) # as an optimization, drop the output from the list once we have found it + s2 = s1.split(':') # colon marks the split between the title and the data + s2 = s2[1].strip() # remove leading and trailing spaces + s2 = s2.split( + ' ' + ) # split on space because there is a unit string after the value we are looking for + s2 = s2[0].strip() # we finally have the result we were looking for + result_s += s2 + ', ' + i += 1 + if i < (len(outputs) - 1): + # go back to the beginning of the file in case the outputs that the user specified are not + # in the order that they appear in the file. + f.seek(0) + break + + s1 = f.readline() + + # append the input values to the output values so the optimal input values are easy to find, + # the form "inputVar:Rando;nextInputVar:Rando..." + result_s += '(' + input_file_entries.replace('\n', ';', -1).replace(', ', ':', -1) + ')' + + # delete temporary files + Path.unlink(Path(tmp_input_file)) + Path.unlink(Path(tmp_output_file)) + + # write out the results + result_s = result_s.strip(' ').strip(',') # get rid of last space and comma + result_s += '\n' + + with open(output_file, 'a') as f: + f.write(result_s) + + +def main(command_line_args=None): + r""" + main - this is the main function that is called when the program is run + It gets most of its key values from the command line: + 0) Code_File: Python code to run + 1) Input_file: The base model for the calculations + 2) MC_Settings_file: The settings file for the MC run: + a) the input variables to change (spelling and case are IMPORTANT), their distribution functions + (choices = normal, uniform, triangular, lognormal, binomial - see numpy.random for documentation), + and the inputs for that distribution function (Comma separated; If the mean is set to "#", + then value from the Input_file as the mode/mean). In the form: + INPUT, Maximum Temperature, normal, mean, std_dev + INPUT, Utilization Factor,uniform, min, max + INPUT, Ambient Temperature,triangular, left, mode, right + b) the output variable(s) to track (spelling and case are IMPORTANT), in the form + [NOTE: THIS LIST SHOULD BE IN THE ORDER THEY APPEAR IN THE OUTPUT FILE]: + OUTPUT, Average Net Electricity Production + OUTPUT, Electricity breakeven price + c) the number of iterations, in the form: + ITERATIONS, 1000 + d) the name of the output file (it will contain one column for each of the output variables to track), + in the form: + MC_OUTPUT_FILE, "D:\Work\GEOPHIRES3-master\MC_Result.txt" + d) the path to the python executable, it it is not already linked to "python", in the form: + PYTHON_PATH, /user/local/bin/python3 + :param enable_geophires_monte_carlo_logging_config: if True, use the logging.conf file to configure logging + :type enable_geophires_monte_carlo_logging_config: bool + """ + + # set the starting directory to be the directory that this file is in + os.chdir(os.path.dirname(os.path.abspath(__file__))) + + logger = _get_logger() + logger.info(f'Init {__name__!s}') + # keep track of execution time + tic = time.time() + + # set the starting directory to be the directory that this file is in + working_dir = os.path.dirname(os.path.abspath(__file__)) + os.chdir(working_dir) + working_dir = working_dir + os.sep + + # get the values off the command line + parser = argparse.ArgumentParser() + parser.add_argument('Code_File', help='Code File') + parser.add_argument('Input_file', help='Input file') + parser.add_argument('MC_Settings_file', help='MC Settings file') + parser.add_argument('MC_OUTPUT_FILE', help='Output file', nargs='?') + + if command_line_args is None: + logger.warn('Command line args were not passed explicitly, falling back to sys.argv') + command_line_args = sys.argv[1:] + + args = parser.parse_args(command_line_args) + + # make a list of the INPUTS, distribution functions, and the inputs for that distribution function. + # Make a list of the OUTPUTs + # Find the iteration value + # Find the Output_file + with open(args.MC_Settings_file, encoding='UTF-8') as f: + flist = f.readlines() + + inputs = [] + outputs = [] + iterations = 0 + output_file = args.MC_OUTPUT_FILE if 'MC_OUTPUT_FILE' in args and args.MC_OUTPUT_FILE is not None else '' + python_path = 'python' + + for line in flist: + clean = line.strip() + pair = clean.split(',') + pair[1] = pair[1].strip() + if pair[0].startswith('INPUT'): + inputs.append(pair[1:]) + elif pair[0].startswith('OUTPUT'): + outputs.append(pair[1]) + elif pair[0].startswith('ITERATIONS'): + iterations = int(pair[1]) + elif pair[0].startswith('MC_OUTPUT_FILE'): + output_file = pair[1] + elif pair[0].startswith('PYTHON_PATH'): + python_path = pair[1] + + # check to see if there is a "#" in an input, if so, use the results file to replace it with the value + for input_value in inputs: + # FIXME assign via index instead of reference + input_value = check_and_replace_mean(input_value, args) + + # create the file output_file. Put headers in it for each of the INPUT and OUTPUT variables + # - these form the column headings when importing into Excel + # - we include the INPUT and OUTPUT variables in the output file so that we can track the results and tell which + # combination of variables produced the interesting values (like lowest or highest, or mean) + # start by creating the string we will write as header + s = '' + + for output in outputs: + s += output + ', ' + + for input in inputs: + s += input[0] + ', ' + + s = ''.join(s.rsplit(' ', 1)) # get rid of last space + s = ''.join(s.rsplit(',', 1)) # get rid of last comma + s += '\n' + + # write the header so it is easy to import and analyze in Excel + with open(output_file, 'w') as f: + f.write(s) + + # build the args list + pass_list = [inputs, outputs, args, output_file, working_dir, python_path] # this list never changes + + args = [] + for _ in range(iterations): + args.append(pass_list) # we need to make Iterations number of copies of this list fr the map + args = tuple(args) # convert to a tuple + + # Now run the executor with the map - that will run it Iterations number of times + with concurrent.futures.ProcessPoolExecutor() as executor: + executor.map(work_package, args) + + print('\n') # See TODO re: tqdm + logger.info('Done with calculations! Summarizing...') + + # read the results into an array + with open(output_file) as f: + s = f.readline() # skip the first line + all_results = f.readlines() + + result_count = 0 + results = [] + for line in all_results: + result_count = result_count + 1 + if '-9999.0' not in line and len(s) > 1: + line = line.strip() + if len(line) > 3: + # FIXME doesn't work for HIP RA results + line, sep, tail = line.partition(', (') # strip off the Input Variable Values + results.append([float(y) for y in line.split(',')]) + else: + logger.warning(f'-9999.0 or space found in line {result_count!s}') + + actual_records_count = len(results) + + # Load the results into a pandas dataframe + results_pd = pd.read_csv(output_file) + df = pd.DataFrame(results_pd) + + # Compute the stats along the specified axes. + mins = np.nanmin(results, 0) + maxs = np.nanmax(results, 0) + medians = np.nanmedian(results, 0) + averages = np.average(results, 0) + means = np.nanmean(results, 0) + std = np.nanstd(results, 0) + + logger.info(f'Calculation Time: {time.time() - tic:10.3f} sec') + logger.info(f'Calculation Time per iteration: {(time.time() - tic) / actual_records_count:10.3f} sec') + if iterations != actual_records_count: + logger.warning( + f'NOTE: {actual_records_count!s} iterations finished successfully and were used to calculate the ' + f'statistics.' + ) + + # write them out + annotations = '' + with open(output_file, 'a') as f: + i = 0 + if iterations != actual_records_count: + f.write( + f'\n\n{actual_records_count!s} iterations finished successfully and were used to calculate the ' + f'statistics\n\n' + ) + for output in outputs: + f.write(f'{output}:\n') + f.write(f' minimum: {mins[i]:,.2f}\n') + annotations += f' minimum: {mins[i]:,.2f}\n' + f.write(f' maximum: {maxs[i]:,.2f}\n') + annotations += f' maximum: {maxs[i]:,.2f}\n' + f.write(f' median: {medians[i]:,.2f}\n') + annotations += f' median: {medians[i]:,.2f}\n' + f.write(f' average: {averages[i]:,.2f}\n') + annotations += f' average: {averages[i]:,.2f}\n' + f.write(f' mean: {means[i]:,.2f}\n') + annotations += f' mean: {means[i]:,.2f}\n' + f.write(f' standard deviation: {std[i]:,.2f}\n') + annotations += f' standard deviation: {std[i]:,.2f}\n' + + plt.figure(figsize=(8, 6)) + ax = plt.subplot() + ax.set_title(output) + ax.set_xlabel('Output units') + ax.set_ylabel('Probability') + + plt.figtext(0.11, 0.74, annotations, fontsize=8) + ret = plt.hist(df[df.columns[i]].tolist(), bins=50, density=True) + f.write(f'bin values (as percentage): {ret[0]!s}\n') + f.write(f'bin edges: {ret[1]!s}\n') + fname = df.columns[i].strip().replace('/', '-') + plt.savefig(Path(Path(output_file).parent, f'{fname}.png')) + i += 1 + annotations = '' + + logger.info(f'Complete {__name__!s}: {sys._getframe().f_code.co_name}') + + +if __name__ == '__main__': + logger = _get_logger() + logger.info(f'Init {__name__!s}') + + main(command_line_args=sys.argv[1:]) diff --git a/src/geophires_monte_carlo/__init__.py b/src/geophires_monte_carlo/__init__.py new file mode 100644 index 00000000..d6505e13 --- /dev/null +++ b/src/geophires_monte_carlo/__init__.py @@ -0,0 +1,99 @@ +import os +from enum import Enum +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Optional + +from geophires_monte_carlo import MC_GeoPHIRES3 +from geophires_monte_carlo.common import _get_logger + + +class SimulationProgram(str, Enum): + GEOPHIRES = 'GEOPHIRES', 'geophires_x/GEOPHIRESv3.py' + HIP_RA = 'HIP-RA', 'hip_ra/HIP_RA.py' + HIP_RA_X = 'HIP-RA-X', 'hip_ra_x/hip_ra_x.py' + + def __new__(cls, *args, **kwds): + obj = str.__new__(cls) + obj._value_ = args[0] + return obj + + # ignore the first param since it's already set by __new__ + def __init__(self, _: str, code_file_path: str): + self._code_file_path: Path = Path(Path(os.path.abspath(__file__)).parent.parent, code_file_path) + + # this makes sure that the description is read-only + @property + def code_file_path(self) -> Path: + return self._code_file_path + + +class MonteCarloRequest: + def __init__( + self, + simulation_program: SimulationProgram, + input_file: Path, + monte_carlo_settings_file: Path, + output_file: Optional[Path] = None, + ): + self._simulation_program: SimulationProgram = simulation_program + + if not input_file.is_absolute(): + raise ValueError(f'Input file path ({input_file}) must be absolute') + self.input_file = input_file + + if not monte_carlo_settings_file.is_absolute(): + raise ValueError(f'Monte Carlo settings file path ({monte_carlo_settings_file}) must be absolute') + self.monte_carlo_settings_file = monte_carlo_settings_file + + if output_file is not None: + self.output_file: Path = output_file + else: + self._temp_output_dir: TemporaryDirectory = TemporaryDirectory(prefix='geophires_monte_carlo-') + self.output_file: Path = Path(self._temp_output_dir.name, 'MC_GEOPHIRES_Result.txt').absolute() + + if not self.output_file.is_absolute(): + raise ValueError(f'Output file path ({output_file}) must be absolute') + + @property + def code_file_path(self) -> Path: + return self._simulation_program.code_file_path + + def __del__(self): + if hasattr(self, '_temp_output_dir'): + self._temp_output_dir.cleanup() + + +class MonteCarloResult: + def __init__(self, request: MonteCarloRequest): + self._request: MonteCarloRequest = request + + @property + def output_file_path(self) -> Path: + return self._request.output_file + + # TODO expose properties in result + + +class GeophiresMonteCarloClient: + def __init__(self): + self._logger = _get_logger() + + def get_monte_carlo_result(self, request: MonteCarloRequest) -> MonteCarloResult: + stash_cwd = Path.cwd() + + args = [str(request.code_file_path), str(request.input_file), str(request.monte_carlo_settings_file)] + if request.output_file is not None: + args.append(str(request.output_file)) + + try: + MC_GeoPHIRES3.main(command_line_args=args) + except Exception as e: + raise RuntimeError(f'Monte Carlo encountered an exception: {e!s}') from e + except SystemExit: + raise RuntimeError('Monte Carlo exited without giving a reason') from None + finally: + # Undo MC internal global settings changes + os.chdir(stash_cwd) + + return MonteCarloResult(request) diff --git a/src/geophires_monte_carlo/common.py b/src/geophires_monte_carlo/common.py new file mode 100644 index 00000000..b492714c --- /dev/null +++ b/src/geophires_monte_carlo/common.py @@ -0,0 +1,18 @@ +import logging +import sys + +_geophires_monte_carlo_logger = None + + +def _get_logger(): + global _geophires_monte_carlo_logger + if _geophires_monte_carlo_logger is None: + sh = logging.StreamHandler(sys.stdout) + sh.setLevel(logging.INFO) + sh.setFormatter(logging.Formatter(fmt='[%(asctime)s][%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')) + + _geophires_monte_carlo_logger = logging.getLogger(__name__) + _geophires_monte_carlo_logger.setLevel(logging.INFO) + _geophires_monte_carlo_logger.addHandler(sh) + + return _geophires_monte_carlo_logger diff --git a/src/geophires_x/.gitignore b/src/geophires_x/.gitignore index 0cc6df7e..b87b3b97 100644 --- a/src/geophires_x/.gitignore +++ b/src/geophires_x/.gitignore @@ -128,6 +128,7 @@ Examples\Test2.json /MC_Result_10.txt /MC_Result_100.txt /MC_Result_1000.txt +/MC_GEOPHIRES_Result.txt /References/Muffler and Cataldi Methods for regional assessment of geothermal resources.pdf /Preliminary_Corpus Christi GRA_093019.xlsx /temperature.txt diff --git a/src/geophires_x/AGSOutputs.py b/src/geophires_x/AGSOutputs.py index 46a4337d..87edbd7a 100644 --- a/src/geophires_x/AGSOutputs.py +++ b/src/geophires_x/AGSOutputs.py @@ -75,10 +75,7 @@ def PrintOutputs(self, model: Model): if not model.economics.econmodel.value == EconomicModel.CLGS: super().PrintOutputs(model) else: - outputfile = "HDR.out" - if len(sys.argv) > 2: - outputfile = sys.argv[2] - with open(outputfile, 'w', encoding='UTF-8') as f: + with open(self.output_file, 'w', encoding='UTF-8') as f: f.write(' *****************\n') f.write(' ***CASE REPORT***\n') f.write(' *****************\n') diff --git a/src/geophires_x/Examples/MC_GEOPHIRES_Settings_file.txt b/src/geophires_x/Examples/MC_GEOPHIRES_Settings_file.txt index 8748403e..ebdc018b 100644 --- a/src/geophires_x/Examples/MC_GEOPHIRES_Settings_file.txt +++ b/src/geophires_x/Examples/MC_GEOPHIRES_Settings_file.txt @@ -7,4 +7,4 @@ OUTPUT, Average Production Temperature OUTPUT, Average Annual Total Electricity Generation ITERATIONS, 250 MC_OUTPUT_FILE, MC_GEOPHIRES_Result.txt -PYTHON_PATH, D:\Work\python-geophires-x-nrel\venv\Scripts\python.exe +PYTHON_PATH, ..\..\venv\Scripts\python.exe diff --git a/src/geophires_x/GEOPHIRESv3.py b/src/geophires_x/GEOPHIRESv3.py index a8cc224e..dd4643f9 100644 --- a/src/geophires_x/GEOPHIRESv3.py +++ b/src/geophires_x/GEOPHIRESv3.py @@ -75,6 +75,7 @@ def main(enable_geophires_logging_config=True): outputfile = 'HDR.out' if len(sys.argv) > 2: outputfile = sys.argv[2] + with open(outputfile, 'r', encoding='UTF-8') as f: content = f.readlines() # store all output in one long list diff --git a/src/geophires_x/GeoPHIRESUtils.py b/src/geophires_x/GeoPHIRESUtils.py index 7b3a48ed..4bbcce5d 100644 --- a/src/geophires_x/GeoPHIRESUtils.py +++ b/src/geophires_x/GeoPHIRESUtils.py @@ -336,32 +336,43 @@ def UtilEff_func(temperature_degC: float) -> float: return util_eff -def read_input_file(return_dict_1, logger=None): +def read_input_file(return_dict_1, logger=None, input_file_name=None): """ Read input file and return a dictionary of parameters :param return_dict_1: dictionary of parameters :param logger: logger object :return: dictionary of parameters :rtype: dict + + FIXME modifies dict instead of returning it - it should do what the doc says it does and return a dict instead, + relying on mutation of parameters is Bad """ + if logger is None: + logger = logging.getLogger(__name__) + logger.info(f'Init {__name__}') # Specify path of input file - it will always be the first command line argument. # If it doesn't exist, simply run the default model without any inputs # read input data (except input from optional filenames) - if len(sys.argv) > 1: - f_name = sys.argv[1] + if input_file_name is None: + logger.warning('Input file name not provided, checking sys.argv') + if len(sys.argv) > 1: + input_file_name = sys.argv[1] + logger.warning(f'Using input file from sys.argv: {input_file_name}') + + if input_file_name is not None: content = [] - if exists(f_name): - logger.info(f'Found filename: {f_name}. Proceeding with run using input parameters from that file') - with open(f_name, encoding='UTF-8') as f: + if exists(input_file_name): + logger.info(f'Found filename: {input_file_name}. Proceeding with run using input parameters from that file') + with open(input_file_name, encoding='UTF-8') as f: # store all input in one long string that will be passed to all objects # so they can parse out their specific parameters (and ignore the rest) content = f.readlines() else: - raise FileNotFoundError(f'Unable to read input file: File {f_name} not found') + raise FileNotFoundError(f'Unable to read input file: File {input_file_name} not found') # successful read of data into list. Now make a dictionary with all the parameter entries. # Index will be the unique name of the parameter. diff --git a/src/geophires_x/MC_GeoPHIRES3.py b/src/geophires_x/MC_GeoPHIRES3.py deleted file mode 100644 index d14c950a..00000000 --- a/src/geophires_x/MC_GeoPHIRES3.py +++ /dev/null @@ -1,358 +0,0 @@ -#! python -# -*- coding: utf-8 -*- -""" -Framework for running Monte Carlo simulations using GEOPHIRES v3.0 & HIP-RA 1.0 -build date: September 2023 -Created on Wed November 16 10:43:04 2017 -@author: Malcolm Ross V3 -""" - -import os -import sys -import time -import logging -import logging.config -import numpy as np -import argparse -import uuid -import shutil -import concurrent.futures -import subprocess -import matplotlib.pyplot as plt -import pandas as pd - - -def CheckAndReplaceMean(input_value, args) -> list: - """ - CheckAndReplaceMean - check to see if the user has requested that a value be replaced by a mean value by specifying - a value as "#" - :param input_value: the value to check - :type input_value: list - :param args: the list of arguments passed in from the command line - :type args: list - :return: the input_value, with the mean value replaced if necessary - :rtype: list - """ - i = 0 - for inputx in input_value: - if "#" in inputx: - # found one we have to process. - VariName = input_value[0] - # find it in the Input_file - with open(args.Input_file) as f: - ss = f.readlines() - for s in ss: - if str(s).startswith(VariName): - s2 = s.split(",") - input_value[i] = s2[1] - break - break - i = i + 1 - return input_value - - -def WorkPackage(pass_list): - """ - WorkPackage - this is the function that is called by the executor. It does the work of running the simulation - :param pass_list: the list of arguments passed in from the command line - :type pass_list: list - :return: None - """ - Inputs = pass_list[0] - Outputs = pass_list[1] - args = pass_list[2] - Outputfile = pass_list[3] - working_dir = pass_list[4] - PythonPath = pass_list[5] - - tmpoutputfile = tmpfilename = "" - # get random values for each of the INPUTS based on the distributions and boundary values - rando = 0.0 - s = "" - print("#", end="") - for input_value in Inputs: - if input_value[1].strip().startswith('normal'): - rando = np.random.normal(float(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + "\n" - elif input_value[1].strip().startswith('uniform'): - rando = np.random.uniform(float(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + "\n" - elif input_value[1].strip().startswith('triangular'): - rando = np.random.triangular(float(input_value[2]), float(input_value[3]), float(input_value[4])) - s = s + input_value[0] + ", " + str(rando) + "\n" - if input_value[1].strip().startswith('lognormal'): - rando = np.random.lognormal(float(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + "\n" - if input_value[1].strip().startswith('binomial'): - rando = np.random.binomial(int(input_value[2]), float(input_value[3])) - s = s + input_value[0] + ", " + str(rando) + "\n" - - # make up a temporary file name that will be shared among files for this iteration - tmpfilename = working_dir + str(uuid.uuid4()) + ".txt" - tmpoutputfile = tmpfilename.replace(".txt", "_result.txt") - - # copy the contents of the Input_file into a new input file - shutil.copyfile(args.Input_file, tmpfilename) - - # append those values to the new input file in the format "variable name, new_random_value". - # This will cause GeoPHIRES/HIP-RA to replace the value in the file with this random value in the calculation - # if it exists in that fiole already, or it will set it to the value as if it was a new value set by the user. - with open(tmpfilename, "a") as f: - f.write(s) - - # start the passed in program name (usually GeoPHIRES or HIP-RA) with the supplied input file. - # Capture the output into a filename that is the same as the input file but has the suffix "_result.txt". - sprocess = subprocess.Popen([PythonPath, args.Code_File, tmpfilename, tmpoutputfile], stdout=subprocess.DEVNULL) - sprocess.wait() - - # look thru "_result.txt" file for the OUTPUT variables that the user asked for. - # For each of them, write them as a column in results file - s1 = "" - s2 = {} - result_s = "" - localOutputs = Outputs - - # make sure a key file exists. If not, exit - if not os.path.exists(tmpoutputfile): - print("Timed out waiting for: " + tmpoutputfile) - # logger.warning("Timed out waiting for: " + tmpoutputfile) - exit(-33) - - with open(tmpoutputfile, "r") as f: - s1 = f.readline() - i = 0 - while s1: # read until the end of the file - for out in localOutputs: # check for each requested output - if out in s1: # If true, we found the output value that the user requested, so process it - localOutputs.remove(out) # as an optimization, drop the output from the list once we have found it - s2 = s1.split(":") # colon marks the split between the title and the data - s2 = s2[1].strip() # remove leading and trailing spaces - s2 = s2.split(" ") # split on space because there is a unit string after the value we are looking for - s2 = s2[0].strip() # we finally have the result we were looking for - result_s = result_s + s2 + ", " - i = i + 1 - if i < (len(Outputs) - 1): - # go back to the beginning of the file in case the outputs that the user specified are not - # in the order that they appear in the file. - f.seek(0) - break - s1 = f.readline() - - # append the input values to the output values so the optimal input values are easy to find, - # the form "inputVar:Rando;nextInputVar:Rando..." - result_s = result_s + "(" + s.replace("\n", ";", -1).replace(", ", ":", -1) + ")" - - # delete temporary files - os.remove(tmpfilename) - os.remove(tmpoutputfile) - - # write out the results - result_s = result_s.strip(" ") # get rid of last space - result_s = result_s.strip(",") # et rid of last comma - result_s = result_s + "\n" - with open(Outputfile, "a") as f: - f.write(result_s) - - -def main(enable_geophires_logging_config=True): - """ - main - this is the main function that is called when the program is run - It gets most of its key values from the command line: - 0) Code_File: Python code to run - 1) Input_file: The base model for the calculations - 2) MC_Settings_file: The settings file for the MC run: - a) the input variables to change (spelling and case are IMPORTANT), their distribution functions - (choices = normal, uniform, triangular, lognormal, binomial - see numpy.random for documentation), - and the inputs for that distribution function (Comma separated; If the mean is set to "#", - then value from the Input_file as the mode/mean). In the form: - INPUT, Maximum Temperature, normal, mean, std_dev - INPUT, Utilization Factor,uniform, min, max - INPUT, Ambient Temperature,triangular, left, mode, right - b) the output variable(s) to track (spelling and case are IMPORTANT), in the form - [NOTE: THIS LIST SHOULD BE IN THE ORDER THEY APPEAR IN THE OUTPUT FILE]: - OUTPUT, Average Net Electricity Production - OUTPUT, Electricity breakeven price - c) the number of iterations, in the form: - ITERATIONS, 1000 - d) the name of the output file (it will contain one column for each of the output variables to track), - in the form: - MC_OUTPUT_FILE, "D:\Work\GEOPHIRES3-master\MC_Result.txt" - d) the path to the python executable, it it is not already linked to "python", in the form: - PYTHON_PATH, /user/local/bin/python3 - :param enable_geophires_logging_config: if True, use the logging.conf file to configure logging - :type enable_geophires_logging_config: bool - :return: None - """ - # set the starting directory to be the directory that this file is in - os.chdir(os.path.dirname(os.path.abspath(__file__))) - # set up logging. - if enable_geophires_logging_config: - # set up logging. - logging.config.fileConfig('logging.conf') - logger = logging.getLogger('root') - logger.info("Init " + str(__name__)) - # keep track of execution time - tic = time.time() - - # set the starting directory to be the directory that this file is in - working_dir = os.path.dirname(os.path.abspath(__file__)) - os.chdir(working_dir) - working_dir = working_dir + os.sep - - # get the values off the command line - parser = argparse.ArgumentParser() - parser.add_argument("Code_File", help="Code_File") - parser.add_argument("Input_file", help="Input file") - parser.add_argument("MC_Settings_file", help="MC Settings file") - args = parser.parse_args() - - # make a list of the INPUTS, distribution functions, and the inputs for that distribution function. - # Make a list of the OUTPUTs - # Find the iteration value - # Find the Output_file - with open(args.MC_Settings_file, encoding='UTF-8') as f: - flist = f.readlines() - - Inputs = [] - Outputs = [] - Iterations = 0 - Outputfile = "" - PythonPath = "python" - for line in flist: - clean = line.strip() - pair = clean.split(",") - pair[1] = pair[1].strip() - if pair[0].startswith("INPUT"): - Inputs.append(pair[1:]) - elif pair[0].startswith("OUTPUT"): - Outputs.append(pair[1]) - elif pair[0].startswith("ITERATIONS"): - Iterations = int(pair[1]) - elif pair[0].startswith("MC_OUTPUT_FILE"): - Outputfile = pair[1] - elif pair[0].startswith("PYTHON_PATH"): - PythonPath = pair[1] - - # check to see if there is a "#" in an input, if so, use the results file to replace it with the value - for input_value in Inputs: - input_value = CheckAndReplaceMean(input_value, args) - - # create the file output_file. Put headers in it for each of the INPUT and OUTPUT variables - # - these form the column headings when importing into Excel - # - we include the INPUT and OUTPUT variables in the output file so that we can track the results and tell which - # combination of variables produced the interesting values (like lowest or highest, or mean) - # start by creating the string we will write as header - s = "" - for output in Outputs: - s = s + output + ", " - for input in Inputs: - s = s + input[0] + ", " - s = "".join(s.rsplit(" ", 1)) # get rid of last space - s = "".join(s.rsplit(",", 1)) # get rid of last comma - s = s + "\n" - - # write the header so it is easy to import and analyze in Excel - with open(Outputfile, "w") as f: - f.write(s) - - # TODO Use a scratch directory to minimize the mess: https://docs.python.org/3/library/tempfile.html#tempfile.TemporaryDirectory - # TODO Use tdqm library to show progress bar on screen: https://github.com/tqdm/tqdm - # build the args list - pass_list = [Inputs, Outputs, args, Outputfile, working_dir, PythonPath] # this list never changes - - args = [] - for i in range(0, Iterations): - args.append(pass_list) # we need to make Iterations number of copies of this list fr the map - args = tuple(args) # convert to a tuple - - # Now run the executor with the map - that will run it Iterations number of times - with concurrent.futures.ProcessPoolExecutor() as executor: - executor.map(WorkPackage, args) - - print("\n" + "Done with calculations! Summarizing..." + "\n") - logger.info("Done with calculations! Summarizing...") - - # read the results into an array - with open(Outputfile, "r") as f: - s = f.readline() # skip the first line - all_results = f.readlines() - - result_count = 0 - Results = [] - for line in all_results: - result_count = result_count + 1 - if "-9999.0" not in line and len(s) > 1: - line = line.strip() - if len(line) > 3: - line, sep, tail = line.partition(', (') # strip off the Input Variable Values - Results.append([float(y) for y in line.split(",")]) - else: - logger.warning("-9999.0 or space found in line " + str(result_count)) - - actual_records_count = len(Results) - - # Load the results into a pandas dataframe - results_pd = pd.read_csv(Outputfile) - df = pd.DataFrame(results_pd) - - # Compute the stats along the specified axes. - mins = np.nanmin(Results, 0) - maxs = np.nanmax(Results, 0) - medians = np.nanmedian(Results, 0) - averages = np.average(Results, 0) - means = np.nanmean(Results, 0) - std = np.nanstd(Results, 0) - - print(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") - logger.info(" Calculation Time: " + "{0:10.3f}".format((time.time() - tic)) + " sec\n") - print(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") - logger.info(" Calculation Time per iteration: " + "{0:10.3f}".format(((time.time() - tic)) / actual_records_count) + " sec\n") - if Iterations != actual_records_count: - print("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") - logger.warning("\n\nNOTE:" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics.\n\n") - - # write them out - annotations = "" - with open(Outputfile, "a") as f: - i = 0 - if Iterations != actual_records_count: - f.write("\n\n" + str(actual_records_count) + " iterations finished successfully and were used to calculate the statistics\n\n") - for output in Outputs: - f.write(output + ":" + "\n") - f.write(f" minimum: {mins[i]:,.2f}\n") - annotations = annotations + f" minimum: {mins[i]:,.2f}\n" - f.write(f" maximum: {maxs[i]:,.2f}\n") - annotations = annotations + f" maximum: {maxs[i]:,.2f}\n" - f.write(f" median: {medians[i]:,.2f}\n") - annotations = annotations + f" median: {medians[i]:,.2f}\n" - f.write(f" average: {averages[i]:,.2f}\n") - annotations = annotations + f" average: {averages[i]:,.2f}\n" - f.write(f" mean: {means[i]:,.2f}\n") - annotations = annotations + f" mean: {means[i]:,.2f}\n" - f.write(f" standard deviation: {std[i]:,.2f}\n") - annotations = annotations + f" standard deviation: {std[i]:,.2f}\n" - - plt.figure(figsize=(8, 6)) - ax = plt.subplot() - ax.set_title(output) - ax.set_xlabel("Output units") - ax.set_ylabel("Probability") - - plt.figtext(0.11, 0.74, annotations, fontsize=8) - ret = plt.hist(df[df.columns[i]].tolist(), bins=50, density=True) - f.write('bin values (as percentage): ' + str(ret[0]) + '\n') - f.write('bin edges: ' + str(ret[1]) + '\n') - fname = df.columns[i].strip().replace("/", "-") - plt.savefig(working_dir + fname + '.png') - i = i + 1 - annotations = "" - - logger.info("Complete " + str(__name__) + ": " + sys._getframe().f_code.co_name) - - -if __name__ == "__main__": - # set up logging. - logger = logging.getLogger('root') - logger.info("Init " + str(__name__)) - - main() diff --git a/src/geophires_x/Model.py b/src/geophires_x/Model.py index 4f4c6f7d..ebf34ab2 100644 --- a/src/geophires_x/Model.py +++ b/src/geophires_x/Model.py @@ -1,3 +1,4 @@ +import sys from pathlib import Path import logging import time @@ -46,17 +47,13 @@ class Model(object): Model is the container class of the application, giving access to everything else, including the logger """ - def __init__(self, enable_geophires_logging_config=True): - """ - The __init__ function is called automatically every time the class is being used to create a new object. - :return: Nothing - """ + def __init__(self, enable_geophires_logging_config=True, input_file=None): # get logging started - self.logger = logging.getLogger('root') + self.logger = logging.getLogger('root') # TODO should be getting __name__ logger instead of root if enable_geophires_logging_config: - logging.config.fileConfig(Path(Path(__file__).parent,'logging.conf')) + logging.config.fileConfig(Path(Path(__file__).parent, 'logging.conf')) self.logger.setLevel(logging.INFO) self.logger.info(f'Init {__class__}: {__name__}') @@ -70,7 +67,10 @@ def __init__(self, enable_geophires_logging_config=True): # we do this as soon as possible because what we instantiate may depend on settings in this file self.InputParameters = {} - read_input_file(self.InputParameters, logger=self.logger) + if input_file is None and len(sys.argv) > 1: + input_file = sys.argv[1] + + read_input_file(self.InputParameters, logger=self.logger, input_file_name=input_file) self.ccuseconomics = None self.ccusoutputs = None @@ -106,7 +106,12 @@ def __init__(self, enable_geophires_logging_config=True): self.wellbores = WellBores(self) self.surfaceplant = SurfacePlant(self) self.economics = Economics(self) - self.outputs = Outputs(self) + + output_file = 'HDR.out' + if len(sys.argv) > 2: + output_file = sys.argv[2] + + self.outputs = Outputs(self, output_file=output_file) if 'Reservoir Model' in self.InputParameters: if self.InputParameters['Reservoir Model'].sValue == '7': @@ -114,7 +119,7 @@ def __init__(self, enable_geophires_logging_config=True): self.wellbores = SUTRAWellBores(self) self.surfaceplant = SurfacePlantSUTRA(self) self.economics = SUTRAEconomics(self) - self.outputs = SUTRAOutputs(self) + self.outputs = SUTRAOutputs(self, output_file=output_file) if 'Is AGS' in self.InputParameters: if self.InputParameters['Is AGS'].sValue in ['True', 'true', 'TRUE', 'T', '1']: @@ -127,27 +132,27 @@ def __init__(self, enable_geophires_logging_config=True): self.wellbores = AGSWellBores(self) self.surfaceplant = SurfacePlantAGS(self) self.economics = AGSEconomics(self) - self.outputs = AGSOutputs(self) + self.outputs = AGSOutputs(self, output_file=output_file) self.wellbores.IsAGS.value = True # if we find out we have an add-ons, we need to instantiate it, then read for the parameters if 'AddOn Nickname 1' in self.InputParameters: self.logger.info("Initiate the Add-on elements") self.addeconomics = EconomicsAddOns(self) - self.addoutputs = OutputsAddOns(self) + self.addoutputs = OutputsAddOns(self, output_file=output_file) # if we find out we have a ccus, we need to instantiate it, then read for the parameters if 'Ending CCUS Credit Value' in self.InputParameters: self.logger.info("Initiate the CCUS elements") self.ccuseconomics = EconomicsCCUS(self) - self.ccusoutputs = OutputsCCUS(self) + self.ccusoutputs = OutputsCCUS(self, output_file=output_file) # if we find out we have an S-DAC-GT calculation, we need to instantiate it if 'S-DAC-GT' in self.InputParameters: if self.InputParameters['S-DAC-GT'].sValue == 'On': self.logger.info("Initiate the S-DAC-GT elements") self.sdacgteconomics = EconomicsS_DAC_GT(self) - self.sdacgtoutputs = OutputsS_DAC_GT(self) + self.sdacgtoutputs = OutputsS_DAC_GT(self, output_file=output_file) self.logger.info(f'Complete {__class__}: {__name__}') diff --git a/src/geophires_x/Outputs.py b/src/geophires_x/Outputs.py index 1d000e22..17fa17b6 100644 --- a/src/geophires_x/Outputs.py +++ b/src/geophires_x/Outputs.py @@ -16,25 +16,16 @@ class Outputs: """ This class handles all the outputs for the GEOPHIRESv3 model. """ - def __init__(self, model:Model): - """ - The __init__ function is called automatically when a class is instantiated. - It initializes the attributes of an object, and sets default values for certain arguments that can be - overridden by user input. - The __init__ function is used to set up all the parameters in the Outputs. - :param model: The container class of the application, giving access to everything else, including the logger - :type model: :class:`~geophires_x.Model.Model` - :return: None - """ - - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + def __init__(self, model:Model, output_file:str ='HDR.out'): + model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') # Dictionary to hold the Units definitions that the user wants for outputs created by GEOPHIRES. # It is empty by default initially - this will expand as the user desires are read from the input file self.ParameterDict = {} self.printoutput = True + self.output_file = output_file - model.logger.info("Complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def __str__(self): return "Outputs" @@ -56,7 +47,7 @@ def read_parameters(self, model:Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') if len(model.InputParameters) > 0: # if the user wants it, we need to know if the user wants to copy the contents of the @@ -75,7 +66,7 @@ def read_parameters(self, model:Model) -> None: # handle special cases - model.logger.info("Complete "+ str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def PrintOutputs(self, model: Model): """ @@ -111,9 +102,7 @@ def PrintOutputs(self, model: Model): # write results to output file and screen try: - outputfile = "HDR.out" - if len(sys.argv) > 2: outputfile = sys.argv[2] - with open(outputfile,'w', encoding='UTF-8') as f: + with open(self.output_file, 'w', encoding='UTF-8') as f: f.write(' *****************\n') f.write(' ***CASE REPORT***\n') f.write(' *****************\n') diff --git a/src/geophires_x/OutputsAddOns.py b/src/geophires_x/OutputsAddOns.py index bce95e1a..cf3ea209 100644 --- a/src/geophires_x/OutputsAddOns.py +++ b/src/geophires_x/OutputsAddOns.py @@ -22,10 +22,7 @@ def PrintOutputs(self, model): # now do AddOn output, which will append to the original output # write results to output file and screen try: - outputfile = "HDR.out" - if len(sys.argv) > 2: - outputfile = sys.argv[2] - with open(outputfile, 'a', encoding='UTF-8') as f: + with open(self.output_file, 'a', encoding='UTF-8') as f: f.write(NL) f.write(NL) f.write(" ***EXTENDED ECONOMICS***\n") diff --git a/src/geophires_x/OutputsCCUS.py b/src/geophires_x/OutputsCCUS.py index 67e421d6..9d43892d 100644 --- a/src/geophires_x/OutputsCCUS.py +++ b/src/geophires_x/OutputsCCUS.py @@ -24,10 +24,7 @@ def PrintOutputs(self, model): # now do CCUS output, which will append to the original output # write results to output file and screen try: - outputfile = "HDR.out" - if len(sys.argv) > 2: - outputfile = sys.argv[2] - with open(outputfile, 'a', encoding='UTF-8') as f: + with open(self.output_file, 'a', encoding='UTF-8') as f: f.write(NL) f.write(NL) f.write(" ***CCUS ECONOMICS***" + NL) diff --git a/src/geophires_x/OutputsS_DAC_GT.py b/src/geophires_x/OutputsS_DAC_GT.py index 4fce8543..a619770e 100644 --- a/src/geophires_x/OutputsS_DAC_GT.py +++ b/src/geophires_x/OutputsS_DAC_GT.py @@ -20,10 +20,7 @@ def PrintOutputs(self, model): # now do S_DAC_GT output, which will append to the original output # write results to output file and screen try: - outputfile = "HDR.out" - if len(sys.argv) > 2: - outputfile = sys.argv[2] - with open(outputfile, 'a', encoding='UTF-8') as f: + with open(self.output_file, 'a', encoding='UTF-8') as f: f.write(NL) f.write(NL) f.write(" ***S_DAC_GT ECONOMICS***" + NL) diff --git a/src/geophires_x/SUTRAOutputs.py b/src/geophires_x/SUTRAOutputs.py index ca5227ca..b1b77056 100644 --- a/src/geophires_x/SUTRAOutputs.py +++ b/src/geophires_x/SUTRAOutputs.py @@ -11,7 +11,9 @@ NL="\n" class SUTRAOutputs: - def __init__(self, model:Model): + """TODO should inherit from Outputs""" + + def __init__(self, model:Model, output_file:str ='HDR.out'): """ The __init__ function is called automatically when a class is instantiated. It initializes the attributes of an object, and sets default values for certain arguments that can be @@ -28,11 +30,12 @@ def __init__(self, model:Model): # It is empty by default initially - this will expand as the user desires are read from the input file self.ParameterDict = {} self.printoutput = True + self.output_file = output_file model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}') def __str__(self): - return "Outputs" + return 'Outputs' def read_parameters(self, model:Model) -> None: """ @@ -106,11 +109,7 @@ def PrintOutputs(self, model: Model): # write results to output file and screen try: - outputfile = "HDR.out" - if len(sys.argv) > 2: - outputfile = sys.argv[2] - - with open(outputfile,'w', encoding='UTF-8') as f: + with open(self.output_file,'w', encoding='UTF-8') as f: f.write(' *****************\n') f.write(' ***CASE REPORT***\n') f.write(' *****************\n') @@ -217,6 +216,7 @@ def PrintOutputs(self, model: Model): print("Error: GEOPHIRES Failed to write the output file. Exiting....Line %i" % tb.tb_lineno) model.logger.critical(str(ex)) model.logger.critical("Error: GEOPHIRES Failed to write the output file. Exiting....Line %i" % tb.tb_lineno) + # FIXME raise exception instead of sys.exit() sys.exit() model.logger.info(f'Complete {str(__class__)}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/TDPReservoir.py b/src/geophires_x/TDPReservoir.py index 8314d95f..a8bc7212 100644 --- a/src/geophires_x/TDPReservoir.py +++ b/src/geophires_x/TDPReservoir.py @@ -1,8 +1,9 @@ import sys -from .Parameter import floatParameter -from .Units import * + +from geophires_x.Parameter import floatParameter import geophires_x.Model as Model -from .Reservoir import Reservoir +from geophires_x.Reservoir import Reservoir +from geophires_x.Units import DrawdownUnit, Units class TDPReservoir(Reservoir): @@ -19,7 +20,7 @@ def __init__(self, model: Model): :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') super().__init__(model) # initialize the parent parameters and variables sclass = str(__class__).replace("", "") @@ -47,7 +48,7 @@ def __init__(self, model: Model): ToolTipText="specify the thermal drawdown for reservoir model 3 and 4" ) - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def __str__(self): return "TDPReservoir" @@ -63,14 +64,14 @@ def read_parameters(self, model: Model) -> None: :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') super().read_parameters(model) # read the parameters for the parent. # if we call super, we don't need to deal with setting the parameters here, just deal with the special cases # for the variables in this class # because the call to the super.readparameters will set all the variables, # including the ones that are specific to this class - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') def Calculate(self, model: Model): """ @@ -79,11 +80,11 @@ def Calculate(self, model: Model): :type model: :class:`~geophires_x.Model.Model` :return: None """ - model.logger.info("Init " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Init {__class__!s}: {sys._getframe().f_code.co_name}') super().Calculate(model) # run calculation for the parent. model.reserv.Tresoutput.value = (1 - model.reserv.drawdp.value * model.reserv.timevector.value) * \ (model.reserv.Trock.value - model.wellbores.Tinj.value) + \ model.wellbores.Tinj.value # this is no longer as in thesis (equation 4.16) - model.logger.info("Complete " + str(__class__) + ": " + sys._getframe().f_code.co_name) + model.logger.info(f'Complete {__class__!s}: {sys._getframe().f_code.co_name}') diff --git a/src/geophires_x/__init__.py b/src/geophires_x/__init__.py index 6c61f23e..f6310076 100644 --- a/src/geophires_x/__init__.py +++ b/src/geophires_x/__init__.py @@ -1 +1 @@ -__version__ = '3.3.4' +__version__ = '3.4.0' diff --git a/tests/examples/example1_addons.txt b/tests/examples/example1_addons.txt index 819c4a62..37a58ea3 100644 --- a/tests/examples/example1_addons.txt +++ b/tests/examples/example1_addons.txt @@ -28,7 +28,7 @@ AddOn OPEX 2, 1 AddOn Electricity Gained 2, 26000.0 AddOn Heat Gained 2, 0 AddOn Profit Gained 2, 2.786 -AddOn Nickname 3, CO2 point capture from methane buring +AddOn Nickname 3, CO2 point capture from methane burning AddOn CAPEX 3, 40 AddOn OPEX 3, 0.6 AddOn Electricity Gained 3, 0.0 diff --git a/tests/geophires_monte_carlo_tests/GEOPHIRES-example1.txt b/tests/geophires_monte_carlo_tests/GEOPHIRES-example1.txt new file mode 100644 index 00000000..4e96ea5d --- /dev/null +++ b/tests/geophires_monte_carlo_tests/GEOPHIRES-example1.txt @@ -0,0 +1,73 @@ +GEOPHIRES v2.0 Input File +Created on 6/11/2018 +Last modified on 6/12/2018 +Geothermal Electricity Problem using a Multiple Parallel Fractures Model + +Example 1 Description: This problem considers an EGS reservoir at 3km depth. +Ramey's model is applied to simulate production wellbore heat losses. The heat +is used in for electricity application with a reinjection temperature of 50deg.C. + + +***Subsurface technical parameters*** +************************************* +Reservoir Model,1, ---Multiple Fractures reservoir model +Reservoir Depth,3, ---[km] +Number of Segments,1, ---[-] +Gradient 1,50, ---[deg.C/km] +Maximum Temperature,400, ---[deg.C] +Number of Production Wells,2, ---[-] +Number of Injection Wells,2, ---[-] +Production Well Diameter,7, ---[inch] +Injection Well Diameter,7, ---[inch] +Ramey Production Wellbore Model,1, ---0 if disabled 1 if enabled +Production Wellbore Temperature Drop,.5, ---[deg.C] +Injection Wellbore Temperature Gain,0, ---[deg.C] +Production Flow Rate per Well,55, ---[kg/s] +Fracture Shape,3, ---[-] Should be 1 2 3 or 4. See manual for details +Fracture Height,900, ---[m] +Reservoir Volume Option,3, ---[-] Should be 1 2 3 or 4. See manual for details +Number of Fractures,20, ---[-] +Reservoir Volume,1000000000, ---[m^3] +Water Loss Fraction,.02, ---[-] +Productivity Index,5, ---[kg/s/bar] +Injectivity Index,5, ---[kg/s/bar] +Injection Temperature,50, ---[deg.C] +Maximum Drawdown,1, ---[-] no redrilling considered +Reservoir Heat Capacity,1000, ---[J/kg/K] +Reservoir Density,2700, ---[kg/m^3] +Reservoir Thermal Conductivity,2.7, ---[W/m/K] + +***SURFACE TECHNICAL PARAMETERS*** +********************************** +End-Use Option,1, ---[-] Electricity +Power Plant Type,2, ---[-] Supercritcal ORC +Circulation Pump Efficiency,.8, ---[-] between .1 and 1 +Utilization Factor,.9, ---[-] between .1 and 1 +Surface Temperature,20, ---[deg.C] +Ambient Temperature,20, ---[deg.C] + +***FINANCIAL PARAMETERS*** +************************** +Plant Lifetime,30, ---[years] +Economic Model,1, ---[-] Fixed Charge Rate Model +Fixed Charge Rate,.05, ---[-] between 0 and 1 +Inflation Rate During Construction,0, ---[-] + +***CAPITAL AND O&M COST PARAMETERS*** +************************************* +Well Drilling and Completion Capital Cost Adjustment Factor,1, ---[-] Use built-in correlations +Well Drilling Cost Correlation,1, ---[-] Use built-in correlations +Reservoir Stimulation Capital Cost Adjustment Factor,1, ---[-] Use built-in correlations +Surface Plant Capital Cost Adjustment Factor,1, ---[-] Use built-in correlations +Field Gathering System Capital Cost Adjustment Factor,1, ---[-] Use built-in correlations +Exploration Capital Cost Adjustment Factor,1, ---[-] Use built-in correlations +Wellfield O&M Cost Adjustment Factor,1, ---[-] Use built-in correlations +Surface Plant O&M Cost Adjustment Factor,1, ---[-] Use built-in correlations +Water Cost Adjustment Factor,1, ---[-] Use built-in correlations + + +***Simulation Parameters*** +*************************** + +Print Output to Console,1, ---[-] Should be 0 (don't print results) or 1 (print results) +Time steps per year,6, ---[1/year] diff --git a/tests/geophires_monte_carlo_tests/HIP-example1.txt b/tests/geophires_monte_carlo_tests/HIP-example1.txt new file mode 100644 index 00000000..3c8818e9 --- /dev/null +++ b/tests/geophires_monte_carlo_tests/HIP-example1.txt @@ -0,0 +1,8 @@ +Reservoir Temperature, 250.0 +Rejection Temperature, 60.0 +Formation Porosity, 10.0 +Reservoir Area, 55.0 +Reservoir Thickness, 0.25 +Reservoir Life Cycle, 25 +Heat Capacity Of Water, -1 +Density Of Water, -1 diff --git a/tests/geophires_monte_carlo_tests/MC_GEOPHIRES_Settings_file.txt b/tests/geophires_monte_carlo_tests/MC_GEOPHIRES_Settings_file.txt new file mode 100644 index 00000000..8d33e75e --- /dev/null +++ b/tests/geophires_monte_carlo_tests/MC_GEOPHIRES_Settings_file.txt @@ -0,0 +1,8 @@ +INPUT, Gradient 1, uniform, 25, 60 +INPUT, Reservoir Temperature, normal, 250, 10 +INPUT, Utilization Factor,uniform, 0.7, 0.95 +INPUT, Ambient Temperature,triangular, 15, 20, 25 +OUTPUT, Average Net Electricity Production +OUTPUT, Average Production Temperature +OUTPUT, Average Annual Total Electricity Generation +ITERATIONS, 5 diff --git a/tests/geophires_monte_carlo_tests/MC_HIP_Settings_file.txt b/tests/geophires_monte_carlo_tests/MC_HIP_Settings_file.txt new file mode 100644 index 00000000..3a6baae6 --- /dev/null +++ b/tests/geophires_monte_carlo_tests/MC_HIP_Settings_file.txt @@ -0,0 +1,12 @@ +INPUT, Formation Porosity, uniform, 9.0, 28.0 +INPUT, Reservoir Area, uniform, 50.0, 120.0 +INPUT, Reservoir Thickness, uniform, 0.122, 0.299 +INPUT, Reservoir Temperature, uniform, 130, 170 +INPUT, Rejection Temperature, uniform, 20, 33 +OUTPUT, Available Heat (fluid) +OUTPUT, Producible Heat (fluid) +OUTPUT, Producible Heat/Unit Area (fluid) +OUTPUT, Producible Electricity (fluid) +OUTPUT, Producible Electricity/Unit Area (fluid) +ITERATIONS, 250 +MC_OUTPUT_FILE, MC_HIP_Result.txt diff --git a/tests/geophires_monte_carlo_tests/__init__.py b/tests/geophires_monte_carlo_tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/geophires_monte_carlo_tests/test_geophires_monte_carlo.py b/tests/geophires_monte_carlo_tests/test_geophires_monte_carlo.py new file mode 100644 index 00000000..1ba830ac --- /dev/null +++ b/tests/geophires_monte_carlo_tests/test_geophires_monte_carlo.py @@ -0,0 +1,53 @@ +import os +import unittest +from pathlib import Path + +from geophires_monte_carlo import GeophiresMonteCarloClient +from geophires_monte_carlo import MonteCarloRequest +from geophires_monte_carlo import MonteCarloResult +from geophires_monte_carlo import SimulationProgram + + +class GeophiresMonteCarloTestCase(unittest.TestCase): + def test_geophires_monte_carlo(self): + client = GeophiresMonteCarloClient() + + result: MonteCarloResult = client.get_monte_carlo_result( + MonteCarloRequest( + SimulationProgram.GEOPHIRES, + self._get_arg_file_path('GEOPHIRES-example1.txt'), + self._get_arg_file_path('MC_GEOPHIRES_Settings_file.txt'), + ) + ) + self.assertIsNotNone(result) + self.assertIsNotNone(result.output_file_path) + + with open(result.output_file_path) as f: + result_content = '\n'.join(f.readlines()) + self.assertIn('Electricity', result_content) + + @unittest.skip(reason='FIXME: MC HIP result parsing is broken') + def test_hip_ra_monte_carlo(self): + client = GeophiresMonteCarloClient() + + result: MonteCarloResult = client.get_monte_carlo_result( + MonteCarloRequest( + SimulationProgram.HIP_RA, + self._get_arg_file_path('HIP-example1.txt'), + self._get_arg_file_path('MC_HIP_Settings_file.txt'), + ) + ) + self.assertIsNotNone(result) + self.assertIsNotNone(result.output_file_path) + + with open(result.output_file_path) as f: + result_content = '\n'.join(f.readlines()) + self.assertIn('Electricity', result_content) + + def _get_arg_file_path(self, arg_file): + test_dir: Path = Path(os.path.abspath(__file__)).parent + return Path(test_dir, arg_file).absolute() + + +if __name__ == '__main__': + unittest.main()