from collections import defaultdict
import numpy as np
import pandas as pd
import plotly.graph_objs as go
import plotly.express as px
from pyfemtet._util.df_util import *
from pyfemtet.opt.history import *
from pyfemtet.opt.problem.problem import MAIN_FIDELITY_NAME
from pyfemtet._i18n import Msg, _
__all__ = [
'get_hypervolume_plot',
'get_default_figure',
'get_objective_plot',
]
# ===== i18n =====
def _i18n_not_defined_default():
return _('Not included in the evaluation.',
'評価対象外')
# optimality
_LOCALIZED_OPTIMALITY = _('Optimality', '最適かどうか')
def _i18n_optimality(value, is_single_objective) -> str:
if value is True:
if is_single_objective:
return _('optimal', '最適解')
else:
return _('non dominated', '非劣解')
elif value is False:
if is_single_objective:
return _('not optimal', '最適でない解')
else:
return _('dominated', '劣解')
else:
return _i18n_not_defined_default()
(_color_mapping_single_objective := defaultdict(lambda: '#888888')).update({
_i18n_optimality(True, True): "#007bff",
_i18n_optimality(False, True): "#6c757d",
})
(_color_mapping_multi_objective := defaultdict(lambda: '#888888')).update({
_i18n_optimality(True, False): "#007bff",
_i18n_optimality(False, False): "#6c757d",
})
# feasibility
_LOCALIZED_FEASIBILITY = _('Feasibility', '実行可能性')
def _i18n_feasibility(value) -> str:
if value is True:
return _('feasible', '実行可能')
elif value is False:
return _('infeasible', '実行不可能')
else:
return _i18n_not_defined_default()
(_symbol_mapping := defaultdict(lambda: 'square-open')).update({
_i18n_feasibility(True): 'circle',
_i18n_feasibility(False): 'circle-open',
})
# common
def _i18n_df(df, is_single_objective):
# 元の df を変更しないようにコピー
df = df.copy()
# feasibility 列のデータに対し、
# <翻訳された列名>: <グラフに表示するデータ> に
# 変換した df を作る。
df[_LOCALIZED_FEASIBILITY] = [_i18n_feasibility(v)
for v in df['feasibility']]
# optimality も同様
df[_LOCALIZED_OPTIMALITY] = [_i18n_optimality(v, is_single_objective)
for v in df['optimality']]
return df
# ===== main =====
[docs]
def get_hypervolume_plot(_: History, df: pd.DataFrame) -> go.Figure:
df = _i18n_df(df, is_single_objective=False)
# メインデータを抽出
df = get_partial_df(df, equality_filters=MAIN_FILTER)
# 成功した試行のみを抽出
df = df[df['feasibility']]
# 番号を振り直し
df['trial'] = range(1, len(df) + 1)
# create figure
fig = px.line(
df,
x="trial",
y="hypervolume",
markers=True,
custom_data=['trial'],
)
fig.update_layout(
dict(
title_text=Msg.GRAPH_TITLE_HYPERVOLUME,
# transition_duration=1000, # Causes graph freeze on tab change
xaxis_title=Msg.GRAPH_AXIS_LABEL_TRIAL,
yaxis_title='hypervolume',
xaxis=dict(
tick0=1,
type='category',
)
)
)
return fig
def _get_single_objective_plot(history: History, df: pd.DataFrame):
df: pd.DataFrame = _i18n_df(df, is_single_objective=True)
obj_name = history.obj_names[0]
# 「成功した試行数」を表示するために
# obj が na ではない row の連番を作成
df = (df[~df[obj_name].isna()]).copy()
SUCCEEDED_TRIAL_COLUMN = 'succeeded_trial'
assert SUCCEEDED_TRIAL_COLUMN not in df.columns
df[SUCCEEDED_TRIAL_COLUMN] = range(len(df))
df[SUCCEEDED_TRIAL_COLUMN] += 1
df_main = get_partial_df(df, equality_filters=MAIN_FILTER)
df.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
df_main.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
obj_name = obj_name.replace(' / ', '<BR>/ ')
sub_fidelity_names = history.sub_fidelity_names
obj_names_per_sub_fidelity_list: list[list[str]] = [
history.obj_names for _ in sub_fidelity_names
]
# ===== base figure =====
fig = go.Figure()
# ===== all results of sub-fidelity =====
for (
sub_fidelity_name,
obj_names_per_sub_fidelity,
color
) in zip(
sub_fidelity_names,
obj_names_per_sub_fidelity_list,
px.colors.qualitative.G10[::-1]
):
if sub_fidelity_name == MAIN_FIDELITY_NAME:
continue
assert len(obj_names_per_sub_fidelity) == 1
obj_name_per_sub_fidelity = obj_names_per_sub_fidelity[0]
df_sub = get_partial_df(df, equality_filters=dict(sub_fidelity_name=sub_fidelity_name))
trace = go.Scatter(
x=df_sub[SUCCEEDED_TRIAL_COLUMN],
y=df_sub[obj_name_per_sub_fidelity],
mode='markers',
marker=dict(color=color, symbol='square-open'),
name=sub_fidelity_name,
)
fig.add_trace(trace)
# ===== i 番目が、その時点までで最適かどうか =====
# NOTE: 最後の direction をその最適化全体の direction と見做す
# その時点までの最適な点 index
indices = []
anti_indices = []
objectives = df_main[obj_name].values
direction = df_main[f'{obj_name}_direction'].values[-1]
for i, obj in enumerate(objectives):
obj_halfway = objectives[:i+1]
feasible_obj_halfway = obj_halfway[np.where(~np.isnan(obj_halfway))]
if len(feasible_obj_halfway) == 0:
indices.append(i)
continue
if direction == 'maximize':
if obj == max(feasible_obj_halfway):
indices.append(i)
else:
anti_indices.append(i)
elif direction == 'minimize':
if obj == min(feasible_obj_halfway):
indices.append(i)
else:
anti_indices.append(i)
else:
residuals_halfway = (feasible_obj_halfway - direction) ** 2
if ((obj - direction) ** 2) == min(residuals_halfway):
indices.append(i)
else:
anti_indices.append(i)
# ===== すべての点を灰色で打つ =====
fig.add_trace(
go.Scatter(
x=df_main[SUCCEEDED_TRIAL_COLUMN],
y=df_main[obj_name],
customdata=df_main['trial'].values.reshape((-1, 1)),
mode="markers",
marker=dict(color='#6c757d', size=6),
name=Msg.LEGEND_LABEL_ALL_SOLUTIONS,
)
)
# ===== その時点までの最小の点を青で打つ(上から描く) =====
fig.add_trace(
go.Scatter(
x=df_main[SUCCEEDED_TRIAL_COLUMN].iloc[indices],
y=df_main[obj_name].iloc[indices],
mode="markers+lines",
marker=dict(color='#007bff', size=9),
name=Msg.LEGEND_LABEL_OPTIMAL_SOLUTIONS,
line=dict(width=1, color='#6c757d',),
customdata=df_main['trial'].iloc[indices].values.reshape((-1, 1)),
legendgroup='optimality',
)
)
# ===== その時点までの最小の点から現在までの平行点線を引く =====
if len(indices) > 1:
x = [df_main[SUCCEEDED_TRIAL_COLUMN].iloc[indices].iloc[-1],
df_main[SUCCEEDED_TRIAL_COLUMN].iloc[-1]]
y = [df_main[obj_name].iloc[indices].iloc[-1]] * 2
fig.add_trace(
go.Scatter(
x=x,
y=y,
mode="lines",
line=dict(width=0.5, color='#6c757d', dash='dash'),
showlegend=False,
legendgroup='optimality',
)
)
# ===== direction が float の場合、目標値を描く =====
if len(df_main) > 1:
if isinstance(direction, float):
x = [df_main[SUCCEEDED_TRIAL_COLUMN].iloc[0],
df_main[SUCCEEDED_TRIAL_COLUMN].iloc[-1]]
y = [direction] * 2
fig.add_trace(
go.Scatter(
x=x,
y=y,
mode="lines",
line=dict(width=0.5, color='#FF2400', dash='dash'),
name=Msg.LEGEND_LABEL_OBJECTIVE_TARGET,
)
)
# ===== layout =====
fig.update_layout(
dict(
title_text=Msg.GRAPH_TITLE_SINGLE_OBJECTIVE,
xaxis_title=Msg.GRAPH_AXIS_LABEL_TRIAL,
yaxis_title=obj_name,
xaxis=dict(
tick0=1,
type='category', # 負の数や小数点を表示しない。ただし hover の callback があるのでオフセットしたり文字を入れたりしないこと
)
)
)
return fig
def _get_multi_objective_pairplot(history: History, df: pd.DataFrame):
df = _i18n_df(df, is_single_objective=False)
df_main = get_partial_df(df, equality_filters=MAIN_FILTER)
obj_names = history.obj_names
df.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
df_main.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
obj_names = [o.replace(' / ', '<BR>/ ') for o in obj_names]
sub_fidelity_names = history.sub_fidelity_names
obj_names_per_sub_fidelity_list: list[list[str]] = [
history.obj_names for _ in sub_fidelity_names
]
common_kwargs = dict(
color=_LOCALIZED_OPTIMALITY,
color_discrete_map=_color_mapping_multi_objective,
symbol=_LOCALIZED_FEASIBILITY,
symbol_map=_symbol_mapping,
custom_data=['trial'],
category_orders={
_LOCALIZED_FEASIBILITY: (
_i18n_feasibility(None), # True, False でなければなんでもよい
_i18n_feasibility(False),
_i18n_feasibility(True),
),
_LOCALIZED_OPTIMALITY: (
_i18n_optimality(None, False),
_i18n_optimality(False, False),
_i18n_optimality(True, False),
),
},
)
if len(obj_names) == 2:
# plot main data
fig = px.scatter(
data_frame=df_main,
x=obj_names[0],
y=obj_names[1],
**common_kwargs,
)
# ===== sub-fidelity =====
for (
sub_fidelity_name,
obj_names_per_sub_fidelity,
color
) in zip(
sub_fidelity_names,
obj_names_per_sub_fidelity_list,
px.colors.qualitative.G10[::-1]
):
if sub_fidelity_name == MAIN_FIDELITY_NAME:
continue
assert len(obj_names_per_sub_fidelity) == 2
name1, name2 = obj_names_per_sub_fidelity
df_sub = get_partial_df(df, equality_filters=dict(sub_fidelity_name=sub_fidelity_name))
trace = go.Scatter(
x=df_sub[name1],
y=df_sub[name2],
mode='markers',
marker=dict(color=color, symbol='square-open'),
name=sub_fidelity_name,
)
fig.add_trace(trace)
fig.update_layout(
dict(
xaxis_title=obj_names[0],
yaxis_title=obj_names[1],
)
)
else:
# plot main data
fig = px.scatter_matrix(
data_frame=df_main,
dimensions=obj_names,
**common_kwargs,
)
# ===== sub-fidelity =====
for (
sub_fidelity_name,
obj_names_per_sub_fidelity,
color
) in zip(
sub_fidelity_names,
obj_names_per_sub_fidelity_list,
px.colors.qualitative.G10[::-1]
):
if sub_fidelity_name == MAIN_FIDELITY_NAME:
continue
df_sub = get_partial_df(df, equality_filters=dict(sub_fidelity_name=sub_fidelity_name))
fig.add_trace(
go.Splom(
dimensions=[
{
'label': obj_name,
'values': df_sub[sub_obj_name]
} for obj_name, sub_obj_name in zip(
obj_names, obj_names_per_sub_fidelity
)
],
marker=dict(color=color, symbol='square-open'),
name=sub_fidelity_name,
)
)
fig.update_traces(
patch={'diagonal.visible': False},
showupperhalf=False,
)
fig.update_layout(
dict(
title_text=Msg.GRAPH_TITLE_MULTI_OBJECTIVE,
)
)
return fig
[docs]
def get_objective_plot(history: History, df: pd.DataFrame, obj_names: list[str]) -> go.Figure:
df = _i18n_df(df, is_single_objective=False)
df_main = get_partial_df(df, equality_filters=MAIN_FILTER)
df.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
df_main.columns = [c.replace(' / ', '<BR>/ ') for c in df.columns]
obj_names = [o.replace(' / ', '<BR>/ ') for o in obj_names]
sub_fidelity_names = history.sub_fidelity_names
obj_names_per_sub_fidelity_list: list[list[str]] = [
history.obj_names for _ in sub_fidelity_names
]
common_kwargs = dict(
color=_LOCALIZED_OPTIMALITY,
color_discrete_map=_color_mapping_multi_objective,
symbol=_LOCALIZED_FEASIBILITY,
symbol_map=_symbol_mapping,
custom_data=['trial'],
category_orders={
_LOCALIZED_FEASIBILITY: (
_i18n_feasibility(None), # True, False でなければなんでもよい
_i18n_feasibility(False),
_i18n_feasibility(True),
),
_LOCALIZED_OPTIMALITY: (
_i18n_optimality(None, False),
_i18n_optimality(False, False),
_i18n_optimality(True, False),
),
},
)
if len(obj_names) == 2:
# main plot
fig = px.scatter(
data_frame=df_main,
x=obj_names[0],
y=obj_names[1],
**common_kwargs,
)
# ===== sub-fidelity =====
for sub_fidelity_name, obj_names_per_sub_fidelity, color \
in zip(sub_fidelity_names,
obj_names_per_sub_fidelity_list,
px.colors.qualitative.G10[::-1]
):
if sub_fidelity_name == MAIN_FIDELITY_NAME:
continue
df_sub = get_partial_df(df, equality_filters=dict(sub_fidelity_name=sub_fidelity_name))
sub_name0 = obj_names_per_sub_fidelity[history.obj_names.index(obj_names[0])]
sub_name1 = obj_names_per_sub_fidelity[history.obj_names.index(obj_names[1])]
trace = go.Scatter(
x=df_sub[sub_name0],
y=df_sub[sub_name1],
mode='markers',
marker=dict(color=color, symbol='square-open'),
name=sub_fidelity_name,
)
fig.add_trace(trace)
fig.update_layout(
dict(
xaxis_title=obj_names[0],
yaxis_title=obj_names[1],
)
)
elif len(obj_names) == 3:
# main plot
fig = px.scatter_3d(
df_main,
x=obj_names[0],
y=obj_names[1],
z=obj_names[2],
**common_kwargs,
)
# ===== sub-fidelity =====
for sub_fidelity_name, obj_names_per_sub_fidelity, color \
in zip(sub_fidelity_names,
obj_names_per_sub_fidelity_list,
px.colors.qualitative.G10[::-1]
):
if sub_fidelity_name == MAIN_FIDELITY_NAME:
continue
df_sub = get_partial_df(df, equality_filters=dict(sub_fidelity_name=sub_fidelity_name))
sub_name0 = obj_names_per_sub_fidelity[history.obj_names.index(obj_names[0])]
sub_name1 = obj_names_per_sub_fidelity[history.obj_names.index(obj_names[1])]
sub_name2 = obj_names_per_sub_fidelity[history.obj_names.index(obj_names[2])]
fig.add_trace(
go.Scatter3d(
x=df_sub[sub_name0],
y=df_sub[sub_name1],
z=df_sub[sub_name2],
mode='markers',
marker=dict(color=color, symbol='square-open'),
name=sub_fidelity_name,
)
)
fig.update_layout(
margin=dict(l=0, r=0, b=0, t=30),
scene=dict(aspectmode="cube")
)
fig.update_traces(
marker=dict(
size=3,
),
)
else:
raise Exception
fig.update_layout(
dict(
title_text="Objective plot",
)
)
fig.update_traces(hoverinfo="none", hovertemplate=None)
return fig