"""
Code for generating a dashboard for case recorder files.
"""
import argparse
import pathlib
import importlib
import openmdao.api as om
from openmdao.utils.om_warnings import CaseRecorderWarning, issue_warning
try:
import panel as pn
except ModuleNotFoundError:
pn = None
try:
import pandas as pd
except ModuleNotFoundError:
pd = None
try:
from openmdao.utils.gui_testing_utils import get_free_port
except ImportError:
# If get_free_port is unavailable, the default port will be used
def get_free_port():
"""
Return the default port number.
Returns
-------
int
Default port number.
"""
return 5000
def _view_cases_setup_parser(parser):
"""
Set up the subparser for the 'openmdao view_cases' command.
Parameters
----------
parser : argparse subparser
The parser we're adding options to.
"""
parser.add_argument(
"case_recorder_file",
type=str,
help="Name of the case recorder file to view",
)
def _view_cases_cmd(options, user_args):
"""
Run the view_cases command.
Parameters
----------
options : argparse Namespace
Command line options.
user_args : list of str
Args to be passed to the user script.
"""
if not options.case_recorder_file:
raise argparse.ArgumentError("case_recorder_file argument missing")
view_cases(options.case_recorder_file)
# boolean value user interface filter elements. Lets user filter based on true, false, or either
tabulator_editors = {
"flat_indices": {"type": "tickCross", "tristate": True, "indeterminateValue": None},
"cache_linear_solution": {
"type": "tickCross",
"tristate": True,
"indeterminateValue": None,
},
"distributed": {"type": "tickCross", "tristate": True, "indeterminateValue": None},
}
# default options used in all the Tabulators
tabulator_defaults = {
'disabled': True,
'header_filters': True,
'pagination': None,
'selectable': False,
'editors': tabulator_editors,
'sortable': True,
'show_index': False,
'sizing_mode': 'stretch_height',
'theme': 'bootstrap5',
'stylesheets': [":host .tabulator {font-size: 10px;}"]
}
# to have a standard order of the metadata across the table in the dashboard display
metadata_names = [
"name",
"orig",
"alias",
"type",
"size",
"units",
"global_size",
"lower",
"upper",
"parallel_deriv_color",
"cache_linear_solution",
"distributed",
"adder",
"total_adder",
"scaler",
"total_scaler",
"ref",
"ref0",
"linear",
"indices",
"source",
"flat_indices",
"equals",
"parent",
]
def _elide_string(s, max_length):
"""
Shorten a string by removing the middle part and replacing with ...
Parameters
----------
s : str
The string to shorten.
max_length : int
Keep the string less than or equal to this length.
Return
----------
s_short : str
A shortened version of the string.
"""
if len(s) <= max_length:
return s
half_length = (max_length - 3) // 2
return s[:half_length] + '...' + s[-half_length:]
[docs]def view_cases(case_recorder_file, show=True):
"""
View the contents of a case recorder file as a dashboard.
Parameters
----------
case_recorder_file : str
The path to the case recorder file to view.
show : bool
If True, show the dashboard. If False, do not show. Mostly for running tests.
"""
if pn is None:
raise RuntimeError(
"The view_cases function requires the 'panel' package, "
"which can be installed with one of the following commands:\n"
" pip install openmdao[visualization]\n"
" pip install panel"
)
if pd is None:
raise RuntimeError(
"The view_cases function requires the 'pandas' package, "
"which can be installed with one of the following commands:\n"
" pip install openmdao[visualization]\n"
" pip install pandas"
)
cr = om.CaseReader(case_recorder_file)
tabs_list = []
# Summary tab
data = [
["openmdao_version", cr.openmdao_version],
["format_version", cr._format_version],
["sources", cr.list_sources()],
["driver name", cr.problem_metadata['driver']['name']],
]
if 'optimizer' in cr.problem_metadata['driver']['options']:
data.append(["driver optimizer", cr.problem_metadata['driver']['options']['optimizer']])
df = pd.DataFrame(data, columns=["Name", "Value"])
summary_table_pane = pn.widgets.Tabulator(
df,
show_index=False,
selectable=False,
sortable=False,
disabled=True,
)
summary_pane = pn.Column(
"This page is a very high level summary of what is in the case recorder file.",
summary_table_pane,
)
tabs_list.append(("Summary", summary_pane))
# Cases Summary
data = []
cases = cr.get_cases()
df = pd.DataFrame(
columns=[
"Name",
"Source",
"Num Inputs",
"Num Outputs",
"Num Residuals",
"Derivatives",
"Abs Err",
"Rel Err",
]
)
for i, case in enumerate(cases):
df.loc[i] = [
case.name,
case.source,
len(case.inputs) if case.inputs else 0,
len(case.outputs) if case.outputs else 0,
len(case.residuals) if case.residuals else 0,
"Yes" if case.derivatives else "No",
case.abs_err,
case.rel_err,
]
cases_table_pane = pn.widgets.Tabulator(
df,
show_index=False,
selectable=False,
sortable=True,
disabled=True,
)
cases_pane = pn.Column(
"This page lists all the Cases found in the case recorder file.", cases_table_pane
)
tabs_list.append(("Cases", cases_pane))
# Give the user an idea of what is in a case by showing them
# what is are the Case inputs, outputs and residuals if available
sources = cr.list_sources()
for source in sources:
case0_id = cr.list_cases(source)[0]
case0 = cr.get_case(case0_id)
case0_inputs_pane = pn.Column(pn.pane.Markdown("# Inputs"))
if case0.inputs:
df_case0_inputs = pd.DataFrame(list(case0.inputs.keys()),
columns=[f"{len(case0.inputs.keys())} Inputs"])
case0_inputs_table_pane = pn.widgets.Tabulator(df_case0_inputs,
**tabulator_defaults)
else:
case0_inputs_table_pane = pn.pane.Markdown("### There are no inputs in Case 0")
case0_inputs_pane.append(case0_inputs_table_pane)
case0_outputs_pane = pn.Column(pn.pane.Markdown("# Outputs"))
if case0.outputs:
df_case0_outputs = pd.DataFrame(list(case0.outputs.keys()),
columns=[f"{len(case0.outputs.keys())} Outputs"])
case0_outputs_table_pane = pn.widgets.Tabulator(df_case0_outputs,
**tabulator_defaults)
else:
case0_outputs_table_pane = pn.pane.Markdown("### There are no outputs in Case 0")
case0_outputs_pane.append(case0_outputs_table_pane)
case0_residuals_pane = pn.Column(pn.pane.Markdown("# Residuals"))
if case0.residuals:
df_case0_residuals = pd.DataFrame(list(case0.residuals.keys()),
columns=[f"{len(case0.residuals.keys())} Residuals"])
case0_residuals_table_pane = pn.widgets.Tabulator(df_case0_residuals,
**tabulator_defaults)
else:
case0_residuals_table_pane = pn.pane.Markdown("### There are no residuals in Case 0")
case0_residuals_pane.append(case0_residuals_table_pane)
case0_variables_pane = pn.Column(
f"There are {len(case0.inputs)if case0.inputs else 0} inputs,"
f" {len(case0.outputs) if case0.outputs else 0} outputs,"
f" and {len(case0.residuals) if case0.residuals else 0} residuals in Case 0.",
pn.Row(
case0_inputs_pane,
case0_outputs_pane,
case0_residuals_pane,
sizing_mode="stretch_width",
styles={
"display": "flex",
"justify-content": "space-between",
"width": "100%",
},
),
)
tabs_list.append((f"Variables recorded for {source}", case0_variables_pane))
# variables pane
# See if we are missing any metadata names.
metadata_names_in_file = set()
for name, properties in cr.problem_metadata['variables'].items():
if isinstance(properties, dict):
metadata_names_in_file.update(list(properties.keys()))
else:
if name not in ['execution_order',]:
issue_warning(
f"problem_metadata['variables'] has unexpected type"
f" of '{type(properties)}' for '{name}'",
category=CaseRecorderWarning,
)
df_variables = variable_metadata_to_data_frame(cr.problem_metadata['variables'])
variables_table_pane = pn.widgets.Tabulator(df_variables, **tabulator_defaults)
variables_pane = pn.Column(
f"Here is the metadata for the {len(cr.problem_metadata['variables'])}"
" variables in the problem",
variables_table_pane
)
tabs_list.append(("Metadata variables", variables_pane))
# design_vars pane
df_metadata_desvars = variable_metadata_to_data_frame(cr.problem_metadata['design_vars'])
metadata_desvars_table_pane = pn.widgets.Tabulator(
df_metadata_desvars, **tabulator_defaults
)
design_vars_pane = pn.Column(
f"Here is the metadata for the {len(cr.problem_metadata['design_vars'])}"
" design variables in the problem",
metadata_desvars_table_pane
)
tabs_list.append(("Metadata design_vars", design_vars_pane))
# responses pane
df_responses = variable_metadata_to_data_frame(cr.problem_metadata['responses'])
responses_table_pane = pn.widgets.Tabulator(df_responses, **tabulator_defaults)
responses_pane = pn.Column(
f"Here is the metadata for the {len(cr.problem_metadata['responses'])}"
" responses in the problem",
responses_table_pane,
)
tabs_list.append(("Metadata responses", responses_pane))
# Execution order pane
if 'execution_order' in cr.problem_metadata['variables'].keys():
# A hack to get around the fact that Panel doesn't do column sizing
# correctly. It only seems to look at the first 40 lines to see what the max size is.
# But making the column heading the length of the longest string is a close
# approximation to what is needed
execution_order = cr.problem_metadata['variables']['execution_order']
max_string_length = len(max(execution_order, key=len))
padded_column_name = "Systems".center(max_string_length, '_')
df_execution_order = pd.DataFrame(execution_order, columns=[padded_column_name])
execution_order_table_pane = pn.widgets.Tabulator(df_execution_order, **tabulator_defaults)
execution_order_pane = pn.Column(
"This page lists the execution order of the systems",
execution_order_table_pane
)
tabs_list.append(("Execution Order", execution_order_pane))
# abs2prom pane
df_abs2prom_inputs = pd.DataFrame(
list(cr.problem_metadata["abs2prom"]["input"].items()),
columns=["Absolute Name", "Promoted Name"],
)
abs2prom_inputs_table_pane = pn.widgets.Tabulator(
df_abs2prom_inputs, **tabulator_defaults
)
df_abs2prom_outputs = pd.DataFrame(
list(cr.problem_metadata["abs2prom"]["output"].items()),
columns=["Absolute Name", "Promoted Name"],
)
abs2prom_outputs_table_pane = pn.widgets.Tabulator(
df_abs2prom_outputs, **tabulator_defaults
)
abs2prom_pane = pn.Column(
f"There are {len(cr.problem_metadata['abs2prom']['input'])} inputs"
f" and {len(cr.problem_metadata['abs2prom']['output'])} outputs in the metadata abs2prom",
pn.pane.Markdown("# Inputs"),
abs2prom_inputs_table_pane,
pn.pane.Markdown("# Outputs"),
abs2prom_outputs_table_pane,
)
tabs_list.append(("Metadata abs2prom", abs2prom_pane))
# connections_list pane
df_connections_list = pd.DataFrame(
cr.problem_metadata["connections_list"], columns=["src", "tgt"]
)
df_connections_list.columns = ["Source", "Target"]
df_connections_table_pane = pn.widgets.Tabulator(df_connections_list, **tabulator_defaults)
connections_list_pane = pn.Column(
f"There are {len(cr.problem_metadata['connections_list'])}"
" items in the metadata connections_list",
df_connections_table_pane,
)
tabs_list.append(("Metadata connections_list", connections_list_pane))
# declare_partials_list pane
df_declare_partials_list = pd.DataFrame(
[item.split(" > ") for item in cr.problem_metadata["declare_partials_list"]],
columns=["Of the Function", "With Respect To"],
)
declare_partials_list_table_pane = pn.widgets.Tabulator(
df_declare_partials_list, **tabulator_defaults
)
declare_partials_list_pane = pn.Column(
f"There are {len(cr.problem_metadata['declare_partials_list'])}"
" items in the metadata declare_partials_list",
declare_partials_list_table_pane,
)
tabs_list.append(("Metadata declare_partials_list", declare_partials_list_pane))
tabs = pn.Tabs(*tabs_list, stylesheets=["assets/view_case_recorder_styles.css"])
template = pn.template.FastListTemplate(
title=f"Dashboard for case recorder file '{case_recorder_file}'",
main=[tabs],
accent_base_color="black",
header_background="rgb(0, 212, 169)",
background_color="white",
theme=pn.theme.DefaultTheme,
theme_toggle=False,
main_layout=None,
)
port = 0
home_dir = "."
assets_dir = pathlib.Path(
importlib.util.find_spec("openmdao").origin
).parent.joinpath("recorders/assets/")
if show:
if port == 0:
port = get_free_port()
server = pn.serve(
template,
port=port,
address="localhost",
websocket_origin=f"localhost:{port}",
show=True,
threaded=False,
static_dirs={
"home": home_dir,
"assets": assets_dir,
},
)
server.stop()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
_view_cases_setup_parser(parser)
args = parser.parse_args()
_view_cases_cmd(args, None)