Skip to content

Commit 9c9919f

Browse files
added rich HTML output for MC_GEOPHIRES. Also fixed bug for when run from command line
1 parent 07e7eaa commit 9c9919f

File tree

1 file changed

+197
-2
lines changed

1 file changed

+197
-2
lines changed

src/geophires_monte_carlo/MC_GeoPHIRES3.py

Lines changed: 197 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,12 @@
2222
import matplotlib.pyplot as plt
2323
import numpy as np
2424
import pandas as pd
25+
from rich.console import Console
26+
from rich.table import Table
2527

2628
from geophires_monte_carlo.common import _get_logger
29+
from geophires_x.Parameter import OutputParameter
30+
from geophires_x.Parameter import floatParameter
2731
from geophires_x_client import GeophiresInputParameters
2832
from geophires_x_client import GeophiresXClient
2933
from geophires_x_client import GeophiresXResult
@@ -33,6 +37,184 @@
3337
from hip_ra_x import HipRaXClient
3438

3539

40+
def Write_HTML_Output(
41+
html_path: str,
42+
df: pd.DataFrame,
43+
outputs: list,
44+
mins: list,
45+
maxs: list,
46+
medians: list,
47+
averages: list,
48+
means: list,
49+
std: list,
50+
full_names: set,
51+
short_names: set,
52+
) -> None:
53+
"""
54+
Write_HTML_Output - write the results of the Monte Carlo simulation to an HTML file
55+
:param html_path: the path to the HTML file to write
56+
:type html_path: str
57+
:param df: the DataFrame with the results
58+
:type df: pd.DataFrame
59+
:param outputs: the list of output variable names
60+
:type outputs: list
61+
:param mins: the list of minimum values for each output variable
62+
:type mins: list
63+
:param maxs: the list of maximum values for each output variable
64+
:type maxs: list
65+
:param medians: the list of median values for each output variable
66+
:type medians: list
67+
:param averages: the list of average values for each output variable
68+
:type averages: list
69+
:param means: the list of mean values for each output variable
70+
:type means: list
71+
:param std: the list of standard deviation values for each output variable
72+
:type std: list
73+
:param full_names: the list of full names for each output variable
74+
:type full_names: set
75+
:param short_names: the list of short names for each output variable
76+
:type short_names: set
77+
"""
78+
79+
# Build the tables that will hold those results, along with the columns for the input variables
80+
results_table = Table(title='GEOPHIRES/HIR-RA Monte Carlo Results')
81+
results_table.add_column('Iteration #', no_wrap=True, justify='center')
82+
for output in df.axes[1]:
83+
results_table.add_column(output.replace(',', ''), no_wrap=True, justify='center')
84+
85+
statistics_table = Table(title='GEOPHIRES/HIR-RA Monte Carlo Statistics')
86+
statistics_table.add_column('Output Parameter Name', no_wrap=True, justify='center')
87+
statistics_table.add_column('minimum', no_wrap=True, justify='center')
88+
statistics_table.add_column('maximum', no_wrap=True, justify='center')
89+
statistics_table.add_column('median', no_wrap=True, justify='center')
90+
statistics_table.add_column('average', no_wrap=True, justify='center')
91+
statistics_table.add_column('mean', no_wrap=True, justify='center')
92+
statistics_table.add_column('standard deviation', no_wrap=True, justify='center')
93+
94+
# Iterate over the rows of the DataFrame and add them to the results table
95+
for index, row in df.iterrows():
96+
data = row.values[0 : len(outputs)]
97+
98+
# have to deal with the special case where thr last column is actually
99+
# a compound string with multiple columns in it that looks like this:
100+
# ' (Gradient 1:47.219846973456924;Reservoir Temperature:264.7789623351493;...)'
101+
str_to_parse = str(row.values[len(outputs)]).strip().replace('(', '').replace(')', '')
102+
fields = str_to_parse.split(';')
103+
for field in fields:
104+
if len(field) > 0:
105+
key, value = field.split(':')
106+
data = np.append(data, float(value))
107+
108+
results_table.add_row(str(int(index)), *[render_default(d) for d in data])
109+
110+
for i in range(len(outputs)):
111+
statistics_table.add_row(
112+
outputs[i],
113+
render_default(mins[i]),
114+
render_default(maxs[i]),
115+
render_default(medians[i]),
116+
render_default(averages[i]),
117+
render_default(means[i]),
118+
render_default(std[i]),
119+
)
120+
121+
console = Console(style='bold white on black', force_terminal=True, record=True, width=500)
122+
console.print(results_table)
123+
console.print(' ')
124+
console.print(statistics_table)
125+
console.save_html(html_path)
126+
127+
# Write a reference to the image(s) into the HTML file by inserting before the "</body>" tag
128+
# build the string to be inserted first
129+
insert_string = ''
130+
for _ in range(len(full_names)):
131+
name_to_use = short_names.pop()
132+
insert_string = insert_string + f'<img src="{name_to_use}.png" alt="{name_to_use}">\n'
133+
134+
match_string = '</body>'
135+
with open(html_path, 'r+', encoding='UTF-8') as html_file:
136+
contents = html_file.readlines()
137+
if match_string in contents[-1]: # Handle last line to prevent IndexError
138+
pass
139+
else:
140+
for index, line in enumerate(contents):
141+
if match_string in line and insert_string not in contents[index + 1]:
142+
contents.insert(index, insert_string)
143+
break
144+
html_file.seek(0)
145+
html_file.writelines(contents)
146+
147+
148+
def UpgradeSymbologyOfUnits(unit: str) -> str:
149+
"""
150+
UpgradeSymbologyOfUnits is a function that takes a string that represents a unit and replaces the **2 and **3
151+
with the appropriate unicode characters for superscript 2 and 3, and replaces "deg" with the unicode character
152+
for degrees.
153+
:param unit: a string that represents a unit
154+
:return: a string that represents a unit with the appropriate unicode characters for superscript 2 and 3, and
155+
replaces "deg" with the unicode character for degrees.
156+
"""
157+
return unit.replace('**2', '\u00b2').replace('**3', '\u00b3').replace('deg', '\u00b0')
158+
159+
160+
def render_default(p: float, unit: str = '') -> str:
161+
"""
162+
RenderDefault - render a float as a string with 2 decimal places, or in scientific notation if it is greater than
163+
10,000 with the unit appended to it if it is not an empty string (the default)
164+
:param p: the float to render
165+
:type p: float
166+
:param unit: the unit to append to the string
167+
:type unit: str
168+
:return: the string representation of the float
169+
:rtype: str
170+
"""
171+
unit = UpgradeSymbologyOfUnits(unit)
172+
# if the number is greater than 10,000, render it in scientific notation
173+
if p > 10_000:
174+
return f'{p:10.2e} {unit}'.strip()
175+
# otherwise, render it with 2 decimal places
176+
else:
177+
return f'{p:10.2f} {unit}'.strip()
178+
179+
180+
def render_scientific(p: float, unit: str = '') -> str:
181+
"""
182+
RenderScientific - render a float as a string in scientific notation with 2 decimal places
183+
and the unit appended to it if it is not an empty string (the default)
184+
:param p: the float to render
185+
:type p: float
186+
:param unit: the unit to append to the string
187+
:type unit: str
188+
:return: the string representation of the float
189+
:rtype: str
190+
"""
191+
unit = UpgradeSymbologyOfUnits(unit)
192+
return f'{p:10.2e} {unit}'.strip()
193+
194+
195+
def render_Parameter_default(p: floatParameter | OutputParameter) -> str:
196+
"""
197+
RenderDefault - render a float as a string with 2 decimal places, or in scientific notation if it is greater than
198+
10,000 with the unit appended to it if it is not an empty string (the default) by calling the render_default base
199+
function
200+
:param p: the parameter to render
201+
:type p: float
202+
:return: the string representation of the float
203+
"""
204+
return render_default(p.value, p.CurrentUnits.value)
205+
206+
207+
def render_parameter_scientific(p: floatParameter | OutputParameter) -> str:
208+
"""
209+
RenderScientific - render a float as a string in scientific notation with 2 decimal places
210+
and the unit appended to it if it is not an empty string (the default) by calling the render_scientific base function
211+
:param p: the parameter to render
212+
:type p: float
213+
:return: the string representation of the float
214+
"""
215+
return render_scientific(p.value, p.CurrentUnits.value)
216+
217+
36218
def check_and_replace_mean(input_value, args) -> list:
37219
"""
38220
CheckAndReplaceMean - check to see if the user has requested that a value be replaced by a mean value by specifying
@@ -268,7 +450,9 @@ def main(command_line_args=None):
268450
if 'MC_OUTPUT_FILE' in args and args.MC_OUTPUT_FILE is not None
269451
else str(Path(Path(args.Input_file).parent, 'MC_Result.txt').absolute())
270452
)
453+
args.MC_OUTPUT_FILE = output_file
271454
python_path = 'python'
455+
html_path = ''
272456

273457
for line in flist:
274458
clean = line.strip()
@@ -284,6 +468,8 @@ def main(command_line_args=None):
284468
output_file = pair[1]
285469
elif pair[0].startswith('PYTHON_PATH'):
286470
python_path = pair[1]
471+
elif pair[0].startswith('HTML_PATH'):
472+
html_path = pair[1]
287473

288474
# check to see if there is a "#" in an input, if so, use the results file to replace it with the value
289475
for input_value in inputs:
@@ -375,6 +561,8 @@ def main(command_line_args=None):
375561
# write them out
376562
annotations = ''
377563
outputs_result: dict[str, dict] = {}
564+
full_names: set = set()
565+
short_names: set = set()
378566
with open(output_file, 'a') as f:
379567
if iterations != actual_records_count:
380568
f.write(
@@ -408,10 +596,17 @@ def main(command_line_args=None):
408596
f.write(f'bin values (as percentage): {ret[0]!s}\n')
409597
f.write(f'bin edges: {ret[1]!s}\n')
410598
fname = df.columns[i].strip().replace('/', '-')
411-
plt.savefig(Path(Path(output_file).parent, f'{fname}.png'))
412-
599+
save_path = Path(Path(output_file).parent, f'{fname}.png')
600+
if html_path:
601+
save_path = Path(Path(html_path).parent, f'{fname}.png')
602+
plt.savefig(save_path)
603+
full_names.add(save_path)
604+
short_names.add(fname)
413605
annotations = ''
414606

607+
if html_path:
608+
Write_HTML_Output(html_path, df, outputs, mins, maxs, medians, averages, means, std, full_names, short_names)
609+
415610
with open(Path(output_file).with_suffix('.json'), 'w') as json_output_file:
416611
json_output_file.write(json.dumps(outputs_result))
417612
logger.info(f'Wrote JSON results to {json_output_file.name}')

0 commit comments

Comments
 (0)