From 85dfb78a35b6d722cf045e37121777b48e4b492a Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Thu, 7 Jul 2022 09:05:20 +0200 Subject: [PATCH 1/9] Add modal issue - add function to stop computation if any alarm is raised - add modal triggered when any alarm is raised - fix logger level and add --logging_level to app arguments --- grid2game/VizServer.py | 150 +++++++++++++++--------- grid2game/_utils/_temporal_callbacks.py | 17 ++- grid2game/_utils/_temporal_layout.py | 33 ++++-- grid2game/app.py | 6 + grid2game/envs/env.py | 73 +++++++++--- 5 files changed, 198 insertions(+), 81 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index b9c87fe..2d1c9fb 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -14,12 +14,12 @@ import dash_bootstrap_components as dbc from dash import html, dcc -from grid2game._utils import (add_callbacks_temporal, - setupLayout_temporal, - add_callbacks, +from grid2game._utils import (add_callbacks_temporal, + setupLayout_temporal, + add_callbacks, setupLayout, - add_callbacks_action_search, - setupLayout_action_search, + add_callbacks_action_search, + setupLayout_action_search, ) from grid2game.envs import Env from grid2game.plot import PlotGrids, PlotTemporalSeries @@ -82,6 +82,20 @@ def __init__(self, os.path.join(os.path.dirname(__file__), "assets") ) + # create the dash app + self.my_app = dash.Dash(__name__, + server=server if server is not None else True, + meta_tags=meta_tags, + assets_folder=assets_dir, + external_stylesheets=external_stylesheets, + external_scripts=external_scripts) + + # Configure logging after dash initialization. + # Otherwise dash is resetting logging level to INFO + + if not logging_level and build_args.logging_level: + logging_level = build_args.logging_level + if logger is None: import logging self.logger = logging.getLogger(__name__) @@ -94,19 +108,14 @@ def __init__(self, if logging_level is not None: fh.setLevel(logging_level) ch.setLevel(logging_level) + self.logger.setLevel(logging_level) self.logger.addHandler(fh) self.logger.addHandler(ch) else: self.logger = logger.getChild("VizServer") - # create the dash app - self.my_app = dash.Dash(__name__, - server=server if server is not None else True, - meta_tags=meta_tags, - assets_folder=assets_dir, - external_stylesheets=external_stylesheets, - external_scripts=external_scripts) self.logger.info("Dash app initialized") + # self.app.config.suppress_callback_exceptions = True # create the grid2op related things @@ -187,37 +196,37 @@ def __init__(self, self.plot_grids.init_figs(self.env.obs, self.env.sim_obs) self.real_time = self.plot_grids.figure_rt self.forecast = self.plot_grids.figure_forecat - + # initialize the layout self._layout_temporal = html.Div(setupLayout_temporal(self), id="all_temporal") self._layout_temporal_tab = dcc.Tab(label='Temporal view', value=f'tab-temporal-view', children=self._layout_temporal) - + self._layout_action_search = html.Div(setupLayout_action_search(self), id="all_action_search") self._layout_action_search_tab = dcc.Tab(label='Explore actions', value='tab-explore-action', children=self._layout_action_search) - + tmp_ = setupLayout(self, self._layout_temporal_tab, self._layout_action_search_tab) - + self.my_app.layout = tmp_ - + add_callbacks_temporal(self.my_app, self) add_callbacks_action_search(self.my_app, self) add_callbacks(self.my_app, self) - + self.logger.info("Viz server initialized") # last node id (to not plot twice the same stuff to gain time) self._last_node_id = -1 # last action taken - self._last_action = "assistant" + self._last_action = "assistant" self._do_display_action = True self._dropdown_value = "assistant" @@ -254,11 +263,11 @@ def run_server(self, debug=False): def change_nb_step_go_fast(self, nb_step_go_fast): if nb_step_go_fast is None: - return dash.no_update, + return dash.no_update, nb = int(nb_step_go_fast) self.nb_step_gofast = nb - return f"+ {self.nb_step_gofast}", + return f"+ {self.nb_step_gofast}", def unit_clicked(self, line_unit, line_side, load_unit, gen_unit, stor_unit, trigger_rt_graph, trigger_for_graph): @@ -296,22 +305,24 @@ def unit_clicked(self, line_unit, line_side, load_unit, gen_unit, stor_unit, def _reset_action_to_assistant_if_not_prev(self): if self._last_action != "prev" : self._next_action_is_assistant() - + # handle the interaction with the grid2op environment - def handle_act_on_env(self, - step_butt, - simulate_butt, - back_butt, - reset_butt, - go_butt, - gofast_clicks, - until_game_over, - untilgo_butt, - self_loop, - state_trigger_rt, - state_trigger_for, - state_trigger_self_loop, - timer): + def handle_act_on_env( + self, + step_butt, + simulate_butt, + back_butt, + reset_butt, + go_butt, + gofast_clicks, + until_game_over, + untilgo_butt, + self_loop, + state_trigger_rt, + state_trigger_for, + state_trigger_self_loop, + timer + ): """ dash do not make "synch" callbacks (two callbacks can be called at the same time), however, grid2op environments are not "thread safe": accessing them from different "thread" @@ -344,6 +355,7 @@ def handle_act_on_env(self, self._go_till_go_button_shape = "btn btn-secondary" change_graph_title = dash.no_update update_progress_bar = 1 + check_issue = dash.no_update # now register the next computation to do, based on the button triggerd if button_id == "step-button": @@ -351,11 +363,13 @@ def handle_act_on_env(self, self.env.next_computation = "step" self.env.next_computation_kwargs = {} self.need_update_figures = False + check_issue = 1 elif button_id == "go_till_game_over-button": self.env.start_computation() self.env.next_computation = "step_end" self.env.next_computation_kwargs = {} self.need_update_figures = True + check_issue = 1 elif button_id == "reset-button": self.env.start_computation() self.env.next_computation = "reset" @@ -378,6 +392,7 @@ def handle_act_on_env(self, self.env.start_computation() self.env.next_computation = "step_rec_fast" self.env.next_computation_kwargs = {"nb_step_gofast": self.nb_step_gofast} + check_issue = 1 elif button_id == "go-button": self.go_clicks += 1 if self.go_clicks % 2: @@ -390,6 +405,7 @@ def handle_act_on_env(self, self.env.start_computation() self._button_shape = "btn btn-secondary" self._gofast_button_shape = "btn btn-secondary" + check_issue = 1 self.env.next_computation = "step_rec" self.env.next_computation_kwargs = {} self.need_update_figures = False @@ -434,7 +450,7 @@ def handle_act_on_env(self, self._go_till_go_button_shape = "btn btn-secondary" self._gofast_button_shape = "btn btn-secondary" self._button_shape = "btn btn-secondary" - + return [display_new_state, self._button_shape, self._button_shape, @@ -446,7 +462,8 @@ def handle_act_on_env(self, i_am_computing_state, i_am_computing_state, change_graph_title, - update_progress_bar] + update_progress_bar, + check_issue] def _wait_for_computing_over(self): i = 0 @@ -458,6 +475,29 @@ def _wait_for_computing_over(self): # in this case the user should probably call reset another time ! raise dash.exceptions.PreventUpdate + def check_issue(self, n1, n_clicks): + # make sure the environment has nothing to compute + while self.env.needs_compute(): + time.sleep(0.1) + + issues = self.env._current_issues + if issues: + len_issues = len(issues) + if len_issues == 1: + issue_text = f"There is {len_issues} issue: " + else: + issue_text = f"There are {len_issues} issues: " + for issue in issues: + issue_text += f"{issue}, " + # Replace last ', ' by '.' to end the sentence + issue_text = '.'.join(issue_text.rsplit(', ', 1)) + is_open = True + else: + issue_text = dash.no_update + is_open = False + + return [is_open, issue_text] + def change_graph_title(self, change_graph_title): # make sure that the environment has done computing self._wait_for_computing_over() @@ -483,7 +523,7 @@ def computation_wrapper(self, display_new_state, recompute_rt_from_timeline): # simulate a "state" of the application that depends on the computation if not self.env.is_computing(): self.env.heavy_compute() - + if self.env.is_computing() and display_new_state != type(self).GO_MODE: # environment is computing I do not update anything raise dash.exceptions.PreventUpdate @@ -513,7 +553,7 @@ def update_rt_fig(self, env_act): trigger_for_graph = 1 else: raise dash.exceptions.PreventUpdate - + if trigger_rt_graph == 1: self.fig_timeline = self.env.get_timeline_figure() @@ -661,7 +701,7 @@ def _next_action_is_assistant(self): self._last_action = "assistant" self._do_display_action = True self._dropdown_value = "assistant" - + def display_action_fun(self, which_action_button, do_display, @@ -712,7 +752,7 @@ def display_action_fun(self, # i should not display the action res = [f"{self.env.current_action}", dropdown_value, update_substation_layout_clicked_from_sub] return res - + # i need to display the action # self._last_action = "manual" # dropdown_value = "manual" @@ -748,7 +788,7 @@ def display_action_fun(self, if not is_modif: raise dash.exceptions.PreventUpdate - + # TODO optim here to save that if not needed because nothing has changed res = [f"{self.env.current_action}", self._dropdown_value, update_substation_layout_clicked_from_sub] return res @@ -821,7 +861,7 @@ def display_click_data(self, button_id == "go-button" or button_id == "gofast-button" or\ button_id == "back-button": # i never clicked on simulate, step, go, gofast or back - do_display_action = 0 + do_display_action = 0 self._last_sub_id = None else: # I clicked on the graph of the grid @@ -980,7 +1020,7 @@ def timeline_set_time(self, time_line_graph_clicked): def tab_content_display(self, tab): res = [self._layout_temporal] - + if tab == 'tab-temporal-view': self.need_update_figures = True return [self._layout_temporal] @@ -991,13 +1031,13 @@ def tab_content_display(self, tab): msg_ = f"Unknown tab {tab}" self.logger.error(msg_) return res - + def _aux_tab_as_retrieve_updated_figs(self): progress_pct = 100. * self._last_step / self._last_max_step progress_label = f"{self._last_step} / {self._last_max_step}" self.fig_timeline = self.env.get_timeline_figure() self.update_obs_fig() - + pbar_value = progress_pct pbar_label = progress_label pbar_color = self._progress_color @@ -1006,7 +1046,7 @@ def _aux_tab_as_retrieve_updated_figs(self): fig_rt = self.real_time return (pbar_value, pbar_label, pbar_color, fig_timeline, dt_label, fig_rt) - + def main_action_search(self, refresh_button, explore_butt_pressed, @@ -1017,9 +1057,9 @@ def main_action_search(self, raise dash.exceptions.PreventUpdate else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] - + something_clicked = True - + # TODO button color here too ! i_am_computing_state = {'display': 'block'} pbar_value = dash.no_update @@ -1029,25 +1069,25 @@ def main_action_search(self, dt_label = dash.no_update fig_rt = dash.no_update start_computation = 1 - + if button_id == "refresh-button_as": # (pbar_value, pbar_label, pbar_color, fig_timeline, # dt_label, fig_rt) = self._aux_tab_as_retrieve_updated_figs() start_computation = dash.no_update # hack for it to resynch everything self.need_update_figures = True - elif button_id == "explore-button_as": + elif button_id == "explore-button_as": self.env.next_computation = "explore" self.need_update_figures = True self.env.start_computation() else: something_clicked = False - + if not self.env.needs_compute(): # don't start the computation if not needed i_am_computing_state = {'display': 'none'} # deactivate the "i am computing button" start_computation = dash.no_update # I am NOT computing I DO update the graphs - + if not self.env.needs_compute() and self.need_update_figures and not something_clicked: # in this case, this should be the last call to this function after the "explore" # function is finished @@ -1058,7 +1098,7 @@ def main_action_search(self, (pbar_value, pbar_label, pbar_color, fig_timeline, dt_label, fig_rt) = self._aux_tab_as_retrieve_updated_figs() - + return [start_computation, pbar_value, pbar_label, diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index 5203481..a3f3fdc 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -112,7 +112,8 @@ def add_callbacks(dash_app, viz_server): dash.dependencies.Output("is_computing_left", "style"), dash.dependencies.Output("is_computing_right", "style"), dash.dependencies.Output("change_graph_title", "n_clicks"), - dash.dependencies.Output("update_progress_bar_from_act", "n_clicks") + dash.dependencies.Output("update_progress_bar_from_act", "n_clicks"), + dash.dependencies.Output("check_issue", "n_clicks"), ], [dash.dependencies.Input("step-button", "n_clicks"), dash.dependencies.Input("simulate-button", "n_clicks"), @@ -123,11 +124,11 @@ def add_callbacks(dash_app, viz_server): dash.dependencies.Input("go_till_game_over-button", "n_clicks"), dash.dependencies.Input("untilgo_butt_call_act_on_env", "value"), dash.dependencies.Input("selfloop_call_act_on_env", "value"), - dash.dependencies.Input("timer", "n_intervals") + dash.dependencies.Input("timer", "n_intervals"), ], [dash.dependencies.State("act_on_env_trigger_rt", "n_clicks"), dash.dependencies.State("act_on_env_trigger_for", "n_clicks"), - dash.dependencies.State("act_on_env_call_selfloop", "value") + dash.dependencies.State("act_on_env_call_selfloop", "value"), ] )(viz_server.handle_act_on_env) @@ -252,3 +253,13 @@ def add_callbacks(dash_app, viz_server): # set the seed dash_app.callback([dash.dependencies.Output("set_seed_dummy_output", "n_clicks")], [dash.dependencies.Input("set_seed", "value")])(viz_server.set_seed) + + # trigger the modal issue + dash_app.callback( + [ + dash.dependencies.Output("modal_issue", "is_open"), + dash.dependencies.Output("modal_issue_text", "children") + ], + [dash.dependencies.Input("check_issue", "n_clicks")], + [dash.dependencies.State("modal_issue", "is_open")] + )(viz_server.check_issue) \ No newline at end of file diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index f59aa92..524d273 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -29,7 +29,7 @@ def setupLayout(viz_server): html.Div([# html.P("Chronics: ", style={"marginRight": 5, "marginLeft": 5}), html.Div([dcc.Dropdown(id="chronic_names", placeholder="Select a chronic", - options=[{"value": el, "label": el} + options=[{"value": el, "label": el} for el in viz_server.env.list_chronics()]) ], id="chronics_dropdown", @@ -41,7 +41,7 @@ def setupLayout(viz_server): dcc.Input(id="set_seed", type="number", placeholder="Select a seed", - ), + ), ], id="seed_selector") ], @@ -71,7 +71,7 @@ def setupLayout(viz_server): id="nb_step_go_fast", type="number", placeholder="steps", - ) + ) go_fast = html.Label(children =f"+ {viz_server.nb_step_gofast}", id="gofast-button", n_clicks=0, @@ -266,7 +266,7 @@ def setupLayout(viz_server): ) save_experiment = html.Div(id='save_expe_box', - children=[ + children=[ html.Div(children=[ dcc.Input(placeholder=save_txt, id="save_expe", @@ -538,7 +538,7 @@ def setupLayout(viz_server): children=[current_action], style={'width': '39%'} ) - + # combine both interaction_and_action = html.Div([action_widget_title, html.Div([layout_click, @@ -682,6 +682,7 @@ def setupLayout(viz_server): ) # triggering the update of the figures + check_issue = html.Label("", id="check_issue", n_clicks=0) act_on_env_trigger_rt = html.Label("", id="act_on_env_trigger_rt", n_clicks=0) act_on_env_trigger_for = html.Label("", id="act_on_env_trigger_for", n_clicks=0) clear_assistant_path = html.Label("", id="clear_assistant_path", n_clicks=0) @@ -711,7 +712,8 @@ def setupLayout(viz_server): chronic_names_dummy_output, set_seed_dummy_output, update_substation_layout_clicked_from_sub, update_substation_layout_clicked_from_grid, trigger_rt_extra_info, trigger_for_extra_info, - update_progress_bar_from_act, update_progress_bar_from_figs + update_progress_bar_from_act, update_progress_bar_from_figs, + check_issue ], id="hidden_buttons_for_callbacks", style={'display': 'none'}) @@ -721,6 +723,22 @@ def setupLayout(viz_server): interval=500. # in ms ) + modal_issue = dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("There is an issue"), + close_button=True + ), + dbc.ModalBody(dbc.Label("", id='modal_issue_text')), + dbc.ModalFooter( + dbc.Button("Show more", id="show_more_issue", className="ml-auto") + ), + ], + id="modal_issue", + size="lg", + is_open=False, + ) + # Final page layout_css = "container-fluid h-100 d-md-flex d-xl-flex flex-md-column flex-xl-column" layout = html.Div(id="grid2game", @@ -740,7 +758,8 @@ def setupLayout(viz_server): temporal_graphs, interval_object, hidden_interactions, - timer_callbacks + timer_callbacks, + modal_issue ]) return layout diff --git a/grid2game/app.py b/grid2game/app.py index 7cb44d7..50c0c99 100755 --- a/grid2game/app.py +++ b/grid2game/app.py @@ -54,6 +54,12 @@ def cli(): action="store_true", default=False, help="INTERNAL: do not use (inform that the app is running on the heroku server)") + # DEBUG + parser.add_argument( + "--logging_level", required=False, default=20, type=int, + help="Python Logging Levels: CRITICAL=50, ERROR=40, WARNING=30, INFO=20 (default), DEBUG=10" + ) + return parser.parse_args() diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index bee34a0..f4a9474 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -23,7 +23,6 @@ # TODO: logger here bkClass = PandaPowerBackend - from grid2game.agents import load_assistant from grid2game.envs.computeWrapper import ComputeWrapper from grid2game.tree import EnvTree @@ -75,6 +74,7 @@ def __init__(self, self._sim_done = None self._sim_info = None self._current_assistant_action = None + self._current_issues = None # define variables self._should_display = True @@ -93,7 +93,7 @@ def __init__(self, # to control which action will be done when self.next_computation = None self.next_computation_kwargs = {} - + # actions to explore self.all_topo_actions = None @@ -167,12 +167,15 @@ def do_computation(self): return self.seed(**self.next_computation_kwargs) elif self.next_computation == "step": res = self.step(**self.next_computation_kwargs) + obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() + if self._stop_if_issue(obs): + self.logger.info("step_end: An alarm is raised, I stop") self.stop_computation() # this is a "one time" call return res elif self.next_computation == "step_rec": # this is the "go" button res = self.step() obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() - if self._stop_if_alarm(obs): + if self._stop_if_issue(obs): # I stop the computation if the agent sends an alarm self.logger.info("step_rec: An alarm is raised, I stop") self.stop_computation() @@ -184,7 +187,7 @@ def do_computation(self): res = self.step() obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() # print(f"do_computation: {self._assistant_action.raise_alarm}") - if self._stop_if_alarm(obs): + if self._stop_if_issue(obs): self.logger.info("step_rec_fast: An alarm is raised, I stop") break self.stop_computation() # this is a "one time" call @@ -196,7 +199,7 @@ def do_computation(self): while not done: res = self.step() obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() - if self._stop_if_alarm(obs): + if self._stop_if_issue(obs): self.logger.info("step_end: An alarm is raised, I stop") break self.stop_computation() # this is a "one time" call @@ -238,7 +241,7 @@ def explore(self): if self.all_topo_actions is None: self.all_topo_actions = self.glop_env.action_space.get_all_unitary_line_change(self.glop_env.action_space) self.all_topo_actions += self.glop_env.action_space.get_all_unitary_topologies_set(self.glop_env.action_space) - + obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() res = [] for act in self.all_topo_actions: @@ -246,7 +249,7 @@ def explore(self): sim_reward = sim_obs.rho.max() if not sim_done else 1000. res.append((act, sim_reward)) res.sort(key=lambda x: x[1]) - + init_node = self.env_tree.current_node till_the_end = False for act, rew in res[:5]: @@ -254,17 +257,55 @@ def explore(self): if not till_the_end: till_the_end = self._donothing_until_end() self.env_tree.go_to_node(init_node) - + def _donothing_until_end(self): obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() while not done: obs, reward, done, info = self.step(self.glop_env.action_space()) return obs.current_step == obs.max_step - + + def _get_current_action(self): + res = None + current_id = self.env_tree.current_node.id + for el in self.env_tree.current_node.father._act_to_sons: + if el.son.id == current_id: + res = el.action + return res + def _stop_if_alarm(self, obs): - if self.do_stop_if_alarm: - if np.any(obs.time_since_last_alarm == 0): - return True + if np.any(obs.time_since_last_alarm == 0): + self.logger.info("Assistant raised an alarm") + return True + return False + + def _stop_if_action(self, act): + if act.can_affect_something(): + self.logger.info("The current action has a chance to change the grid") + return True + return False + + def _stop_if_bad_kpi(self, obs): + # Check if overload + if obs.rho.max() >= 1.0: + self.logger.info("Overload") + return True + return False + + def _stop_if_issue(self, obs): + issues = [] + self._current_issues = None + act = self._get_current_action() + self.logger.debug(act) + self.logger.debug(type(act)) + if self._stop_if_alarm(obs): + issues.append("Assistant raised an alarm") + if self._stop_if_action(act): + issues.append("The current action has a chance to change the grid") + if self._stop_if_bad_kpi(obs): + issues.append("Overload") + if len(issues) > 0: + self._current_issues = issues + return True return False @property @@ -369,7 +410,7 @@ def step(self, action=None): self._sim_reward = self.glop_env.reward_range[0] self._sim_info = {} self._sim_obs.set_game_over(self.glop_env) - # print(f"step: {np.any(self._assistant_action.raise_alarm)}") + # print(f"step: {np.any(self._assistant_action.raise_alarm)}") return obs, reward, done, info def choose_next_assistant_action(self): @@ -399,7 +440,7 @@ def back(self): def reset(self, chronics_id=None, seed=None): if chronics_id is not None: - + chron = self.glop_env.chronics_handler if hasattr(chron, "available_chronics"): # "proxy" for "grid2op >= 1.6.5" self.glop_env.set_id(chronics_id) @@ -412,7 +453,7 @@ def reset(self, chronics_id=None, seed=None): def init_state(self): self.env_tree.clear() - obs = self.glop_env.reset() + obs = self.glop_env.reset() self.env_tree.root(assistant=self.assistant, obs=obs, env=self.glop_env) self._current_action = self.glop_env.action_space() @@ -470,7 +511,7 @@ def handle_click_timeline(self, time_line_graph_clcked) -> int: self.is_computing() res = self.env_tree.move_from_click(time_line_graph_clcked) self._current_action = copy.deepcopy(self.env_tree.get_last_action()) - + obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() if not done: self.choose_next_assistant_action() From 6262705a2fcacdedffc64d7f2a323c29c417a2bc Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Fri, 8 Jul 2022 12:30:20 +0200 Subject: [PATCH 2/9] Add recommandations table --- grid2game/VizServer.py | 73 ++++- grid2game/_utils/_temporal_callbacks.py | 339 +++++++++++++----------- grid2game/_utils/_temporal_layout.py | 94 ++++++- grid2game/envs/computeWrapper.py | 10 + grid2game/envs/env.py | 24 ++ setup.py | 1 + 6 files changed, 383 insertions(+), 158 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index 2d1c9fb..ef655c6 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -9,10 +9,13 @@ import os import sys import time +import pandas as pd +import copy import dash import dash_bootstrap_components as dbc from dash import html, dcc +from dash.dash_table import DataTable from grid2game._utils import (add_callbacks_temporal, setupLayout_temporal, @@ -475,7 +478,15 @@ def _wait_for_computing_over(self): # in this case the user should probably call reset another time ! raise dash.exceptions.PreventUpdate - def check_issue(self, n1, n_clicks): + def check_issue( + self, + n_check_issue, + n_show_more, + is_open + ): + # Close modal when clicking on Show more + if n_show_more: + return [not is_open, ""] # make sure the environment has nothing to compute while self.env.needs_compute(): time.sleep(0.1) @@ -1109,3 +1120,63 @@ def main_action_search(self, 1, i_am_computing_state, i_am_computing_state] + + def show_recommandations(self, n_show_more, n_close, is_open): + + if n_close: + return [dash.no_update, not is_open] + + if n_show_more: + + self.env.start_recommandations_computation() + + agent_name = self.format_path(os.path.abspath(self.assistant_path)) + agent_action = self.env._assistant_action + variant_env_tree = copy.deepcopy(self.env.env_tree) + current_node = variant_env_tree.current_node + obs, reward, done, info = current_node.get_obs_rewar_done_info() + overloads = (obs.rho[obs.rho > 1.0]).tolist() + max_rho = obs.rho.max() + holding_steps = self.env.nb_steps_from_node_until_end(current_node, variant_env_tree) + + self.env.stop_recommandations_computation() + + d = { + 'Agent': [agent_name], + 'Overload': [str(overloads)], + 'Max Overload': [max_rho], + 'Holding Time': [holding_steps] + } + + recommandations = pd.DataFrame(data=d) + return [ + DataTable( + id="recommandations_table", + columns=[ + {"name": i, "id": i} for i in recommandations.columns + ], + data=recommandations.to_dict("records"), + style_table={"overflowX": "auto"}, + row_selectable="single", + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + }, + tooltip_data=[ + { + column: {"value": str(value), "type": "markdown"} + for column, value in row.items() + } + for row in recommandations.to_dict("rows") + ], + ), + not is_open, + ] + return [dash.no_update, is_open] + + def loading_recommandations_table(self, n_clicks): + time.sleep(0.1) + while self.env.is_computing_recommandations(): + time.sleep(0.1) + return [""] diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index a3f3fdc..f4e17d5 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -7,259 +7,286 @@ # This file is part of Grid2Game, Grid2Game a gamified platform to interact with grid2op environments. import dash +from dash.dependencies import Input, Output, State def add_callbacks(dash_app, viz_server): - dash_app.callback([dash.dependencies.Output("gofast-button", "children")], - [dash.dependencies.Input("nb_step_go_fast", "value")] + dash_app.callback([Output("gofast-button", "children")], + [Input("nb_step_go_fast", "value")] )(viz_server.change_nb_step_go_fast) # handle the press to one of the button to change the units - dash_app.callback([dash.dependencies.Output("unit_trigger_rt_graph", "n_clicks"), - dash.dependencies.Output("unit_trigger_for_graph", "n_clicks"), + dash_app.callback([Output("unit_trigger_rt_graph", "n_clicks"), + Output("unit_trigger_for_graph", "n_clicks"), ], - [dash.dependencies.Input("line-info-dropdown", "value"), - dash.dependencies.Input("line-side-dropdown", "value"), - dash.dependencies.Input("load-info-dropdown", "value"), - dash.dependencies.Input("gen-info-dropdown", "value"), - dash.dependencies.Input("stor-info-dropdown", "value") + [Input("line-info-dropdown", "value"), + Input("line-side-dropdown", "value"), + Input("load-info-dropdown", "value"), + Input("gen-info-dropdown", "value"), + Input("stor-info-dropdown", "value") ], - [dash.dependencies.State("unit_trigger_rt_graph", "n_clicks"), - dash.dependencies.State("unit_trigger_for_graph", "n_clicks") + [State("unit_trigger_rt_graph", "n_clicks"), + State("unit_trigger_for_graph", "n_clicks") ] )(viz_server.unit_clicked) # handle the interaction with the graph - dash_app.callback([dash.dependencies.Output("do_display_action", "value"), - dash.dependencies.Output("generator_clicked", "style"), - dash.dependencies.Output("gen-redisp-curtail", "children"), - dash.dependencies.Output("gen-id-hidden", "children"), - dash.dependencies.Output("gen-id-clicked", "children"), - dash.dependencies.Output("gen-dispatch", "min"), - dash.dependencies.Output("gen-dispatch", "max"), - dash.dependencies.Output("gen-dispatch", "value"), - dash.dependencies.Output("gen_p", "children"), - dash.dependencies.Output("target_disp", "children"), - dash.dependencies.Output("actual_disp", "children"), + dash_app.callback([Output("do_display_action", "value"), + Output("generator_clicked", "style"), + Output("gen-redisp-curtail", "children"), + Output("gen-id-hidden", "children"), + Output("gen-id-clicked", "children"), + Output("gen-dispatch", "min"), + Output("gen-dispatch", "max"), + Output("gen-dispatch", "value"), + Output("gen_p", "children"), + Output("target_disp", "children"), + Output("actual_disp", "children"), - dash.dependencies.Output("storage_clicked", "style"), - dash.dependencies.Output("storage-id-hidden", "children"), - dash.dependencies.Output("stor-id-clicked", "children"), - dash.dependencies.Output("storage-power-input", "min"), - dash.dependencies.Output("storage-power-input", "max"), - dash.dependencies.Output("storage-power-input", "value"), - dash.dependencies.Output("storage_p", "children"), - dash.dependencies.Output("storage_energy", "children"), + Output("storage_clicked", "style"), + Output("storage-id-hidden", "children"), + Output("stor-id-clicked", "children"), + Output("storage-power-input", "min"), + Output("storage-power-input", "max"), + Output("storage-power-input", "value"), + Output("storage_p", "children"), + Output("storage_energy", "children"), - dash.dependencies.Output("line_clicked", "style"), - dash.dependencies.Output("line-id-hidden", "children"), - dash.dependencies.Output("line-id-clicked", "children"), - dash.dependencies.Output("line-status-input", "value"), - dash.dependencies.Output("line_flow", "children"), + Output("line_clicked", "style"), + Output("line-id-hidden", "children"), + Output("line-id-clicked", "children"), + Output("line-status-input", "value"), + Output("line_flow", "children"), - dash.dependencies.Output("sub_clicked", "style"), - dash.dependencies.Output("sub-id-hidden", "children"), - dash.dependencies.Output("sub-id-clicked", "children"), - # dash.dependencies.Output("graph_clicked_sub", "figure"), - dash.dependencies.Output("update_substation_layout_clicked_from_grid", "n_clicks"), + Output("sub_clicked", "style"), + Output("sub-id-hidden", "children"), + Output("sub-id-clicked", "children"), + # Output("graph_clicked_sub", "figure"), + Output("update_substation_layout_clicked_from_grid", "n_clicks"), ], - [dash.dependencies.Input('real-time-graph', 'clickData'), - dash.dependencies.Input("back-button", "n_clicks"), - dash.dependencies.Input("step-button", "n_clicks"), - dash.dependencies.Input("simulate-button", "n_clicks"), - dash.dependencies.Input("go-button", "n_clicks"), - dash.dependencies.Input("gofast-button", "n_clicks"), - dash.dependencies.Input("go_till_game_over-button", "n_clicks"), + [Input('real-time-graph', 'clickData'), + Input("back-button", "n_clicks"), + Input("step-button", "n_clicks"), + Input("simulate-button", "n_clicks"), + Input("go-button", "n_clicks"), + Input("gofast-button", "n_clicks"), + Input("go_till_game_over-button", "n_clicks"), ])(viz_server.display_click_data) # handle display of the action, if needed - dash_app.callback([dash.dependencies.Output("current_action", "children"), - dash.dependencies.Output("which_action_button", "value"), - dash.dependencies.Output("update_substation_layout_clicked_from_sub", "n_clicks") + dash_app.callback([Output("current_action", "children"), + Output("which_action_button", "value"), + Output("update_substation_layout_clicked_from_sub", "n_clicks") ], - [dash.dependencies.Input("which_action_button", "value"), - dash.dependencies.Input("do_display_action", "value"), - dash.dependencies.Input("gen-redisp-curtail", "children"), - dash.dependencies.Input("gen-id-hidden", "children"), - dash.dependencies.Input('gen-dispatch', "value"), - dash.dependencies.Input("storage-id-hidden", "children"), - dash.dependencies.Input('storage-power-input', "value"), - dash.dependencies.Input("line-id-hidden", "children"), - dash.dependencies.Input('line-status-input', "value"), - dash.dependencies.Input('sub-id-hidden', "children"), - dash.dependencies.Input("graph_clicked_sub", "clickData") + [Input("which_action_button", "value"), + Input("do_display_action", "value"), + Input("gen-redisp-curtail", "children"), + Input("gen-id-hidden", "children"), + Input('gen-dispatch', "value"), + Input("storage-id-hidden", "children"), + Input('storage-power-input', "value"), + Input("line-id-hidden", "children"), + Input('line-status-input', "value"), + Input('sub-id-hidden', "children"), + Input("graph_clicked_sub", "clickData") ])(viz_server.display_action_fun) # plot the substation that changes when we click - dash_app.callback([dash.dependencies.Output("graph_clicked_sub", "figure")], - [dash.dependencies.Input("update_substation_layout_clicked_from_sub", "n_clicks"), - dash.dependencies.Input("update_substation_layout_clicked_from_grid", "n_clicks"), + dash_app.callback([Output("graph_clicked_sub", "figure")], + [Input("update_substation_layout_clicked_from_sub", "n_clicks"), + Input("update_substation_layout_clicked_from_grid", "n_clicks"), ])(viz_server.display_grid_substation) # handle the interaction with self.env, that should be done all in one function, otherwise # there are concurrency issues dash_app.callback([ # trigger the computation if needed - dash.dependencies.Output("trigger_computation", "value"), + Output("trigger_computation", "value"), # update the button color / shape / etc. if needed - dash.dependencies.Output("step-button", "className"), - dash.dependencies.Output("simulate-button", "className"), - dash.dependencies.Output("back-button", "className"), - dash.dependencies.Output("reset-button", "className"), - dash.dependencies.Output("go-button", "className"), - dash.dependencies.Output("gofast-button", "className"), - dash.dependencies.Output("go_till_game_over-button", "className"), - dash.dependencies.Output("is_computing_left", "style"), - dash.dependencies.Output("is_computing_right", "style"), - dash.dependencies.Output("change_graph_title", "n_clicks"), - dash.dependencies.Output("update_progress_bar_from_act", "n_clicks"), - dash.dependencies.Output("check_issue", "n_clicks"), + Output("step-button", "className"), + Output("simulate-button", "className"), + Output("back-button", "className"), + Output("reset-button", "className"), + Output("go-button", "className"), + Output("gofast-button", "className"), + Output("go_till_game_over-button", "className"), + Output("is_computing_left", "style"), + Output("is_computing_right", "style"), + Output("change_graph_title", "n_clicks"), + Output("update_progress_bar_from_act", "n_clicks"), + Output("check_issue", "n_clicks"), ], - [dash.dependencies.Input("step-button", "n_clicks"), - dash.dependencies.Input("simulate-button", "n_clicks"), - dash.dependencies.Input("back-button", "n_clicks"), - dash.dependencies.Input("reset-button", "n_clicks"), - dash.dependencies.Input("go-button", "n_clicks"), - dash.dependencies.Input("gofast-button", "n_clicks"), - dash.dependencies.Input("go_till_game_over-button", "n_clicks"), - dash.dependencies.Input("untilgo_butt_call_act_on_env", "value"), - dash.dependencies.Input("selfloop_call_act_on_env", "value"), - dash.dependencies.Input("timer", "n_intervals"), + [Input("step-button", "n_clicks"), + Input("simulate-button", "n_clicks"), + Input("back-button", "n_clicks"), + Input("reset-button", "n_clicks"), + Input("go-button", "n_clicks"), + Input("gofast-button", "n_clicks"), + Input("go_till_game_over-button", "n_clicks"), + Input("untilgo_butt_call_act_on_env", "value"), + Input("selfloop_call_act_on_env", "value"), + Input("timer", "n_intervals"), ], - [dash.dependencies.State("act_on_env_trigger_rt", "n_clicks"), - dash.dependencies.State("act_on_env_trigger_for", "n_clicks"), - dash.dependencies.State("act_on_env_call_selfloop", "value"), + [State("act_on_env_trigger_rt", "n_clicks"), + State("act_on_env_trigger_for", "n_clicks"), + State("act_on_env_call_selfloop", "value"), ] )(viz_server.handle_act_on_env) - dash_app.callback([dash.dependencies.Output("act_on_env_trigger_rt", "n_clicks"), - dash.dependencies.Output("act_on_env_trigger_for", "n_clicks") + dash_app.callback([Output("act_on_env_trigger_rt", "n_clicks"), + Output("act_on_env_trigger_for", "n_clicks") ], - [dash.dependencies.Input("trigger_computation", "value"), - dash.dependencies.Input("recompute_rt_from_timeline", "n_clicks") + [Input("trigger_computation", "value"), + Input("recompute_rt_from_timeline", "n_clicks") ] )(viz_server.computation_wrapper) # handle triggers: refresh of the figures for real time (graph part) - dash_app.callback([dash.dependencies.Output("figrt_trigger_temporal_figs", "n_clicks"), - dash.dependencies.Output("figrt_trigger_rt_graph", "n_clicks"), - dash.dependencies.Output("figrt_trigger_for_graph", "n_clicks"), - dash.dependencies.Output("timeline_graph", "figure"), - dash.dependencies.Output("update_progress_bar_from_figs", "n_clicks") + dash_app.callback([Output("figrt_trigger_temporal_figs", "n_clicks"), + Output("figrt_trigger_rt_graph", "n_clicks"), + Output("figrt_trigger_for_graph", "n_clicks"), + Output("timeline_graph", "figure"), + Output("update_progress_bar_from_figs", "n_clicks") ], - [dash.dependencies.Input("act_on_env_trigger_rt", "n_clicks")], + [Input("act_on_env_trigger_rt", "n_clicks")], [] )(viz_server.update_rt_fig) dash_app.callback([ - dash.dependencies.Output("scenario_progression", "value"), - dash.dependencies.Output("scenario_progression", "label"), - dash.dependencies.Output("scenario_progression", "color"), + Output("scenario_progression", "value"), + Output("scenario_progression", "label"), + Output("scenario_progression", "color"), ],[ - dash.dependencies.Input("update_progress_bar_from_act", "n_clicks"), - dash.dependencies.Input("update_progress_bar_from_figs", "n_clicks"), + Input("update_progress_bar_from_act", "n_clicks"), + Input("update_progress_bar_from_figs", "n_clicks"), ])(viz_server.update_progress_bar) # handle triggers: refresh of the figures for the forecast - dash_app.callback([dash.dependencies.Output("figfor_trigger_for_graph", "n_clicks")], - [dash.dependencies.Input("act_on_env_trigger_for", "n_clicks")], + dash_app.callback([Output("figfor_trigger_for_graph", "n_clicks")], + [Input("act_on_env_trigger_for", "n_clicks")], [] )(viz_server.update_simulated_fig) # final graph display # handle triggers: refresh the figures (temporal series part) - dash_app.callback([dash.dependencies.Output("graph_gen_load", "figure"), - dash.dependencies.Output("graph_flow_cap", "figure"), + dash_app.callback([Output("graph_gen_load", "figure"), + Output("graph_flow_cap", "figure"), ], - [dash.dependencies.Input("figrt_trigger_temporal_figs", "n_clicks"), - dash.dependencies.Input("showtempo_trigger_rt_graph", "n_clicks") + [Input("figrt_trigger_temporal_figs", "n_clicks"), + Input("showtempo_trigger_rt_graph", "n_clicks") ], )(viz_server.update_temporal_figs) - # dash_app.callback([dash.dependencies.Output('temporal_graphs', "style"), - # dash.dependencies.Output("showtempo_trigger_rt_graph", "n_clicks") + # dash_app.callback([Output('temporal_graphs', "style"), + # Output("showtempo_trigger_rt_graph", "n_clicks") # ], - # [dash.dependencies.Input('show-temporal-graph', "value")] + # [Input('show-temporal-graph', "value")] # )(self.show_hide_tempo_graph) # handle final graph of the real time grid - dash_app.callback([dash.dependencies.Output("real-time-graph", "figure"), - dash.dependencies.Output("rt_date_time", "children"), - dash.dependencies.Output("trigger_rt_extra_info", "n_clicks") + dash_app.callback([Output("real-time-graph", "figure"), + Output("rt_date_time", "children"), + Output("trigger_rt_extra_info", "n_clicks") ], - [dash.dependencies.Input("figrt_trigger_rt_graph", "n_clicks"), - dash.dependencies.Input("unit_trigger_rt_graph", "n_clicks"), + [Input("figrt_trigger_rt_graph", "n_clicks"), + Input("unit_trigger_rt_graph", "n_clicks"), ] )(viz_server.update_rt_graph_figs) # handle final graph for the forecast grid - dash_app.callback([dash.dependencies.Output("simulated-graph", "figure"), - dash.dependencies.Output("forecast_date_time", "children"), - dash.dependencies.Output("trigger_for_extra_info", "n_clicks") + dash_app.callback([Output("simulated-graph", "figure"), + Output("forecast_date_time", "children"), + Output("trigger_for_extra_info", "n_clicks") ], - [dash.dependencies.Input("figrt_trigger_for_graph", "n_clicks"), - dash.dependencies.Input("figfor_trigger_for_graph", "n_clicks"), - dash.dependencies.Input("unit_trigger_for_graph", "n_clicks"), + [Input("figrt_trigger_for_graph", "n_clicks"), + Input("figfor_trigger_for_graph", "n_clicks"), + Input("unit_trigger_for_graph", "n_clicks"), ] )(viz_server.update_for_graph_figs) if viz_server._app_heroku is False: # this is deactivated on heroku at the moment ! # load the assistant - dash_app.callback([dash.dependencies.Output("current_assistant_path", "children"), - dash.dependencies.Output("clear_assistant_path", "n_clicks"), - dash.dependencies.Output("loading_assistant_output", "children"), + dash_app.callback([Output("current_assistant_path", "children"), + Output("clear_assistant_path", "n_clicks"), + Output("loading_assistant_output", "children"), ], - [dash.dependencies.Input("load_assistant_button", "n_clicks") + [Input("load_assistant_button", "n_clicks") ], - [dash.dependencies.State("select_assistant", "value")] + [State("select_assistant", "value")] )(viz_server.load_assistant) - dash_app.callback([dash.dependencies.Output("select_assistant", "value")], - [dash.dependencies.Input("clear_assistant_path", "n_clicks")] + dash_app.callback([Output("select_assistant", "value")], + [Input("clear_assistant_path", "n_clicks")] )(viz_server.clear_loading) # save the current experiment - dash_app.callback([dash.dependencies.Output("current_save_path", "children"), - dash.dependencies.Output("loading_save_output", "children"), + dash_app.callback([Output("current_save_path", "children"), + Output("loading_save_output", "children"), ], - [dash.dependencies.Input("save_expe_button", "n_clicks")], - [dash.dependencies.State("save_expe", "value")] + [Input("save_expe_button", "n_clicks")], + [State("save_expe", "value")] )(viz_server.save_expe) # tell if action was illegal - dash_app.callback([dash.dependencies.Output("forecast_extra_info", "style")], - [dash.dependencies.Input("trigger_for_extra_info", "n_clicks")] + dash_app.callback([Output("forecast_extra_info", "style")], + [Input("trigger_for_extra_info", "n_clicks")] )(viz_server.tell_illegal_for) - dash_app.callback([dash.dependencies.Output("rt_extra_info", "style")], - [dash.dependencies.Input("trigger_rt_extra_info", "n_clicks")] + dash_app.callback([Output("rt_extra_info", "style")], + [Input("trigger_rt_extra_info", "n_clicks")] )(viz_server.tell_illegal_rt) # callback for the timeline - dash_app.callback([dash.dependencies.Output("recompute_rt_from_timeline", "n_clicks")], - [dash.dependencies.Input('timeline_graph', 'clickData')])(viz_server.timeline_set_time) + dash_app.callback([Output("recompute_rt_from_timeline", "n_clicks")], + [Input('timeline_graph', 'clickData')])(viz_server.timeline_set_time) # callbacks when the "reset" button is pressed - dash_app.callback([dash.dependencies.Output("scenario_id_title", "children"), - dash.dependencies.Output("scenario_seed_title", "children"), - dash.dependencies.Output("chronic_names", "value"), - dash.dependencies.Output("set_seed", "value"), + dash_app.callback([Output("scenario_id_title", "children"), + Output("scenario_seed_title", "children"), + Output("chronic_names", "value"), + Output("set_seed", "value"), ], - [dash.dependencies.Input("change_graph_title", "n_clicks")])(viz_server.change_graph_title) + [Input("change_graph_title", "n_clicks")])(viz_server.change_graph_title) # set the chronics - dash_app.callback([dash.dependencies.Output("chronic_names_dummy_output", "n_clicks")], - [dash.dependencies.Input("chronic_names", "value")])(viz_server.set_chronics) + dash_app.callback([Output("chronic_names_dummy_output", "n_clicks")], + [Input("chronic_names", "value")])(viz_server.set_chronics) # set the seed - dash_app.callback([dash.dependencies.Output("set_seed_dummy_output", "n_clicks")], - [dash.dependencies.Input("set_seed", "value")])(viz_server.set_seed) + dash_app.callback([Output("set_seed_dummy_output", "n_clicks")], + [Input("set_seed", "value")])(viz_server.set_seed) # trigger the modal issue dash_app.callback( [ - dash.dependencies.Output("modal_issue", "is_open"), - dash.dependencies.Output("modal_issue_text", "children") + Output("modal_issue", "is_open"), + Output("modal_issue_text", "children"), ], - [dash.dependencies.Input("check_issue", "n_clicks")], - [dash.dependencies.State("modal_issue", "is_open")] - )(viz_server.check_issue) \ No newline at end of file + [ + Input("check_issue", "n_clicks"), + # Close modal when clicking on Show more + Input("show_more_issue", "n_clicks"), + ], + [ + State("modal_issue", "is_open"), + ] + )(viz_server.check_issue) + + # show the recommandations table + dash_app.callback( + [ + Output("recommandations_div", "children"), + Output("recommandations_container", "is_open"), + ], + [ + Input("show_more_issue", "n_clicks"), + Input("close_recommandations_button", "n_clicks"), + ], + [ + State("recommandations_container", "is_open"), + ] + )(viz_server.show_recommandations) + + dash_app.callback( + [Output("loading_recommandations_output", "children")], + [Input("show_more_issue", "n_clicks")] + )(viz_server.loading_recommandations_table) diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 524d273..99f54c1 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -731,7 +731,7 @@ def setupLayout(viz_server): ), dbc.ModalBody(dbc.Label("", id='modal_issue_text')), dbc.ModalFooter( - dbc.Button("Show more", id="show_more_issue", className="ml-auto") + dbc.Button("Show more", id="show_more_issue", className="ml-auto", n_clicks=0) ), ], id="modal_issue", @@ -739,6 +739,95 @@ def setupLayout(viz_server): is_open=False, ) + recommandations_container = dbc.Collapse( + html.Div( + children=[ + dbc.Button( + "Close", + id="close_recommandations_button", + className="ml-auto", + n_clicks=0, + style={ + "display": "flex" + } + ), + dbc.Container([ + dbc.Label( + 'Recommandations', + style={ + "padding": "5px" + } + ), + html.Div( + children=[ + html.Div( + # Recommandations DataTable is loaded here from the callback + children = [], + id="recommandations_div", + style={ + "padding": "10px" + } + ), + html.Div( + children=[ + dbc.Button("Show details", id="show_recommandation_details_button", className="ml-auto"), + dbc.Button("To Knowledge Base", id="add_to_knowledge_base_button", className="ml-auto"), + dbc.Button("Add to variant trees", id="add_to_variant_trees_button", className="ml-auto"), + dbc.Button("Apply", id="apply_recommandation", className="ml-auto"), + ], + style={ + "alignItems":"left", + "display": "flex", + "padding": "2px", + "justifyContent": "space-between", + "width": "50%" + } + ) + ], + style={ + 'borderWidth': '1px', + 'borderStyle': 'solid', + 'borderRadius': '5px', + 'width': '100%', + "padding": "10px" + } + ), + html.Div( + children=[ + dbc.Button("Explore", id="show_details", className="ml-auto") + ], + style={ + "alignItems": "end", + "display": "flex", + "padding": "4px" + } + ) + ]) + ], + style={ + 'borderWidth': '1px', + 'borderStyle': 'solid', + 'borderRadius': '5px', + 'width': '80%', + "alignItems":"center", + "marginLeft": "auto", + "marginRight": "auto" + } + ), + id="recommandations_container", + is_open=False, + style={ + "paddingBottom": "20px" + } + ) + + loading_recommandations = dcc.Loading( + id="loading_recommandations", + type="default", + children=html.Div(id="loading_recommandations_output"), + ) + + # Final page layout_css = "container-fluid h-100 d-md-flex d-xl-flex flex-md-column flex-xl-column" layout = html.Div(id="grid2game", @@ -750,6 +839,9 @@ def setupLayout(viz_server): html.Br(), progress_bar_for_scenario, html.Br(), + loading_recommandations, + recommandations_container, + html.Br(), # state_row, # the two graphs of the grid graph_col, # the two graphs of the grid html.Br(), diff --git a/grid2game/envs/computeWrapper.py b/grid2game/envs/computeWrapper.py index 119df0b..6812469 100644 --- a/grid2game/envs/computeWrapper.py +++ b/grid2game/envs/computeWrapper.py @@ -17,6 +17,7 @@ class ComputeWrapper(ABC): def __init__(self): self.__is_computing = False # whether or not something is being computed self.__computation_started = False # whether or not something needs to be computed + self.__is_computing_recommandations = False # whether or not variant trees are being computed self.count = 0 @abstractmethod @@ -33,6 +34,15 @@ def heavy_compute(self): self.__is_computing = False return res + def is_computing_recommandations(self): + return self.__is_computing_recommandations + + def start_recommandations_computation(self): + self.__is_computing_recommandations = True + + def stop_recommandations_computation(self): + self.__is_computing_recommandations = False + def is_computing(self): return self.__is_computing diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index f4a9474..ca36e37 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -527,3 +527,27 @@ def handle_click_timeline(self, time_line_graph_clcked) -> int: def get_current_action_list(self): """return the list of actions from the current point in the tree up to the root""" return self.env_tree.get_current_action_list() + + def step_variant(self, env_tree, action=None): + # Step used to compute holding time of recommandations + obs, reward, done, info = env_tree.current_node.get_obs_rewar_done_info() + if done: + self.stop_computation() + obs, reward, done, info = env_tree.current_node.get_obs_rewar_done_info() + return obs, reward, done, info + + # Do nothing action + action = self.glop_env.action_space() + + env_tree.make_step(assistant=self.assistant, chosen_action=action) + obs, reward, done, info = env_tree.current_node.get_obs_rewar_done_info() + + return obs, reward, done, info + + def nb_steps_from_node_until_end(self, node, env_tree): + obs, reward, done, info = node.get_obs_rewar_done_info() + steps = 0 + while not done: + obs, reward, done, info = self.step_variant(env_tree=env_tree) + steps += 1 + return steps diff --git a/setup.py b/setup.py index 3413403..7623601 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,7 @@ "grid2op>=1.6.4", "imageio", "orjson", + "pandas", # "igraph" # "graphviz", # "networkx" From aa55fc65c1a487f37d65bdcabdf4d6ca91b151e3 Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Mon, 11 Jul 2022 11:18:37 +0200 Subject: [PATCH 3/9] Add to variant trees --- grid2game/VizServer.py | 213 ++++++++++++++++++------ grid2game/_utils/_temporal_callbacks.py | 40 ++++- grid2game/_utils/_temporal_layout.py | 56 ++++++- grid2game/envs/env.py | 2 - 4 files changed, 248 insertions(+), 63 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index ef655c6..70f98f2 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -12,10 +12,12 @@ import pandas as pd import copy + import dash import dash_bootstrap_components as dbc from dash import html, dcc from dash.dash_table import DataTable +from dash.exceptions import PreventUpdate from grid2game._utils import (add_callbacks_temporal, setupLayout_temporal, @@ -233,6 +235,10 @@ def __init__(self, self._do_display_action = True self._dropdown_value = "assistant" + # Variant trees + self.variant_env_trees = [] + self._variant_tree_added = False + def _make_glop_env_config(self, build_args): g2op_config = {} cont_ = True @@ -343,7 +349,7 @@ def handle_act_on_env( ctx = dash.callback_context if not ctx.triggered: # no click have been made yet - raise dash.exceptions.PreventUpdate + raise PreventUpdate else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] @@ -379,6 +385,7 @@ def handle_act_on_env( self.env.next_computation_kwargs = {"chronics_id": self.chronics_id, "seed": self.seed} self.need_update_figures = False change_graph_title = 1 + check_issue = 0 self._next_action_is_assistant() elif button_id == "simulate-button": self.env.start_computation() @@ -390,6 +397,7 @@ def handle_act_on_env( self.env.next_computation = "back" self.env.next_computation_kwargs = {} self.need_update_figures = False + check_issue = 0 elif button_id == "gofast-button": # this button is off now ! self.env.start_computation() @@ -476,7 +484,7 @@ def _wait_for_computing_over(self): if i >= 20: # in this case, the environment has not finished running for 2s, I stop here # in this case the user should probably call reset another time ! - raise dash.exceptions.PreventUpdate + raise PreventUpdate def check_issue( self, @@ -485,14 +493,14 @@ def check_issue( is_open ): # Close modal when clicking on Show more - if n_show_more: - return [not is_open, ""] + if n_show_more and is_open: + return [False, ""] # make sure the environment has nothing to compute while self.env.needs_compute(): time.sleep(0.1) issues = self.env._current_issues - if issues: + if n_check_issue and issues and not is_open: len_issues = len(issues) if len_issues == 1: issue_text = f"There is {len_issues} issue: " @@ -502,12 +510,10 @@ def check_issue( issue_text += f"{issue}, " # Replace last ', ' by '.' to end the sentence issue_text = '.'.join(issue_text.rsplit(', ', 1)) - is_open = True + return [True, issue_text] else: - issue_text = dash.no_update - is_open = False + raise PreventUpdate - return [is_open, issue_text] def change_graph_title(self, change_graph_title): # make sure that the environment has done computing @@ -530,28 +536,38 @@ def set_seed(self, seed): self.seed = int(seed) return [1] - def computation_wrapper(self, display_new_state, recompute_rt_from_timeline): - # simulate a "state" of the application that depends on the computation - if not self.env.is_computing(): - self.env.heavy_compute() - - if self.env.is_computing() and display_new_state != type(self).GO_MODE: - # environment is computing I do not update anything - raise dash.exceptions.PreventUpdate - - if display_new_state == 1 or display_new_state == type(self).GO_MODE: + def computation_wrapper( + self, + display_new_state, + recompute_rt_from_timeline, + variant_tree_added, + ): + if self._variant_tree_added: + self._variant_tree_added = False trigger_rt = 1 - trigger_for = 1 - - # update the state only if needed - if self.env.get_current_node_id() == self._last_node_id: - # the state did not change, i do not update anything - raise dash.exceptions.PreventUpdate - else: - self._last_node_id = self.env.get_current_node_id() - else: - trigger_rt = dash.no_update trigger_for = dash.no_update + else: + # simulate a "state" of the application that depends on the computation + if not self.env.is_computing(): + self.env.heavy_compute() + + if self.env.is_computing() and display_new_state != type(self).GO_MODE: + # environment is computing I do not update anything + raise PreventUpdate + + if display_new_state == 1 or display_new_state == type(self).GO_MODE: + trigger_rt = 1 + trigger_for = 1 + + # update the state only if needed + if self.env.get_current_node_id() == self._last_node_id: + # the state did not change, i do not update anything + raise PreventUpdate + else: + self._last_node_id = self.env.get_current_node_id() + else: + trigger_rt = dash.no_update + trigger_for = dash.no_update return [trigger_rt, trigger_for] # handle the layout @@ -563,7 +579,7 @@ def update_rt_fig(self, env_act): trigger_rt_graph = 1 trigger_for_graph = 1 else: - raise dash.exceptions.PreventUpdate + raise PreventUpdate if trigger_rt_graph == 1: self.fig_timeline = self.env.get_timeline_figure() @@ -578,7 +594,7 @@ def update_rt_fig(self, env_act): def update_progress_bar(self, from_act, from_figs): """update the progress bar""" # if from_act is None and from_figs is None: - # raise dash.exceptions.PreventUpdate + # raise PreventUpdate if self.env.env_tree.current_node is None: # A reset has just been called and the grid2op env is not reset yet self._progress_color = "primary" @@ -624,13 +640,13 @@ def update_simulated_fig(self, env_act): self.plot_grids.update_forecat(self.env.sim_obs, self.env) self.for_datetime = f"{self.env.sim_obs.get_time_stamp():%Y-%m-%d %H:%M}" else: - raise dash.exceptions.PreventUpdate + raise PreventUpdate return [trigger_for_graph] def show_temporal_graphs(self, show_temporal_graph): """handles the action that displays (or not) the time series graphs""" if (show_temporal_graph is None or show_temporal_graph.empty()): - raise dash.exceptions.PreventUpdate + raise PreventUpdate return [1] # define the style of the temporal graph, whether is how it or not @@ -646,7 +662,7 @@ def show_hide_tempo_graph(self, do_i_show): def update_temporal_figs(self, figrt_trigger, showhide_trigger): if (figrt_trigger is None or figrt_trigger == 0) and \ (showhide_trigger is None or showhide_trigger == 0): - raise dash.exceptions.PreventUpdate + raise PreventUpdate self.fig_load_gen, self.fig_line_cap = self.plot_temporal.update_trace(self.env, self.env.env_tree) return [self.fig_load_gen, self.fig_line_cap] @@ -654,7 +670,7 @@ def update_rt_graph_figs(self, figrt_trigger, unit_trigger): if (figrt_trigger is None or figrt_trigger == 0) and \ (unit_trigger is None or unit_trigger == 0): # nothing really triggered this call - raise dash.exceptions.PreventUpdate + raise PreventUpdate self._wait_for_computing_over() if self.env.env_tree.current_node.prev_action_is_illegal: is_illegal = 1 @@ -671,7 +687,7 @@ def update_for_graph_figs(self, figrt_trigger, figfor_trigger, unit_trigger): (figfor_trigger is None or figfor_trigger == 0) and \ (unit_trigger is None or unit_trigger == 0): # nothing really triggered this call - raise dash.exceptions.PreventUpdate + raise PreventUpdate self._wait_for_computing_over() if self.env.is_assistant_illegal(): is_illegal = 1 @@ -798,7 +814,7 @@ def display_action_fun(self, self.env._current_action.set_bus = [(obj_id, new_bus)] if not is_modif: - raise dash.exceptions.PreventUpdate + raise PreventUpdate # TODO optim here to save that if not needed because nothing has changed res = [f"{self.env.current_action}", self._dropdown_value, update_substation_layout_clicked_from_sub] @@ -807,14 +823,14 @@ def display_action_fun(self, def display_grid_substation(self, update_substation_layout_clicked_from_sub, update_substation_layout_clicked_from_grid): """update the figure of the substation (when zoomed in)""" if update_substation_layout_clicked_from_sub != 1 and update_substation_layout_clicked_from_grid != 1: - raise dash.exceptions.PreventUpdate + raise PreventUpdate if update_substation_layout_clicked_from_sub is None and update_substation_layout_clicked_from_grid is None: - raise dash.exceptions.PreventUpdate + raise PreventUpdate # update "in real time" the topology of the substation (https://github.com/BDonnot/grid2game/issues/36) if self._last_sub_id is None: self.logger.error("display_click_data: Unable to update the substatin plot: no know last substation id") - raise dash.exceptions.PreventUpdate + raise PreventUpdate sub_res = self.plot_grids.update_sub_figure(self.env._current_action, self._last_sub_id) return [sub_res[-1]] @@ -903,7 +919,7 @@ def display_click_data(self, self._last_sub_id = obj_id update_substation_layout_clicked_from_grid = 1 else: - raise dash.exceptions.PreventUpdate + raise PreventUpdate # self._next_action_is_manual() return [do_display_action, style_gen_input, gen_redisp_curtail, gen_id_clicked, *gen_res, @@ -925,7 +941,7 @@ def load_assistant(self, trigger_load, assistant_path): """loads an assistant and display the right things""" loader_state = "" if assistant_path is None: - raise dash.exceptions.PreventUpdate + raise PreventUpdate self.assistant_path = assistant_path.rstrip().lstrip() try: properly_loaded = self.env.load_assistant(self.assistant_path) @@ -944,7 +960,7 @@ def load_assistant(self, trigger_load, assistant_path): def clear_loading(self, need_clearing): """once an assistant has been """ if need_clearing == 0: - raise dash.exceptions.PreventUpdate + raise PreventUpdate return [""] def save_expe(self, button, save_expe_path): @@ -959,7 +975,7 @@ def save_expe(self, button, save_expe_path): ctx = dash.callback_context if not ctx.triggered: # no click have been made yet - raise dash.exceptions.PreventUpdate + raise PreventUpdate if self.env.is_computing(): # cannot save while an experiment is running @@ -1019,11 +1035,11 @@ def save_expe(self, button, save_expe_path): def timeline_set_time(self, time_line_graph_clicked): if self.env.is_computing(): # nothing is updated if i am doing a computation - raise dash.exceptions.PreventUpdate + raise PreventUpdate if time_line_graph_clicked is None: # I did no click on anything - raise dash.exceptions.PreventUpdate + raise PreventUpdate res = self.env.handle_click_timeline(time_line_graph_clicked) self.need_update_figures = True # hack to have the progress bar properly recomputed @@ -1065,7 +1081,7 @@ def main_action_search(self, ctx = dash.callback_context if not ctx.triggered: # no click have been made yet - raise dash.exceptions.PreventUpdate + raise PreventUpdate else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] @@ -1121,30 +1137,51 @@ def main_action_search(self, i_am_computing_state, i_am_computing_state] - def show_recommandations(self, n_show_more, n_close, is_open): + def show_recommandations( + self, + n_show_more, + n_close, + is_open + ): if n_close: - return [dash.no_update, not is_open] + self.env._current_issues = None + return [dash.no_update, not is_open, dash.no_update] if n_show_more: self.env.start_recommandations_computation() + self.variant_env_trees = [] + agent_name = self.format_path(os.path.abspath(self.assistant_path)) agent_action = self.env._assistant_action + # TODO: handle the tree in a better way variant_env_tree = copy.deepcopy(self.env.env_tree) + current_node = variant_env_tree.current_node + obs, reward, done, info = current_node.get_obs_rewar_done_info() overloads = (obs.rho[obs.rho > 1.0]).tolist() max_rho = obs.rho.max() holding_steps = self.env.nb_steps_from_node_until_end(current_node, variant_env_tree) + # Go back to current_node + variant_env_tree.go_to_node(current_node) + + self.variant_env_trees.append( + { + "agent_name": agent_name, + "variant_env_tree": variant_env_tree, + } + ) + self.env.stop_recommandations_computation() d = { 'Agent': [agent_name], 'Overload': [str(overloads)], - 'Max Overload': [max_rho], + 'Max Rho': [max_rho], 'Holding Time': [holding_steps] } @@ -1172,11 +1209,85 @@ def show_recommandations(self, n_show_more, n_close, is_open): ], ), not is_open, + recommandations.to_dict(), ] - return [dash.no_update, is_open] + + raise PreventUpdate def loading_recommandations_table(self, n_clicks): time.sleep(0.1) while self.env.is_computing_recommandations(): time.sleep(0.1) return [""] + + def select_recommandation( + self, + selected_rows, + recommandations, + ): + if not selected_rows: + raise PreventUpdate + recommandations = pd.DataFrame.from_dict(recommandations) + selected_recommandation_index = selected_rows[0] + selected_recommandation = recommandations.iloc[selected_recommandation_index] + return [selected_recommandation.to_dict()] + + def add_to_variant_trees( + self, + n_clicks, + selected_recommandation, + recommandations_added_to_variant_trees, + ): + if not n_clicks: + raise PreventUpdate + + if not selected_recommandation: + return [ + "Please choose a recommandation", + dash.no_update, + dash.no_update, + ] + + if not recommandations_added_to_variant_trees: + recommandations_added_to_variant_trees = [] + + selected_recommandation_df = pd.DataFrame(selected_recommandation, index=[0]) + selected_agent_name = selected_recommandation_df['Agent'].item() + self.logger.debug(f"selected_agent_name={selected_agent_name}") + + for recommandation_added in recommandations_added_to_variant_trees: + recommandation_added_df = pd.DataFrame.from_dict(recommandation_added) + # TODO: test on more columns to handle multi recommandations by same agent + added_agent_name = recommandation_added_df['Agent'].item() + self.logger.debug(f"added_agent_name={added_agent_name}") + if added_agent_name == selected_agent_name: + return [ + "Recommandation already added to variant trees", + dash.no_update, + dash.no_update, + ] + + selected_variant_tree = None + for variant_tree_dict in self.variant_env_trees: + variant_agent_name = variant_tree_dict.get("agent_name") + self.logger.debug(f"variant_agent_name={variant_agent_name}") + if variant_agent_name == selected_agent_name: + selected_variant_tree = variant_tree_dict.get("variant_env_tree") + + if not selected_variant_tree: + return [ + "Variant tree not found", + dash.no_update, + dash.no_update, + ] + + recommandations_added_to_variant_trees.append(selected_recommandation) + # Replace current env_tree by variant tree + self.env.env_tree = selected_variant_tree + self._variant_tree_added = True + + return [ + "Variant tree added !", + recommandations_added_to_variant_trees, + 1, + ] diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index f4e17d5..c31c1af 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -137,7 +137,8 @@ def add_callbacks(dash_app, viz_server): Output("act_on_env_trigger_for", "n_clicks") ], [Input("trigger_computation", "value"), - Input("recompute_rt_from_timeline", "n_clicks") + Input("recompute_rt_from_timeline", "n_clicks"), + Input("variant_tree_added", "n_clicks"), ] )(viz_server.computation_wrapper) @@ -276,6 +277,7 @@ def add_callbacks(dash_app, viz_server): [ Output("recommandations_div", "children"), Output("recommandations_container", "is_open"), + Output("recommandations_store", "data"), ], [ Input("show_more_issue", "n_clicks"), @@ -287,6 +289,38 @@ def add_callbacks(dash_app, viz_server): )(viz_server.show_recommandations) dash_app.callback( - [Output("loading_recommandations_output", "children")], - [Input("show_more_issue", "n_clicks")] + [ + Output("loading_recommandations_output", "children"), + ], + [ + Input("show_more_issue", "n_clicks"), + ] )(viz_server.loading_recommandations_table) + + dash_app.callback( + [ + # TODO confirmation message + Output("added_to_variant_trees_message", "children"), + Output("recommandations_added_to_variant_trees_store", "data"), + Output("variant_tree_added", "n_clicks"), + ], + [ + Input("add_to_variant_trees_button", "n_clicks"), + ], + [ + State("selected_recommandation_store", "data"), + State("recommandations_added_to_variant_trees_store", "data"), + ] + )(viz_server.add_to_variant_trees) + + dash_app.callback( + [ + Output("selected_recommandation_store", "data"), + ], + [ + Input("recommandations_table", "selected_rows"), + ], + [ + State("recommandations_store", "data"), + ], + )(viz_server.select_recommandation) diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 99f54c1..e6eef57 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -683,6 +683,7 @@ def setupLayout(viz_server): # triggering the update of the figures check_issue = html.Label("", id="check_issue", n_clicks=0) + variant_tree_added = html.Label("", id="variant_tree_added", n_clicks=0) act_on_env_trigger_rt = html.Label("", id="act_on_env_trigger_rt", n_clicks=0) act_on_env_trigger_for = html.Label("", id="act_on_env_trigger_for", n_clicks=0) clear_assistant_path = html.Label("", id="clear_assistant_path", n_clicks=0) @@ -713,7 +714,8 @@ def setupLayout(viz_server): update_substation_layout_clicked_from_sub, update_substation_layout_clicked_from_grid, trigger_rt_extra_info, trigger_for_extra_info, update_progress_bar_from_act, update_progress_bar_from_figs, - check_issue + check_issue, + variant_tree_added, ], id="hidden_buttons_for_callbacks", style={'display': 'none'}) @@ -770,10 +772,29 @@ def setupLayout(viz_server): ), html.Div( children=[ - dbc.Button("Show details", id="show_recommandation_details_button", className="ml-auto"), - dbc.Button("To Knowledge Base", id="add_to_knowledge_base_button", className="ml-auto"), - dbc.Button("Add to variant trees", id="add_to_variant_trees_button", className="ml-auto"), - dbc.Button("Apply", id="apply_recommandation", className="ml-auto"), + dbc.Button( + "Show details", + id="show_recommandation_details_button", + className="ml-auto", + n_clicks=0 + ), + dbc.Button( + "To Knowledge Base", + id="add_to_knowledge_base_button", + className="ml-auto" + ), + dbc.Button( + "Add to variant trees", + id="add_to_variant_trees_button", + className="ml-auto", + n_clicks=0 + ), + dbc.Button( + "Apply", + id="apply_recommandation", + className="ml-auto", + n_clicks=0 + ), ], style={ "alignItems":"left", @@ -792,9 +813,18 @@ def setupLayout(viz_server): "padding": "10px" } ), + dbc.Label( + "", + id="added_to_variant_trees_message", + ), html.Div( children=[ - dbc.Button("Explore", id="show_details", className="ml-auto") + dbc.Button( + "Explore", + id="show_details", + className="ml-auto", + n_clicks=0 + ) ], style={ "alignItems": "end", @@ -827,6 +857,15 @@ def setupLayout(viz_server): children=html.Div(id="loading_recommandations_output"), ) + recommandations_store = dcc.Store( + id="recommandations_store" + ) + selected_recommandation_store = dcc.Store( + id="selected_recommandation_store" + ) + recommandations_added_to_variant_trees_store = dcc.Store( + id="recommandations_added_to_variant_trees_store" + ) # Final page layout_css = "container-fluid h-100 d-md-flex d-xl-flex flex-md-column flex-xl-column" @@ -851,7 +890,10 @@ def setupLayout(viz_server): interval_object, hidden_interactions, timer_callbacks, - modal_issue + modal_issue, + recommandations_store, + selected_recommandation_store, + recommandations_added_to_variant_trees_store, ]) return layout diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index ca36e37..1050104 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -295,8 +295,6 @@ def _stop_if_issue(self, obs): issues = [] self._current_issues = None act = self._get_current_action() - self.logger.debug(act) - self.logger.debug(type(act)) if self._stop_if_alarm(obs): issues.append("Assistant raised an alarm") if self._stop_if_action(act): From f81f1d060947f0b133a746bc375d2ef912284e24 Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Mon, 11 Jul 2022 16:02:54 +0200 Subject: [PATCH 4/9] Add running modes --- grid2game/VizServer.py | 18 ++- grid2game/_utils/_temporal_callbacks.py | 15 +++ grid2game/_utils/_temporal_layout.py | 151 ++++++++++++++++-------- grid2game/envs/env.py | 45 +++++-- 4 files changed, 172 insertions(+), 57 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index 70f98f2..7a74f5d 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -1149,9 +1149,7 @@ def show_recommandations( return [dash.no_update, not is_open, dash.no_update] if n_show_more: - self.env.start_recommandations_computation() - self.variant_env_trees = [] agent_name = self.format_path(os.path.abspath(self.assistant_path)) @@ -1160,6 +1158,8 @@ def show_recommandations( variant_env_tree = copy.deepcopy(self.env.env_tree) current_node = variant_env_tree.current_node + variant_node = copy.deepcopy(current_node) + current_node.father.add_son(agent_action, variant_node) obs, reward, done, info = current_node.get_obs_rewar_done_info() overloads = (obs.rho[obs.rho > 1.0]).tolist() @@ -1291,3 +1291,17 @@ def add_to_variant_trees( recommandations_added_to_variant_trees, 1, ] + + def dropdown_mode(self, mode, manual_is_open, auto_is_open): + + self.env.mode = mode + + if mode in [self.env.MODE_MANUAL]: + manual_is_open = True + auto_is_open = False + + elif mode in [self.env.MODE_RECOMMAND, self.env.MODE_ASSISTANT]: + manual_is_open = False + auto_is_open = True + + return [manual_is_open, auto_is_open] diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index c31c1af..f8db476 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -324,3 +324,18 @@ def add_callbacks(dash_app, viz_server): State("recommandations_store", "data"), ], )(viz_server.select_recommandation) + + # dropdown mode + dash_app.callback( + [ + Output("controls_manual_collapse", "is_open"), + Output("controls_auto_collapse", "is_open"), + ], + [ + Input("mode_names", "value"), + ], + [ + State("controls_manual_collapse", "is_open"), + State("controls_auto_collapse", "is_open"), + ] + )(viz_server.dropdown_mode) diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index e6eef57..9ed5f94 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -24,31 +24,62 @@ def setupLayout(viz_server): n_clicks=0, className="btn btn-primary") # reset_button_dummy = html.P("", style={'display': 'none'}) - reset_col = html.Div(id="reset-col", - children=[reset_button, - html.Div([# html.P("Chronics: ", style={"marginRight": 5, "marginLeft": 5}), - html.Div([dcc.Dropdown(id="chronic_names", - placeholder="Select a chronic", - options=[{"value": el, "label": el} - for el in viz_server.env.list_chronics()]) - ], - id="chronics_dropdown", - style={"width": "100%"}), - ], - id="chronics_selector", - style={"display": "flex", "minWidth": "20%", "marginRight": "10", "marginLeft": "10"}), - html.Div([ - dcc.Input(id="set_seed", - type="number", - placeholder="Select a seed", - ), - ], - id="seed_selector") - ], - style={"display": "flex", - # 'justify-content': 'space-between' - } - ) + reset_col = html.Div( + id="reset-col", + children=[ + reset_button, + html.Div([# html.P("Chronics: ", style={"marginRight": 5, "marginLeft": 5}), + html.Div( + [ + dcc.Dropdown( + id="chronic_names", + placeholder="Select a chronic", + options=[ + {"value": el, "label": el} + for el in viz_server.env.list_chronics() + ] + ) + ], + id="chronics_dropdown", + style={"width": "100%"}), + ], + id="chronics_selector", + style={"display": "flex", "minWidth": "20%", "marginRight": "10", "marginLeft": "10"} + ), + html.Div([ + dcc.Input(id="set_seed", + type="number", + placeholder="Select a seed", + ), + ], + id="seed_selector"), + html.Div([ + html.Div( + [ + dcc.Dropdown( + id="mode_names", + placeholder="Select a mode", + options=[ + {"value": value, "label": label} + for value, label in viz_server.env.list_modes() + ], + # Default value + value=viz_server.env.default_mode, + ) + ], + id="mode_dropdown", + style={"width": "100%"}), + ], + style={ + "display": "flex", + "min-width": "20%" + } + ) + ], + style={"display": "flex", + # 'justify-content': 'space-between' + } + ) # Controls widget (step, reset etc.) step_button = html.Label("Step", @@ -92,7 +123,7 @@ def setupLayout(viz_server): id="is_computing_right", style={'display': 'none'}) - controls_row = html.Div(id="control-buttons", + controls_manual_row = html.Div(id="control-buttons", # className="row", children=[ is_computing_left, @@ -111,6 +142,17 @@ def setupLayout(viz_server): "display": "flex"} ) + controls_auto_row = html.Div(id="control-buttons", + # className="row", + children=[ + is_computing_left, + go_till_game_over, + is_computing_right + ], + style={'justifyContent': 'space-between', + "display": "flex"} + ) + # Units displayed control # TODO add a button "trust assistant up to" that will play the actions suggested by the # TODO assistant @@ -200,18 +242,23 @@ def setupLayout(viz_server): storinfo_col = html.Div(id="storinfo-col", className=button_css_class, children=[stor_info_label, stor_info], style=style_button) # general layout - change_units = html.Div(id="change_units", - children=[ - lineinfo_col, - lineside_col, - loadinfo_col, - geninfo_col, - storinfo_col, - # show_temporal_graph - ], - style={"display": "flex", - 'justifyContent': 'space-between'}, - ) + change_units = html.Div( + id="change_units", + children=[ + lineinfo_col, + lineside_col, + loadinfo_col, + geninfo_col, + storinfo_col, + # show_temporal_graph + ], + style={ + "display": "flex", + 'justifyContent': 'space-between', + "paddingBottom": "20px", + "paddingTop": "5px" + }, + ) if viz_server._app_heroku: assistant_txt = "Feature currently unavailable on heroku" save_txt = "Feature currently unavailable on heroku" @@ -320,14 +367,24 @@ def setupLayout(viz_server): ] ) - controls_row = html.Div(id="controls-row", - children=[ - reset_col, - controls_row, - select_assistant, - save_experiment, - change_units - ]) + controls_row = html.Div( + id="controls-row", + children=[ + reset_col, + dbc.Collapse( + controls_manual_row, + id="controls_manual_collapse", + is_open=False, + ), + dbc.Collapse( + controls_auto_row, + id="controls_auto_collapse", + is_open=False, + ), + select_assistant, + save_experiment, + ] + ) # progress in the scenario (progress bar and timeline) progress_bar_for_scenario = html.Div(children=[html.Div(dbc.Progress(id="scenario_progression", @@ -881,6 +938,8 @@ def setupLayout(viz_server): loading_recommandations, recommandations_container, html.Br(), + change_units, + html.Br(), # state_row, # the two graphs of the grid graph_col, # the two graphs of the grid html.Br(), diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index 1050104..0e15654 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -38,6 +38,10 @@ class Env(ComputeWrapper): LIKE_PREVIOUS = 2 MANUAL = 3 + MODE_MANUAL = 0 # operator full-control + MODE_RECOMMAND = 1 # agent stops on any issue + MODE_ASSISTANT = 2 # agent stops only when raising alarm + def __init__(self, env_name, assistant_path=None, @@ -97,6 +101,9 @@ def __init__(self, # actions to explore self.all_topo_actions = None + self.default_mode = self.MODE_RECOMMAND + self.mode = self.default_mode + def is_assistant_illegal(self): if "is_illegal" in self._sim_info: return self._sim_info["is_illegal"] @@ -132,6 +139,13 @@ def list_chronics(self): res = [os.path.split(el)[-1] for el in res] return res + def list_modes(self): + return [ + (self.MODE_MANUAL, "Full Control"), + (self.MODE_RECOMMAND, "Assisted"), + (self.MODE_ASSISTANT, "Delegated"), + ] + def load_assistant(self, assistant_path): self.logger.info(f"attempt to load assistant with path : \"{assistant_path}\"") has_been_loaded = False @@ -294,13 +308,19 @@ def _stop_if_bad_kpi(self, obs): def _stop_if_issue(self, obs): issues = [] self._current_issues = None - act = self._get_current_action() - if self._stop_if_alarm(obs): - issues.append("Assistant raised an alarm") - if self._stop_if_action(act): - issues.append("The current action has a chance to change the grid") - if self._stop_if_bad_kpi(obs): - issues.append("Overload") + + if self.mode in [self.MODE_RECOMMAND, self.MODE_MANUAL]: + act = self._get_current_action() + if self._stop_if_alarm(obs): + issues.append("Assistant raised an alarm") + if self._stop_if_action(act): + issues.append("The current action has a chance to change the grid") + if self._stop_if_bad_kpi(obs): + issues.append("Overload") + elif self.mode == self.MODE_ASSISTANT: + if self._stop_if_alarm(obs): + issues.append("Assistant raised an alarm") + if len(issues) > 0: self._current_issues = issues return True @@ -379,7 +399,10 @@ def step(self, action=None): return obs, reward, done, info if self._assistant_action is None: + self.logger.debug("step: self._assistant_action is None") self.choose_next_assistant_action() + if self._assistant_action is None: + self.logger.debug("step: self._assistant_action is still None") if action is None: self.choose_next_action() @@ -388,6 +411,8 @@ def step(self, action=None): # TODO is this correct ? I never really tested that self._current_action = action + + self.env_tree.make_step(assistant=self.assistant, chosen_action=action) obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() @@ -397,7 +422,7 @@ def step(self, action=None): if not done: self.choose_next_assistant_action() - self.logger.info("step: done is False") + #self.logger.info("step: done is False") try: self._sim_obs, self._sim_reward, self._sim_done, self._sim_info = obs.simulate(self._assistant_action) except NoForecastAvailable: @@ -412,9 +437,11 @@ def step(self, action=None): return obs, reward, done, info def choose_next_assistant_action(self): + self.logger.debug("choose_next_assistant_action") self._assistant_action = copy.deepcopy(self.env_tree.current_node.assistant_action) def choose_next_action(self): + self.logger.debug(f"choose_next_action self.next_action_from={self.next_action_from}") if self.next_action_from == self.ASSISTANT: self._current_action = copy.deepcopy(self._assistant_action) elif self.next_action_from == self.LIKE_PREVIOUS or self.next_action_from == self.MANUAL: @@ -513,7 +540,7 @@ def handle_click_timeline(self, time_line_graph_clcked) -> int: obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() if not done: self.choose_next_assistant_action() - self.logger.info("step: done is False") + self.logger.info("handle_click_timeline: done is False") try: self._sim_obs, self._sim_reward, self._sim_done, self._sim_info = obs.simulate(self._assistant_action) except NoForecastAvailable: From 962a6827db3acbb5be2a035f73d8d63a195c6d50 Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Tue, 12 Jul 2022 22:17:15 +0200 Subject: [PATCH 5/9] Apply recommandation --- grid2game/VizServer.py | 337 +++++++++++++++--------- grid2game/_utils/_temporal_callbacks.py | 61 +++-- grid2game/_utils/_temporal_layout.py | 130 ++++----- grid2game/envs/computeWrapper.py | 14 +- grid2game/envs/env.py | 22 +- 5 files changed, 334 insertions(+), 230 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index 7a74f5d..2eba69e 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -15,7 +15,7 @@ import dash import dash_bootstrap_components as dbc -from dash import html, dcc +from dash import html, dcc, ctx from dash.dash_table import DataTable from dash.exceptions import PreventUpdate @@ -202,6 +202,11 @@ def __init__(self, self.real_time = self.plot_grids.figure_rt self.forecast = self.plot_grids.figure_forecat + # Variant trees + self.variant_env_trees = [] + self._variant_tree_added = 0 + self._check_issue = 0 + # initialize the layout self._layout_temporal = html.Div(setupLayout_temporal(self), id="all_temporal") @@ -235,9 +240,6 @@ def __init__(self, self._do_display_action = True self._dropdown_value = "assistant" - # Variant trees - self.variant_env_trees = [] - self._variant_tree_added = False def _make_glop_env_config(self, build_args): g2op_config = {} @@ -372,20 +374,21 @@ def handle_act_on_env( self.env.next_computation = "step" self.env.next_computation_kwargs = {} self.need_update_figures = False - check_issue = 1 + self._check_issue += 1 + check_issue = self._check_issue elif button_id == "go_till_game_over-button": self.env.start_computation() self.env.next_computation = "step_end" self.env.next_computation_kwargs = {} self.need_update_figures = True - check_issue = 1 + self._check_issue += 1 + check_issue = self._check_issue elif button_id == "reset-button": self.env.start_computation() self.env.next_computation = "reset" self.env.next_computation_kwargs = {"chronics_id": self.chronics_id, "seed": self.seed} self.need_update_figures = False change_graph_title = 1 - check_issue = 0 self._next_action_is_assistant() elif button_id == "simulate-button": self.env.start_computation() @@ -397,13 +400,13 @@ def handle_act_on_env( self.env.next_computation = "back" self.env.next_computation_kwargs = {} self.need_update_figures = False - check_issue = 0 elif button_id == "gofast-button": # this button is off now ! self.env.start_computation() self.env.next_computation = "step_rec_fast" self.env.next_computation_kwargs = {"nb_step_gofast": self.nb_step_gofast} - check_issue = 1 + self._check_issue += 1 + check_issue = self._check_issue elif button_id == "go-button": self.go_clicks += 1 if self.go_clicks % 2: @@ -416,7 +419,8 @@ def handle_act_on_env( self.env.start_computation() self._button_shape = "btn btn-secondary" self._gofast_button_shape = "btn btn-secondary" - check_issue = 1 + self._check_issue += 1 + check_issue = self._check_issue self.env.next_computation = "step_rec" self.env.next_computation_kwargs = {} self.need_update_figures = False @@ -492,15 +496,20 @@ def check_issue( n_show_more, is_open ): + button_id = ctx.triggered_id + self.logger.debug(f"check_issue: triggered_id = {button_id}") + + if not button_id: + raise PreventUpdate # Close modal when clicking on Show more - if n_show_more and is_open: + if button_id == "show_more_issue": return [False, ""] # make sure the environment has nothing to compute while self.env.needs_compute(): time.sleep(0.1) issues = self.env._current_issues - if n_check_issue and issues and not is_open: + if button_id == "check_issue" and issues and not is_open: len_issues = len(issues) if len_issues == 1: issue_text = f"There is {len_issues} issue: " @@ -542,8 +551,9 @@ def computation_wrapper( recompute_rt_from_timeline, variant_tree_added, ): - if self._variant_tree_added: - self._variant_tree_added = False + button_id = ctx.triggered_id + + if button_id == "variant_tree_added": trigger_rt = 1 trigger_for = dash.no_update else: @@ -1137,24 +1147,52 @@ def main_action_search(self, i_am_computing_state, i_am_computing_state] - def show_recommandations( + def handle_recommendations( self, + # buttons n_show_more, n_close, - is_open + n_add_to_variants, + n_apply, + # recommendation container + is_open, + # stores + selected_recommendation, + recommendations_added_to_variant_trees, ): + button_id = ctx.triggered_id + self.logger.debug(f"handle_recommendations: triggered_id = {button_id}") + + if not button_id: + raise PreventUpdate + + recommendations_div = dash.no_update + recommendations_container_open = dash.no_update + recommendations_store = dash.no_update + recommendations_message = None + variant_tree_added = dash.no_update - if n_close: + if button_id == "close_recommendations_button": + recommendations_added_to_variant_trees = dash.no_update + + # Reset issues self.env._current_issues = None - return [dash.no_update, not is_open, dash.no_update] + # Collapse recommendations + recommendations_container_open = False + + elif button_id == "show_more_issue": + recommendations_added_to_variant_trees = dash.no_update + + # Enable dash loading + self.env.start_recommendations_computation() - if n_show_more: - self.env.start_recommandations_computation() self.variant_env_trees = [] agent_name = self.format_path(os.path.abspath(self.assistant_path)) agent_action = self.env._assistant_action - # TODO: handle the tree in a better way + # TODO: Handle the tree in a better way: + # Do not copy the env_tree, but instead add attributes to the Node and TemporalNodeData + # of the variant_node to control their visibility in the timeline graph variant_env_tree = copy.deepcopy(self.env.env_tree) current_node = variant_env_tree.current_node @@ -1176,7 +1214,8 @@ def show_recommandations( } ) - self.env.stop_recommandations_computation() + # Disable dash loading + self.env.stop_recommendations_computation() d = { 'Agent': [agent_name], @@ -1185,118 +1224,176 @@ def show_recommandations( 'Holding Time': [holding_steps] } - recommandations = pd.DataFrame(data=d) - return [ - DataTable( - id="recommandations_table", - columns=[ - {"name": i, "id": i} for i in recommandations.columns - ], - data=recommandations.to_dict("records"), - style_table={"overflowX": "auto"}, - row_selectable="single", - style_cell={ - "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": 0, - }, - tooltip_data=[ - { - column: {"value": str(value), "type": "markdown"} - for column, value in row.items() - } - for row in recommandations.to_dict("rows") - ], - ), - not is_open, - recommandations.to_dict(), - ] - - raise PreventUpdate - - def loading_recommandations_table(self, n_clicks): - time.sleep(0.1) - while self.env.is_computing_recommandations(): - time.sleep(0.1) - return [""] + recommendations = pd.DataFrame(data=d) + + recommendations_div = DataTable( + id="recommendations_table", + columns=[ + {"name": i, "id": i} for i in recommendations.columns + ], + data=recommendations.to_dict("records"), + style_table={"overflowX": "auto"}, + row_selectable="single", + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + }, + tooltip_data=[ + { + column: {"value": str(value), "type": "markdown"} + for column, value in row.items() + } + for row in recommendations.to_dict("rows") + ], + ) + recommendations_container_open = True + recommendations_store = recommendations.to_dict() - def select_recommandation( - self, - selected_rows, - recommandations, - ): - if not selected_rows: - raise PreventUpdate - recommandations = pd.DataFrame.from_dict(recommandations) - selected_recommandation_index = selected_rows[0] - selected_recommandation = recommandations.iloc[selected_recommandation_index] - return [selected_recommandation.to_dict()] + elif button_id == "add_to_variant_trees_button": - def add_to_variant_trees( - self, - n_clicks, - selected_recommandation, - recommandations_added_to_variant_trees, - ): - if not n_clicks: - raise PreventUpdate + if not selected_recommendation: + recommendations_message = "Please choose a recommendation" + recommendations_added_to_variant_trees = dash.no_update + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + if not recommendations_added_to_variant_trees: + recommendations_added_to_variant_trees = [] + + selected_recommendation_df = pd.DataFrame(selected_recommendation, index=[0]) + selected_agent_name = selected_recommendation_df['Agent'].item() + self.logger.debug(f"selected_agent_name={selected_agent_name}") + + for recommendation_added in recommendations_added_to_variant_trees: + recommendation_added_df = pd.DataFrame.from_dict(recommendation_added) + # TODO: test on more columns than agent's name to handle multi recommendations by same agent + added_agent_name = recommendation_added_df['Agent'].item() + self.logger.debug(f"added_agent_name={added_agent_name}") + if added_agent_name == selected_agent_name: + recommendations_message = "recommendation already added to variant trees", + recommendations_added_to_variant_trees = dash.no_update + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] - if not selected_recommandation: - return [ - "Please choose a recommandation", - dash.no_update, - dash.no_update, - ] - - if not recommandations_added_to_variant_trees: - recommandations_added_to_variant_trees = [] - - selected_recommandation_df = pd.DataFrame(selected_recommandation, index=[0]) - selected_agent_name = selected_recommandation_df['Agent'].item() - self.logger.debug(f"selected_agent_name={selected_agent_name}") - - for recommandation_added in recommandations_added_to_variant_trees: - recommandation_added_df = pd.DataFrame.from_dict(recommandation_added) - # TODO: test on more columns to handle multi recommandations by same agent - added_agent_name = recommandation_added_df['Agent'].item() - self.logger.debug(f"added_agent_name={added_agent_name}") - if added_agent_name == selected_agent_name: + selected_variant_tree = None + for variant_tree_dict in self.variant_env_trees: + variant_agent_name = variant_tree_dict.get("agent_name") + self.logger.debug(f"variant_agent_name={variant_agent_name}") + if variant_agent_name == selected_agent_name: + selected_variant_tree = variant_tree_dict.get("variant_env_tree") + + if not selected_variant_tree: + recommendations_message = "Variant tree not found", + recommendations_added_to_variant_trees = dash.no_update + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + recommendations_added_to_variant_trees.append(selected_recommendation) + # Replace current env_tree by variant tree + # TODO: Intead of duplicating the env_tree, create all variant nodes on the env_tree, + # and enable the visibility of the selected variant tree on the timeline graph + self.env.env_tree = selected_variant_tree + + recommendations_message = "Variant tree added !" + self._variant_tree_added += 1 + variant_tree_added = self._variant_tree_added + + elif button_id == "apply_recommendation_button": + # TODO: Intead of duplicating the env_tree, create all variant nodes on the env_tree + # and remove the variant nodes that haven't been added by the user. + + if not selected_recommendation: + recommendations_message = "Please choose a recommendation" + recommendations_added_to_variant_trees = dash.no_update return [ - "Recommandation already added to variant trees", - dash.no_update, - dash.no_update, + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added ] - selected_variant_tree = None - for variant_tree_dict in self.variant_env_trees: - variant_agent_name = variant_tree_dict.get("agent_name") - self.logger.debug(f"variant_agent_name={variant_agent_name}") - if variant_agent_name == selected_agent_name: - selected_variant_tree = variant_tree_dict.get("variant_env_tree") - - if not selected_variant_tree: - return [ - "Variant tree not found", - dash.no_update, - dash.no_update, - ] - - recommandations_added_to_variant_trees.append(selected_recommandation) - # Replace current env_tree by variant tree - self.env.env_tree = selected_variant_tree - self._variant_tree_added = True + selected_recommendation_df = pd.DataFrame(selected_recommendation, index=[0]) + selected_agent_name = selected_recommendation_df['Agent'].item() + self.logger.debug(f"selected_agent_name={selected_agent_name}") + + selected_variant_tree = None + for variant_tree_dict in self.variant_env_trees: + variant_agent_name = variant_tree_dict.get("agent_name") + self.logger.debug(f"variant_agent_name={variant_agent_name}") + if variant_agent_name == selected_agent_name: + selected_variant_tree = variant_tree_dict.get("variant_env_tree") + + if not selected_variant_tree: + recommendations_message = "Variant tree not found", + recommendations_added_to_variant_trees = dash.no_update + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + # Replace current env_tree by variant tree + self.env.env_tree = selected_variant_tree + + # Reset message + recommendations_message = "" + # Reset stores + recommendations_store = None + recommendations_added_to_variant_trees = None + # Reset issues + self.env._current_issues = None + # Collapse recommendations + recommendations_container_open = False + + else: + raise PreventUpdate return [ - "Variant tree added !", - recommandations_added_to_variant_trees, - 1, + recommendations_div, + recommendations_container_open, + recommendations_store, + recommendations_message, + recommendations_added_to_variant_trees, + variant_tree_added, ] + def loading_recommendations_table(self, n_clicks): + button_id = ctx.triggered_id + + if not button_id: + raise PreventUpdate + + time.sleep(0.1) + while self.env.is_computing_recommendations(): + time.sleep(0.1) + return [""] + + def select_recommendation( + self, + selected_rows, + recommendations, + ): + if not selected_rows: + raise PreventUpdate + recommendations = pd.DataFrame.from_dict(recommendations) + selected_recommendation_index = selected_rows[0] + selected_recommendation = recommendations.iloc[selected_recommendation_index] + return [selected_recommendation.to_dict()] + def dropdown_mode(self, mode, manual_is_open, auto_is_open): self.env.mode = mode - if mode in [self.env.MODE_MANUAL]: + if mode in [self.env.MODE_MANUAL, self.env.MODE_LEGACY]: manual_is_open = True auto_is_open = False diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index f8db476..b633567 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -272,58 +272,61 @@ def add_callbacks(dash_app, viz_server): ] )(viz_server.check_issue) - # show the recommandations table + # show the recommendations table dash_app.callback( [ - Output("recommandations_div", "children"), - Output("recommandations_container", "is_open"), - Output("recommandations_store", "data"), + Output("recommendations_div", "children"), + Output("recommendations_container", "is_open"), + Output("recommendations_store", "data"), + Output("recommendations_message", "children"), + Output("recommendations_added_to_variant_trees_store", "data"), + Output("variant_tree_added", "n_clicks"), ], [ Input("show_more_issue", "n_clicks"), - Input("close_recommandations_button", "n_clicks"), + Input("close_recommendations_button", "n_clicks"), + Input("add_to_variant_trees_button", "n_clicks"), + Input("apply_recommendation_button", "n_clicks"), ], [ - State("recommandations_container", "is_open"), + State("recommendations_container", "is_open"), + State("selected_recommendation_store", "data"), + State("recommendations_added_to_variant_trees_store", "data"), ] - )(viz_server.show_recommandations) + )(viz_server.handle_recommendations) dash_app.callback( [ - Output("loading_recommandations_output", "children"), + Output("loading_recommendations_output", "children"), ], [ Input("show_more_issue", "n_clicks"), ] - )(viz_server.loading_recommandations_table) - - dash_app.callback( - [ - # TODO confirmation message - Output("added_to_variant_trees_message", "children"), - Output("recommandations_added_to_variant_trees_store", "data"), - Output("variant_tree_added", "n_clicks"), - ], - [ - Input("add_to_variant_trees_button", "n_clicks"), - ], - [ - State("selected_recommandation_store", "data"), - State("recommandations_added_to_variant_trees_store", "data"), - ] - )(viz_server.add_to_variant_trees) + )(viz_server.loading_recommendations_table) + + # dash_app.callback( + # [ + # Output("recommendations_message", "children"), + # ], + # [ + # Input("apply_recommendation_button", "n_clicks"), + # ], + # [ + # State("selected_recommendation_store", "data"), + # ], + # )(viz_server.apply_recommendation) dash_app.callback( [ - Output("selected_recommandation_store", "data"), + Output("selected_recommendation_store", "data"), ], [ - Input("recommandations_table", "selected_rows"), + Input("recommendations_table", "selected_rows"), ], [ - State("recommandations_store", "data"), + State("recommendations_store", "data"), ], - )(viz_server.select_recommandation) + )(viz_server.select_recommendation) # dropdown mode dash_app.callback( diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 9ed5f94..7a508ba 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -108,7 +108,7 @@ def setupLayout(viz_server): n_clicks=0, className="btn btn-primary", ) - go_till_game_over = html.Label("End", + go_till_game_over = html.Label("Go to End", id="go_till_game_over-button", n_clicks=0, className="btn btn-primary", @@ -123,36 +123,6 @@ def setupLayout(viz_server): id="is_computing_right", style={'display': 'none'}) - controls_manual_row = html.Div(id="control-buttons", - # className="row", - children=[ - is_computing_left, - back_button, # TODO display back only if its possible in the viz_server.env - step_button, - simulate_button, - go_butt, - html.Div([nb_step_go_fast, - go_fast], - id="control_nb_step_fast", - style= {"display": "flex"}), - go_till_game_over, - is_computing_right - ], - style={'justifyContent': 'space-between', - "display": "flex"} - ) - - controls_auto_row = html.Div(id="control-buttons", - # className="row", - children=[ - is_computing_left, - go_till_game_over, - is_computing_right - ], - style={'justifyContent': 'space-between', - "display": "flex"} - ) - # Units displayed control # TODO add a button "trust assistant up to" that will play the actions suggested by the # TODO assistant @@ -367,19 +337,53 @@ def setupLayout(viz_server): ] ) + controls_manual_row = html.Div(id="control-manual-buttons", + # className="row", + children=[ + back_button, # TODO display back only if its possible in the viz_server.env + step_button, + simulate_button, + go_butt, + html.Div([nb_step_go_fast, + go_fast], + id="control_nb_step_fast", + style= {"display": "flex"}), + go_till_game_over, + ], + style={'justifyContent': 'space-between', + "display": "flex"} + ) + + controls_auto_row = html.Div(id="control-auto-buttons", + # className="row", + children=[ + go_till_game_over, + ], + style={'justifyContent': 'space-between', + "display": "flex"} + ) + controls_row = html.Div( id="controls-row", children=[ reset_col, - dbc.Collapse( - controls_manual_row, - id="controls_manual_collapse", - is_open=False, - ), - dbc.Collapse( - controls_auto_row, - id="controls_auto_collapse", - is_open=False, + html.Div( + id="control-buttons", + # className="row", + children=[ + is_computing_left, + dbc.Collapse( + controls_auto_row, + id="controls_auto_collapse", + is_open=False, + ), + dbc.Collapse( + controls_manual_row, + id="controls_manual_collapse", + is_open=False, + ), + is_computing_right + ] ), select_assistant, save_experiment, @@ -798,12 +802,12 @@ def setupLayout(viz_server): is_open=False, ) - recommandations_container = dbc.Collapse( + recommendations_container = dbc.Collapse( html.Div( children=[ dbc.Button( "Close", - id="close_recommandations_button", + id="close_recommendations_button", className="ml-auto", n_clicks=0, style={ @@ -812,7 +816,7 @@ def setupLayout(viz_server): ), dbc.Container([ dbc.Label( - 'Recommandations', + 'recommendations', style={ "padding": "5px" } @@ -820,9 +824,9 @@ def setupLayout(viz_server): html.Div( children=[ html.Div( - # Recommandations DataTable is loaded here from the callback + # recommendations DataTable is loaded here from the callback children = [], - id="recommandations_div", + id="recommendations_div", style={ "padding": "10px" } @@ -831,7 +835,7 @@ def setupLayout(viz_server): children=[ dbc.Button( "Show details", - id="show_recommandation_details_button", + id="show_recommendation_details_button", className="ml-auto", n_clicks=0 ), @@ -848,7 +852,7 @@ def setupLayout(viz_server): ), dbc.Button( "Apply", - id="apply_recommandation", + id="apply_recommendation_button", className="ml-auto", n_clicks=0 ), @@ -872,7 +876,7 @@ def setupLayout(viz_server): ), dbc.Label( "", - id="added_to_variant_trees_message", + id="recommendations_message", ), html.Div( children=[ @@ -901,27 +905,27 @@ def setupLayout(viz_server): "marginRight": "auto" } ), - id="recommandations_container", + id="recommendations_container", is_open=False, style={ "paddingBottom": "20px" } ) - loading_recommandations = dcc.Loading( - id="loading_recommandations", + loading_recommendations = dcc.Loading( + id="loading_recommendations", type="default", - children=html.Div(id="loading_recommandations_output"), + children=html.Div(id="loading_recommendations_output"), ) - recommandations_store = dcc.Store( - id="recommandations_store" + recommendations_store = dcc.Store( + id="recommendations_store" ) - selected_recommandation_store = dcc.Store( - id="selected_recommandation_store" + selected_recommendation_store = dcc.Store( + id="selected_recommendation_store" ) - recommandations_added_to_variant_trees_store = dcc.Store( - id="recommandations_added_to_variant_trees_store" + recommendations_added_to_variant_trees_store = dcc.Store( + id="recommendations_added_to_variant_trees_store" ) # Final page @@ -935,8 +939,8 @@ def setupLayout(viz_server): html.Br(), progress_bar_for_scenario, html.Br(), - loading_recommandations, - recommandations_container, + loading_recommendations, + recommendations_container, html.Br(), change_units, html.Br(), @@ -950,9 +954,9 @@ def setupLayout(viz_server): hidden_interactions, timer_callbacks, modal_issue, - recommandations_store, - selected_recommandation_store, - recommandations_added_to_variant_trees_store, + recommendations_store, + selected_recommendation_store, + recommendations_added_to_variant_trees_store, ]) return layout diff --git a/grid2game/envs/computeWrapper.py b/grid2game/envs/computeWrapper.py index 6812469..9cd517b 100644 --- a/grid2game/envs/computeWrapper.py +++ b/grid2game/envs/computeWrapper.py @@ -17,7 +17,7 @@ class ComputeWrapper(ABC): def __init__(self): self.__is_computing = False # whether or not something is being computed self.__computation_started = False # whether or not something needs to be computed - self.__is_computing_recommandations = False # whether or not variant trees are being computed + self.__is_computing_recommendations = False # whether or not variant trees are being computed self.count = 0 @abstractmethod @@ -34,14 +34,14 @@ def heavy_compute(self): self.__is_computing = False return res - def is_computing_recommandations(self): - return self.__is_computing_recommandations + def is_computing_recommendations(self): + return self.__is_computing_recommendations - def start_recommandations_computation(self): - self.__is_computing_recommandations = True + def start_recommendations_computation(self): + self.__is_computing_recommendations = True - def stop_recommandations_computation(self): - self.__is_computing_recommandations = False + def stop_recommendations_computation(self): + self.__is_computing_recommendations = False def is_computing(self): return self.__is_computing diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index 0e15654..08498e6 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -41,6 +41,7 @@ class Env(ComputeWrapper): MODE_MANUAL = 0 # operator full-control MODE_RECOMMAND = 1 # agent stops on any issue MODE_ASSISTANT = 2 # agent stops only when raising alarm + MODE_LEGACY = 3 def __init__(self, env_name, @@ -101,7 +102,7 @@ def __init__(self, # actions to explore self.all_topo_actions = None - self.default_mode = self.MODE_RECOMMAND + self.default_mode = self.MODE_LEGACY self.mode = self.default_mode def is_assistant_illegal(self): @@ -144,6 +145,7 @@ def list_modes(self): (self.MODE_MANUAL, "Full Control"), (self.MODE_RECOMMAND, "Assisted"), (self.MODE_ASSISTANT, "Delegated"), + (self.MODE_LEGACY, "Legacy") ] def load_assistant(self, assistant_path): @@ -269,14 +271,16 @@ def explore(self): for act, rew in res[:5]: self.step(act) if not till_the_end: - till_the_end = self._donothing_until_end() + till_the_end, nb_steps = self._donothing_until_end() self.env_tree.go_to_node(init_node) def _donothing_until_end(self): + steps = 0 obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() while not done: obs, reward, done, info = self.step(self.glop_env.action_space()) - return obs.current_step == obs.max_step + steps += 1 + return (obs.current_step == obs.max_step, steps) def _get_current_action(self): res = None @@ -317,9 +321,10 @@ def _stop_if_issue(self, obs): issues.append("The current action has a chance to change the grid") if self._stop_if_bad_kpi(obs): issues.append("Overload") - elif self.mode == self.MODE_ASSISTANT: + elif self.mode in [self.MODE_ASSISTANT, self.MODE_LEGACY]: if self._stop_if_alarm(obs): - issues.append("Assistant raised an alarm") + if self.mode == self.MODE_ASSISTANT: + issues.append("Assistant raised an alarm") if len(issues) > 0: self._current_issues = issues @@ -399,10 +404,7 @@ def step(self, action=None): return obs, reward, done, info if self._assistant_action is None: - self.logger.debug("step: self._assistant_action is None") self.choose_next_assistant_action() - if self._assistant_action is None: - self.logger.debug("step: self._assistant_action is still None") if action is None: self.choose_next_action() @@ -437,11 +439,9 @@ def step(self, action=None): return obs, reward, done, info def choose_next_assistant_action(self): - self.logger.debug("choose_next_assistant_action") self._assistant_action = copy.deepcopy(self.env_tree.current_node.assistant_action) def choose_next_action(self): - self.logger.debug(f"choose_next_action self.next_action_from={self.next_action_from}") if self.next_action_from == self.ASSISTANT: self._current_action = copy.deepcopy(self._assistant_action) elif self.next_action_from == self.LIKE_PREVIOUS or self.next_action_from == self.MANUAL: @@ -554,7 +554,7 @@ def get_current_action_list(self): return self.env_tree.get_current_action_list() def step_variant(self, env_tree, action=None): - # Step used to compute holding time of recommandations + # Step used to compute holding time of recommendations obs, reward, done, info = env_tree.current_node.get_obs_rewar_done_info() if done: self.stop_computation() From 2ddf1396206fa68435788217b826d05bff0ba24d Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Wed, 13 Jul 2022 09:22:24 +0200 Subject: [PATCH 6/9] Add simulate and integrate manual action - only layout, no callbacks yet --- grid2game/VizServer.py | 12 ++++- grid2game/_utils/_temporal_callbacks.py | 2 + grid2game/_utils/_temporal_layout.py | 63 ++++++++++++++++++++----- 3 files changed, 63 insertions(+), 14 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index 2eba69e..d1a4996 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -872,6 +872,8 @@ def display_click_data(self, sub_res = ["", self.plot_grids.sub_fig] update_substation_layout_clicked_from_grid = 0 + style_action_buttons = {'display': 'none', "width": "70%"} + # check which call backs triggered this calls # see https://dash.plotly.com/advanced-callbacks # section "Determining which Input has fired with dash.callback_context" @@ -884,7 +886,8 @@ def display_click_data(self, style_storage_input, storage_id_clicked, *storage_res, style_line_input, line_id_clicked, *line_res, style_sub_input, sub_id_clicked, *sub_res[:-1], - update_substation_layout_clicked_from_grid + update_substation_layout_clicked_from_grid, + style_action_buttons ] else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] @@ -902,6 +905,10 @@ def display_click_data(self, self._last_sub_id = None else: # I clicked on the graph of the grid + + if self.env.mode != self.env.MODE_LEGACY: + style_action_buttons = {'display': 'flex', "width": "70%"} + self._last_sub_id = None obj_type, obj_id, res_type = self.plot_grids.get_object_clicked(clickData) if obj_type == "gen": @@ -936,7 +943,8 @@ def display_click_data(self, style_storage_input, storage_id_clicked, *storage_res, style_line_input, line_id_clicked, *line_res, style_sub_input, sub_id_clicked, *sub_res[:-1], - update_substation_layout_clicked_from_grid + update_substation_layout_clicked_from_grid, + style_action_buttons ] def format_path(self, path): diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index b633567..e7f115c 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -63,6 +63,8 @@ def add_callbacks(dash_app, viz_server): Output("sub-id-clicked", "children"), # Output("graph_clicked_sub", "figure"), Output("update_substation_layout_clicked_from_grid", "n_clicks"), + + Output("action_buttons", "style"), ], [Input('real-time-graph', 'clickData'), Input("back-button", "n_clicks"), diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 7a508ba..73aa6ba 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -583,9 +583,8 @@ def setupLayout(viz_server): # Title action_widget_title = html.Div(id="action_widget_title", - children=[html.P("Action: "), - which_action_button], - style={}, + children=[html.P("Action: ")], + style={"width": "30%"}, ) # display the action layout_click = html.Div(id="action_clicked", @@ -594,22 +593,62 @@ def setupLayout(viz_server): line_clicked, sub_clicked], style={"width": "59%",}) + + action_buttons = html.Div( + id="action_buttons", + children=[ + dbc.Button( + "Simulate", + id="simulate_manual_action", + className="ml-auto", + n_clicks=0 + ), + dbc.Button( + "Integrate Manual Action", + id="integrate_manual_action", + className="ml-auto", + n_clicks=0 + ) + ], + style={'display': 'none', "width": "70%"} + ) + # action as text action_col = html.Div(id="action_widget", children=[current_action], style={'width': '39%'} ) + # combine both - interaction_and_action = html.Div([action_widget_title, - html.Div([layout_click, - action_col], - id="action_display", - style={"width": "100%", "display": "flex"}) - ], - id="action_select_and_print", - style={"width": "100%"}, - ) + interaction_and_action = html.Div( + [ + html.Div( + [ + action_widget_title, + action_buttons, + ], + id="action_header", + style={"width": "100%", "display": "flex"} + ), + html.Div( + [ + which_action_button, + ], + id="which_action_dropdown" + ), + html.Div( + [ + layout_click, + action_col + ], + id="action_display", + style={"width": "100%", "display": "flex"} + ) + ], + id="action_select_and_print", + style={"width": "100%"}, + ) ## temporal graphs graph_gen_load = dcc.Graph(id="graph_gen_load", From 423d0eab33894089d1f63ae54438d598d4dc608e Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Wed, 13 Jul 2022 18:55:18 +0200 Subject: [PATCH 7/9] Add simulate and integrate manual action callbacks - Simulate and integrate manual action buttons will show only if the recommendations are open. - Simulate button doesn't work. There is a bug on main branch, simulated figure doesn't update on manual action. - Refactoring of recommendations - TODO: Handle multi-recommendations for human integrated actions --- grid2game/VizServer.py | 288 ++++++++++++++++-------- grid2game/_utils/_temporal_callbacks.py | 15 +- grid2game/_utils/_temporal_layout.py | 4 +- 3 files changed, 208 insertions(+), 99 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index d1a4996..8f7b57e 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -747,19 +747,28 @@ def display_action_fun(self, redisp, stor_id, storage_p, line_id, line_status, - sub_id, clicked_sub_fig): + sub_id, clicked_sub_fig, + clicked_real_fig, + recommendations_is_open): """ modify the action taken based on the inputs, then displays the action (as text) """ # TODO handle better the action (this is ugly to access self.env._current_action from here) + style_action_buttons = dash.no_update + ctx = dash.callback_context dropdown_value = self._last_action update_substation_layout_clicked_from_sub = 0 if not ctx.triggered: # no click have been made yet - return [f"{self.env.current_action}", dropdown_value, update_substation_layout_clicked_from_sub] + return [ + f"{self.env.current_action}", + dropdown_value, + update_substation_layout_clicked_from_sub, + style_action_buttons + ] else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] @@ -770,24 +779,52 @@ def display_action_fun(self, self._last_action = "dn" self._do_display_action = False self._dropdown_value = "dn" + style_action_buttons = {'display': 'none', "width": "70%"} elif which_action_button == "assistant": self._next_action_is_assistant() + style_action_buttons = {'display': 'none', "width": "70%"} elif which_action_button == "prev": self.env.next_action_is_previous() self._last_action = "prev" self._do_display_action = False self._dropdown_value = "prev" + style_action_buttons = {'display': 'none', "width": "70%"} elif which_action_button == "manual": self._next_action_is_manual() + if self.env.mode != self.env.MODE_LEGACY and recommendations_is_open: + # Show the buttons only not in legacy mode, and if the + # recommendations table is open + style_action_buttons = {'display': 'flex', "width": "70%"} else: # nothing is done pass - res = [f"{self.env.current_action}", dropdown_value, update_substation_layout_clicked_from_sub] + res = [ + f"{self.env.current_action}", + dropdown_value, + update_substation_layout_clicked_from_sub, + style_action_buttons + ] return res + if button_id == "real-time-graph": + if self.env.mode != self.env.MODE_LEGACY and recommendations_is_open: + style_action_buttons = {'display': 'flex', "width": "70%"} + return [ + dash.no_update, + dash.no_update, + dash.no_update, + style_action_buttons + ] + + if not self._do_display_action: # i should not display the action - res = [f"{self.env.current_action}", dropdown_value, update_substation_layout_clicked_from_sub] + res = [ + f"{self.env.current_action}", + dropdown_value, + update_substation_layout_clicked_from_sub, + style_action_buttons + ] return res # i need to display the action @@ -827,7 +864,12 @@ def display_action_fun(self, raise PreventUpdate # TODO optim here to save that if not needed because nothing has changed - res = [f"{self.env.current_action}", self._dropdown_value, update_substation_layout_clicked_from_sub] + res = [ + f"{self.env.current_action}", + self._dropdown_value, + update_substation_layout_clicked_from_sub, + style_action_buttons + ] return res def display_grid_substation(self, update_substation_layout_clicked_from_sub, update_substation_layout_clicked_from_grid): @@ -872,8 +914,6 @@ def display_click_data(self, sub_res = ["", self.plot_grids.sub_fig] update_substation_layout_clicked_from_grid = 0 - style_action_buttons = {'display': 'none', "width": "70%"} - # check which call backs triggered this calls # see https://dash.plotly.com/advanced-callbacks # section "Determining which Input has fired with dash.callback_context" @@ -886,8 +926,7 @@ def display_click_data(self, style_storage_input, storage_id_clicked, *storage_res, style_line_input, line_id_clicked, *line_res, style_sub_input, sub_id_clicked, *sub_res[:-1], - update_substation_layout_clicked_from_grid, - style_action_buttons + update_substation_layout_clicked_from_grid ] else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] @@ -905,10 +944,6 @@ def display_click_data(self, self._last_sub_id = None else: # I clicked on the graph of the grid - - if self.env.mode != self.env.MODE_LEGACY: - style_action_buttons = {'display': 'flex', "width": "70%"} - self._last_sub_id = None obj_type, obj_id, res_type = self.plot_grids.get_object_clicked(clickData) if obj_type == "gen": @@ -943,8 +978,7 @@ def display_click_data(self, style_storage_input, storage_id_clicked, *storage_res, style_line_input, line_id_clicked, *line_res, style_sub_input, sub_id_clicked, *sub_res[:-1], - update_substation_layout_clicked_from_grid, - style_action_buttons + update_substation_layout_clicked_from_grid ] def format_path(self, path): @@ -1155,6 +1189,105 @@ def main_action_search(self, i_am_computing_state, i_am_computing_state] + def add_recommendation( + self, + recommendations_store, + agent_name, + agent_action, + ): + # Enable dash loading + self.env.start_recommendations_computation() + + # TODO: Handle the tree in a better way: + # Do not copy the env_tree, but instead add attributes to the Node and TemporalNodeData + # of the variant_node to control their visibility in the timeline graph + variant_env_tree = copy.deepcopy(self.env.env_tree) + + current_node = variant_env_tree.current_node + variant_node = copy.deepcopy(current_node) + current_node.father.add_son(agent_action, variant_node) + + obs, reward, done, info = current_node.get_obs_rewar_done_info() + overloads = (obs.rho[obs.rho > 1.0]).tolist() + max_rho = obs.rho.max() + holding_steps = self.env.nb_steps_from_node_until_end(current_node, variant_env_tree) + + # Go back to current_node + variant_env_tree.go_to_node(current_node) + + self.variant_env_trees.append( + { + "agent_name": agent_name, + "variant_env_tree": variant_env_tree, + } + ) + + # Disable dash loading + self.env.stop_recommendations_computation() + + d = { + 'Agent': [agent_name], + 'Overload': [str(overloads)], + 'Max Rho': [max_rho], + 'Holding Time': [holding_steps] + } + + new_recommendation = pd.DataFrame(data=d) + + if not recommendations_store: + # First recommendation + recommendations = new_recommendation + else: + # Add recommendation to stored recommendations + recommendations = pd.DataFrame.from_dict(recommendations_store) + recommendations = pd.concat( + [recommendations, new_recommendation], axis=0, ignore_index=True + ) + + # Return dataframe to fill the table + return recommendations + + + def fill_recommendations_table(self, recommendations): + recommendations_div = DataTable( + id="recommendations_table", + columns=[ + {"name": i, "id": i} for i in recommendations.columns + ], + data=recommendations.to_dict("records"), + style_table={"overflowX": "auto"}, + row_selectable="single", + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + }, + tooltip_data=[ + { + column: {"value": str(value), "type": "markdown"} + for column, value in row.items() + } + for row in recommendations.to_dict("rows") + ], + ) + return recommendations_div + + def get_selected_agent_name(self, selected_recommendation): + df = pd.DataFrame(selected_recommendation, index=[0]) + agent_name = df['Agent'].item() + self.logger.debug(f"selected_agent_name={agent_name}") + return agent_name + + def get_variant_tree(self, agent_name): + variant_tree = None + for variant_tree_dict in self.variant_env_trees: + variant_agent_name = variant_tree_dict.get("agent_name") + + if variant_agent_name == agent_name: + variant_tree = variant_tree_dict.get("variant_env_tree") + return variant_tree + + def handle_recommendations( self, # buttons @@ -1162,9 +1295,11 @@ def handle_recommendations( n_close, n_add_to_variants, n_apply, + n_integrate_manual_action, # recommendation container is_open, # stores + recommendations_store, selected_recommendation, recommendations_added_to_variant_trees, ): @@ -1176,12 +1311,12 @@ def handle_recommendations( recommendations_div = dash.no_update recommendations_container_open = dash.no_update - recommendations_store = dash.no_update recommendations_message = None variant_tree_added = dash.no_update if button_id == "close_recommendations_button": recommendations_added_to_variant_trees = dash.no_update + recommendations_store = dash.no_update # Reset issues self.env._current_issues = None @@ -1191,75 +1326,25 @@ def handle_recommendations( elif button_id == "show_more_issue": recommendations_added_to_variant_trees = dash.no_update - # Enable dash loading - self.env.start_recommendations_computation() - self.variant_env_trees = [] agent_name = self.format_path(os.path.abspath(self.assistant_path)) agent_action = self.env._assistant_action - # TODO: Handle the tree in a better way: - # Do not copy the env_tree, but instead add attributes to the Node and TemporalNodeData - # of the variant_node to control their visibility in the timeline graph - variant_env_tree = copy.deepcopy(self.env.env_tree) - current_node = variant_env_tree.current_node - variant_node = copy.deepcopy(current_node) - current_node.father.add_son(agent_action, variant_node) - - obs, reward, done, info = current_node.get_obs_rewar_done_info() - overloads = (obs.rho[obs.rho > 1.0]).tolist() - max_rho = obs.rho.max() - holding_steps = self.env.nb_steps_from_node_until_end(current_node, variant_env_tree) - - # Go back to current_node - variant_env_tree.go_to_node(current_node) - - self.variant_env_trees.append( - { - "agent_name": agent_name, - "variant_env_tree": variant_env_tree, - } + recommendations = self.add_recommendation( + recommendations_store, + agent_name, + agent_action ) - # Disable dash loading - self.env.stop_recommendations_computation() - - d = { - 'Agent': [agent_name], - 'Overload': [str(overloads)], - 'Max Rho': [max_rho], - 'Holding Time': [holding_steps] - } - - recommendations = pd.DataFrame(data=d) - - recommendations_div = DataTable( - id="recommendations_table", - columns=[ - {"name": i, "id": i} for i in recommendations.columns - ], - data=recommendations.to_dict("records"), - style_table={"overflowX": "auto"}, - row_selectable="single", - style_cell={ - "overflow": "hidden", - "textOverflow": "ellipsis", - "maxWidth": 0, - }, - tooltip_data=[ - { - column: {"value": str(value), "type": "markdown"} - for column, value in row.items() - } - for row in recommendations.to_dict("rows") - ], - ) + recommendations_div = self.fill_recommendations_table(recommendations) recommendations_container_open = True recommendations_store = recommendations.to_dict() elif button_id == "add_to_variant_trees_button": + recommendations_store = dash.no_update + # User didn't select a recommendation if not selected_recommendation: recommendations_message = "Please choose a recommendation" recommendations_added_to_variant_trees = dash.no_update @@ -1269,13 +1354,13 @@ def handle_recommendations( recommendations_added_to_variant_trees, variant_tree_added ] + # User first selection, init the selected list if not recommendations_added_to_variant_trees: recommendations_added_to_variant_trees = [] - selected_recommendation_df = pd.DataFrame(selected_recommendation, index=[0]) - selected_agent_name = selected_recommendation_df['Agent'].item() - self.logger.debug(f"selected_agent_name={selected_agent_name}") + selected_agent_name = self.get_selected_agent_name(selected_recommendation) + # Check if selection has already been added for recommendation_added in recommendations_added_to_variant_trees: recommendation_added_df = pd.DataFrame.from_dict(recommendation_added) # TODO: test on more columns than agent's name to handle multi recommendations by same agent @@ -1290,14 +1375,12 @@ def handle_recommendations( recommendations_added_to_variant_trees, variant_tree_added ] - selected_variant_tree = None - for variant_tree_dict in self.variant_env_trees: - variant_agent_name = variant_tree_dict.get("agent_name") - self.logger.debug(f"variant_agent_name={variant_agent_name}") - if variant_agent_name == selected_agent_name: - selected_variant_tree = variant_tree_dict.get("variant_env_tree") + # Retrieve the variant tree linked to the selected recommendation + selected_variant_tree = self.get_variant_tree(selected_agent_name) + # This shouldn't happen, log the error and inform the user if not selected_variant_tree: + self.logger.error(f"Variant tree not found") recommendations_message = "Variant tree not found", recommendations_added_to_variant_trees = dash.no_update return [ @@ -1306,6 +1389,7 @@ def handle_recommendations( recommendations_added_to_variant_trees, variant_tree_added ] + # Add selection to added recommandations recommendations_added_to_variant_trees.append(selected_recommendation) # Replace current env_tree by variant tree # TODO: Intead of duplicating the env_tree, create all variant nodes on the env_tree, @@ -1313,13 +1397,17 @@ def handle_recommendations( self.env.env_tree = selected_variant_tree recommendations_message = "Variant tree added !" + + # Trigger the chain: computation_wrapper > update_rt_fig to update the timeline_graph self._variant_tree_added += 1 variant_tree_added = self._variant_tree_added elif button_id == "apply_recommendation_button": + recommendations_store = dash.no_update # TODO: Intead of duplicating the env_tree, create all variant nodes on the env_tree # and remove the variant nodes that haven't been added by the user. + # User didn't select a recommendation if not selected_recommendation: recommendations_message = "Please choose a recommendation" recommendations_added_to_variant_trees = dash.no_update @@ -1329,18 +1417,14 @@ def handle_recommendations( recommendations_added_to_variant_trees, variant_tree_added ] - selected_recommendation_df = pd.DataFrame(selected_recommendation, index=[0]) - selected_agent_name = selected_recommendation_df['Agent'].item() - self.logger.debug(f"selected_agent_name={selected_agent_name}") + selected_agent_name = self.get_selected_agent_name(selected_recommendation) - selected_variant_tree = None - for variant_tree_dict in self.variant_env_trees: - variant_agent_name = variant_tree_dict.get("agent_name") - self.logger.debug(f"variant_agent_name={variant_agent_name}") - if variant_agent_name == selected_agent_name: - selected_variant_tree = variant_tree_dict.get("variant_env_tree") + # Retrieve the variant tree linked to the selected recommendation + selected_variant_tree = self.get_variant_tree(selected_agent_name) + # This shouldn't happen, log the error and inform the user if not selected_variant_tree: + self.logger.error(f"Variant tree not found") recommendations_message = "Variant tree not found", recommendations_added_to_variant_trees = dash.no_update return [ @@ -1362,6 +1446,26 @@ def handle_recommendations( # Collapse recommendations recommendations_container_open = False + elif button_id == "integrate_manual_action": + recommendations_added_to_variant_trees = dash.no_update + + # Add only if the recommandations are open + if not is_open: + self.logger.error("Recommandations aren't open !") + + agent_name = "Human" + agent_action = self.env.current_action + + recommendations = self.add_recommendation( + recommendations_store, + agent_name, + agent_action + ) + + recommendations_div = self.fill_recommendations_table(recommendations) + recommendations_container_open = True + recommendations_store = recommendations.to_dict() + else: raise PreventUpdate @@ -1374,7 +1478,7 @@ def handle_recommendations( variant_tree_added, ] - def loading_recommendations_table(self, n_clicks): + def loading_recommendations_table(self, n_show_more, n_integrate_manual_action): button_id = ctx.triggered_id if not button_id: diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index e7f115c..52230ad 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -63,8 +63,6 @@ def add_callbacks(dash_app, viz_server): Output("sub-id-clicked", "children"), # Output("graph_clicked_sub", "figure"), Output("update_substation_layout_clicked_from_grid", "n_clicks"), - - Output("action_buttons", "style"), ], [Input('real-time-graph', 'clickData'), Input("back-button", "n_clicks"), @@ -78,7 +76,8 @@ def add_callbacks(dash_app, viz_server): # handle display of the action, if needed dash_app.callback([Output("current_action", "children"), Output("which_action_button", "value"), - Output("update_substation_layout_clicked_from_sub", "n_clicks") + Output("update_substation_layout_clicked_from_sub", "n_clicks"), + Output("action_buttons", "style"), ], [Input("which_action_button", "value"), Input("do_display_action", "value"), @@ -90,8 +89,11 @@ def add_callbacks(dash_app, viz_server): Input("line-id-hidden", "children"), Input('line-status-input', "value"), Input('sub-id-hidden', "children"), - Input("graph_clicked_sub", "clickData") - ])(viz_server.display_action_fun) + Input("graph_clicked_sub", "clickData"), + Input('real-time-graph', 'clickData'), + ], + [State("recommendations_container", "is_open")] + )(viz_server.display_action_fun) # plot the substation that changes when we click dash_app.callback([Output("graph_clicked_sub", "figure")], @@ -289,9 +291,11 @@ def add_callbacks(dash_app, viz_server): Input("close_recommendations_button", "n_clicks"), Input("add_to_variant_trees_button", "n_clicks"), Input("apply_recommendation_button", "n_clicks"), + Input("integrate_manual_action", "n_clicks"), ], [ State("recommendations_container", "is_open"), + State("recommendations_store", "data"), State("selected_recommendation_store", "data"), State("recommendations_added_to_variant_trees_store", "data"), ] @@ -303,6 +307,7 @@ def add_callbacks(dash_app, viz_server): ], [ Input("show_more_issue", "n_clicks"), + Input("integrate_manual_action", "n_clicks"), ] )(viz_server.loading_recommendations_table) diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 73aa6ba..874ec00 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -599,7 +599,7 @@ def setupLayout(viz_server): children=[ dbc.Button( "Simulate", - id="simulate_manual_action", + id="simulate-button", className="ml-auto", n_clicks=0 ), @@ -855,7 +855,7 @@ def setupLayout(viz_server): ), dbc.Container([ dbc.Label( - 'recommendations', + 'Recommendations', style={ "padding": "5px" } From 8467290cc02d4aa2a97429a1f8ee2b5ac25cd7ed Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Mon, 18 Jul 2022 18:13:06 +0200 Subject: [PATCH 8/9] Add simulate recommendation button --- grid2game/VizServer.py | 44 ++++++++++++++++++++++--- grid2game/_utils/_temporal_callbacks.py | 2 ++ grid2game/_utils/_temporal_layout.py | 6 ++-- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index 8f7b57e..63b3eb1 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -329,10 +329,12 @@ def handle_act_on_env( until_game_over, untilgo_butt, self_loop, + timer, state_trigger_rt, state_trigger_for, state_trigger_self_loop, - timer + recommendations_container_is_open, + selected_recommendation, ): """ dash do not make "synch" callbacks (two callbacks can be called at the same time), @@ -395,6 +397,13 @@ def handle_act_on_env( self.env.next_computation = "simulate" self.env.next_computation_kwargs = {} self.need_update_figures = False + + if self.env.mode != self.env.MODE_LEGACY: + if recommendations_container_is_open: + selected_agent_name = self.get_selected_agent_name(selected_recommendation) + selected_action = self.get_variant_action(selected_agent_name) + self.logger.debug(f"Simulate selected action from agent {selected_agent_name}: {selected_action}") + self.env._current_action = selected_action elif button_id == "back-button": self.env.start_computation() self.env.next_computation = "back" @@ -1215,9 +1224,11 @@ def add_recommendation( # Go back to current_node variant_env_tree.go_to_node(current_node) + # TODO: handle several human recommendations self.variant_env_trees.append( { "agent_name": agent_name, + "agent_action": agent_action, "variant_env_tree": variant_env_tree, } ) @@ -1278,11 +1289,20 @@ def get_selected_agent_name(self, selected_recommendation): self.logger.debug(f"selected_agent_name={agent_name}") return agent_name + def get_variant_action(self, agent_name): + variant_action = None + for variant_tree_dict in self.variant_env_trees: + variant_agent_name = variant_tree_dict.get("agent_name") + # TODO: handle several human recommendations, this will retrieve the first human recommendation + if variant_agent_name == agent_name: + variant_action = variant_tree_dict.get("agent_action") + return variant_action + def get_variant_tree(self, agent_name): variant_tree = None for variant_tree_dict in self.variant_env_trees: variant_agent_name = variant_tree_dict.get("agent_name") - + # TODO: handle several human recommendations, this will retrieve the first human recommendation if variant_agent_name == agent_name: variant_tree = variant_tree_dict.get("variant_env_tree") return variant_tree @@ -1315,9 +1335,14 @@ def handle_recommendations( variant_tree_added = dash.no_update if button_id == "close_recommendations_button": - recommendations_added_to_variant_trees = dash.no_update - recommendations_store = dash.no_update + # TODO: restore self._current_action in case of simulations + + # Reset message + recommendations_message = "" + # Reset stores + recommendations_store = None + recommendations_added_to_variant_trees = None # Reset issues self.env._current_issues = None # Collapse recommendations @@ -1466,6 +1491,17 @@ def handle_recommendations( recommendations_container_open = True recommendations_store = recommendations.to_dict() + # User didn't select a recommendation + if not selected_recommendation: + recommendations_message = "Please choose a recommendation" + recommendations_added_to_variant_trees = dash.no_update + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + else: raise PreventUpdate diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index 52230ad..7930140 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -134,6 +134,8 @@ def add_callbacks(dash_app, viz_server): [State("act_on_env_trigger_rt", "n_clicks"), State("act_on_env_trigger_for", "n_clicks"), State("act_on_env_call_selfloop", "value"), + State("recommendations_container", "is_open"), + State("selected_recommendation_store", "data"), ] )(viz_server.handle_act_on_env) diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 874ec00..20604a8 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -873,8 +873,8 @@ def setupLayout(viz_server): html.Div( children=[ dbc.Button( - "Show details", - id="show_recommendation_details_button", + "Simulate", + id="simulate-button", className="ml-auto", n_clicks=0 ), @@ -921,7 +921,7 @@ def setupLayout(viz_server): children=[ dbc.Button( "Explore", - id="show_details", + id="expert_agent_button", className="ml-auto", n_clicks=0 ) From 6d8b9199f658ad184fc272097966c08222a98979 Mon Sep 17 00:00:00 2001 From: Paul-Marie RAFFI Date: Thu, 21 Jul 2022 16:07:40 +0200 Subject: [PATCH 9/9] Add Expert Assistance - add expert assist tab with basic view from grid2viz/src/simulation/ExpertAssist.py this tab interacts with the recommendations - add some user input persistence through tab switch - prevent initial call on most of CAB's callbacks to fix bugs --- .gitignore | 3 + README.md | 16 +- grid2game/VizServer.py | 147 +++++++-- grid2game/_utils/_temporal_callbacks.py | 68 ++-- grid2game/_utils/_temporal_layout.py | 422 +++++++++++++----------- grid2game/_utils/main_layout.py | 6 +- grid2game/envs/env.py | 7 +- grid2game/expert/BaseAssistant.py | 132 ++++++++ grid2game/expert/ExpertAssist.py | 354 ++++++++++++++++++++ grid2game/expert/__init__.py | 0 requirements.txt | 4 +- setup.py | 1 + 12 files changed, 905 insertions(+), 255 deletions(-) create mode 100644 grid2game/expert/BaseAssistant.py create mode 100644 grid2game/expert/ExpertAssist.py create mode 100644 grid2game/expert/__init__.py diff --git a/.gitignore b/.gitignore index eb7cfc3..9706deb 100644 --- a/.gitignore +++ b/.gitignore @@ -146,3 +146,6 @@ g2op_env_customization.py g2op_params.json **/~$*.pptx assistant_test/ + +# outputs of ExpertOp4Grid +*.csv diff --git a/README.md b/README.md index f0490ee..23ff38b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It allows human and possibly an "assistant" (aka an "agent", for example develop ## Disclaimer This package is still in "early alpha" mode. -Performances are not that great and some useful features are still missing. The layout is also relatively "ugly" +Performances are not that great and some useful features are still missing. The layout is also relatively "ugly" at the moment. @@ -16,7 +16,7 @@ at the moment. To install the package, you need to install it from source: ```commandline python3 -m pip install git+https://github.com/BDonnot/grid2game.git -``` +``` An alternative is to clone the repository, and then install it from there: ```commandline @@ -25,6 +25,12 @@ cd grid2game python3 install -e . ``` +To enable Expert Assistance, you need to install ExpertOp4Grid from source: +```commandline +python3 -m pip install https://github.com/marota/ExpertOp4Grid.git +``` + + **IMPORTANT** In all cases, we highly recommend to perform these modifications inside a python virtual environment (for example using `conda create -n grid2game_env; conda activate grid2game_env` or `python -m virtualenv grid2game_env; source/grid2game_env/bin/activate`). **NOTE** It will install some dependencies and will require, in particular grid2op >= 1.6.4. The use of `lightsim2grid `_ is also highly recommended. @@ -42,7 +48,7 @@ grid2game --dev --env_name educ_case14_storage --is_test - `--dev` specifies that the dash server will run in "dev" mode, we recommend you to use it - `--env_name educ_case14_storage` specifies that the application will run the `educ_case14_storage` environment -- `--is_test` specifies that the grid2op environment will be built with `test=True` (so in this +- `--is_test` specifies that the grid2op environment will be built with `test=True` (so in this case `grid2op.make("educ_case14_storage", test=True)) You can also add more parameters: @@ -56,10 +62,10 @@ You can also add more parameters: assistant can be loaded after the interface is started. - `--assistant_seed SEED` allows you to specify the seed used by your agent (for reproductibility) Depending on how you agent is coded, this might not work. This only calls `your_agent.seed(SEED)`. -- `--g2op_param ./g2op_params.json` set of parameters used to update the environment (this should be compatible +- `--g2op_param ./g2op_params.json` set of parameters used to update the environment (this should be compatible with `param.init_from_json` from grid2op) - `--g2op_config ./g2op_env_customization.py` how to configure the grid2op environment, this file should contain - a dictionnary named `env_config` and it will be used to initialize the grid2Op environment with : + a dictionnary named `env_config` and it will be used to initialize the grid2Op environment with : `env.make(..., **env_config)` For example, a more complete command line would be: diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index 63b3eb1..f5eacb3 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -11,6 +11,8 @@ import time import pandas as pd import copy +import warnings +import numpy as np import dash @@ -29,6 +31,18 @@ from grid2game.envs import Env from grid2game.plot import PlotGrids, PlotTemporalSeries +try: + from grid2game.expert.ExpertAssist import Assist +except (ImportError, ModuleNotFoundError) as e: + print("Cannot load assistant") + print(e) + from grid2game.expert.BaseAssistant import EmptyAssist as Assist + + warnings.warn( + "ExpertOp4Grid is not installed and the assist feature will not be available." + " To use the Assist feature, you can install ExpertOp4Grid by " + "\n\t{} -m pip install ExpertOp4Grid\n".format(sys.executable) + ) class VizServer: SELF_LOOP_STOP = 0 @@ -220,16 +234,30 @@ def __init__(self, value='tab-explore-action', children=self._layout_action_search) - tmp_ = setupLayout(self, - self._layout_temporal_tab, - self._layout_action_search_tab) + # This layout has neither "choose or assist" nor the graph + self.expert_assistant = Assist(env=self.env) + self._layout_expert_assist = self.expert_assistant.layout() + self._layout_expert_assist_tab = dcc.Tab( + label='Expert Assist', + value='tab-expert-assist', + children=self._layout_expert_assist + ) + + tmp_ = setupLayout( + self, + self._layout_temporal_tab, + self._layout_action_search_tab, + self._layout_expert_assist_tab, + ) self.my_app.layout = tmp_ + self.expert_assistant.register_callbacks(self.my_app) add_callbacks_temporal(self.my_app, self) add_callbacks_action_search(self.my_app, self) add_callbacks(self.my_app, self) + self.logger.info("Viz server initialized") # last node id (to not plot twice the same stuff to gain time) @@ -240,6 +268,8 @@ def __init__(self, self._do_display_action = True self._dropdown_value = "assistant" + self._open_tab = 0 + def _make_glop_env_config(self, build_args): g2op_config = {} @@ -327,6 +357,7 @@ def handle_act_on_env( go_butt, gofast_clicks, until_game_over, + until_game_over_auto, untilgo_butt, self_loop, timer, @@ -378,7 +409,7 @@ def handle_act_on_env( self.need_update_figures = False self._check_issue += 1 check_issue = self._check_issue - elif button_id == "go_till_game_over-button": + elif button_id in ["go_till_game_over-button", "go_till_game_over_auto-button"]: self.env.start_computation() self.env.next_computation = "step_end" self.env.next_computation_kwargs = {} @@ -483,6 +514,7 @@ def handle_act_on_env( self._go_button_shape, self._gofast_button_shape, self._go_till_go_button_shape, + self._go_till_go_button_shape, i_am_computing_state, i_am_computing_state, change_graph_title, @@ -895,14 +927,17 @@ def display_grid_substation(self, update_substation_layout_clicked_from_sub, upd sub_res = self.plot_grids.update_sub_figure(self.env._current_action, self._last_sub_id) return [sub_res[-1]] - def display_click_data(self, - clickData, - back_clicked, - step_clicked, - simulate_clicked, - go_clicked, - gofast_clicked, - until_gameover): + def display_click_data( + self, + clickData, + back_clicked, + step_clicked, + simulate_clicked, + go_clicked, + gofast_clicked, + until_gameover, + until_gameover_auto, + ): """display the interaction window when the real time graph is clicked on""" do_display_action = 0 gen_redisp_curtail = "" @@ -1109,12 +1144,17 @@ def timeline_set_time(self, time_line_graph_clicked): def tab_content_display(self, tab): res = [self._layout_temporal] + self.logger.debug(f"tab_content_display: tab={tab}") + if tab == 'tab-temporal-view': self.need_update_figures = True return [self._layout_temporal] elif tab == 'tab-explore-action': self.need_update_figures = True return [self._layout_action_search] + elif tab == 'tab-expert-assist': + self.need_update_figures = True + return [self._layout_expert_assist] else: msg_ = f"Unknown tab {tab}" self.logger.error(msg_) @@ -1268,6 +1308,7 @@ def fill_recommendations_table(self, recommendations): data=recommendations.to_dict("records"), style_table={"overflowX": "auto"}, row_selectable="single", + sort_action="native", style_cell={ "overflow": "hidden", "textOverflow": "ellipsis", @@ -1316,6 +1357,8 @@ def handle_recommendations( n_add_to_variants, n_apply, n_integrate_manual_action, + n_add_to_knowledge_base_button, + n_add_expert_recommendation, # recommendation container is_open, # stores @@ -1324,8 +1367,12 @@ def handle_recommendations( recommendations_added_to_variant_trees, ): button_id = ctx.triggered_id + self.logger.debug(f"handle_recommendations: triggered_id = {button_id}") + if self.env.expert_selected_action: + button_id = "add_expert_recommendation" + if not button_id: raise PreventUpdate @@ -1414,7 +1461,7 @@ def handle_recommendations( recommendations_added_to_variant_trees, variant_tree_added ] - # Add selection to added recommandations + # Add selection to added recommendations recommendations_added_to_variant_trees.append(selected_recommendation) # Replace current env_tree by variant tree # TODO: Intead of duplicating the env_tree, create all variant nodes on the env_tree, @@ -1474,9 +1521,9 @@ def handle_recommendations( elif button_id == "integrate_manual_action": recommendations_added_to_variant_trees = dash.no_update - # Add only if the recommandations are open + # Add only if the recommendations are open if not is_open: - self.logger.error("Recommandations aren't open !") + self.logger.error("Recommendations aren't open !") agent_name = "Human" agent_action = self.env.current_action @@ -1491,16 +1538,36 @@ def handle_recommendations( recommendations_container_open = True recommendations_store = recommendations.to_dict() - # User didn't select a recommendation - if not selected_recommendation: - recommendations_message = "Please choose a recommendation" - recommendations_added_to_variant_trees = dash.no_update - return [ - recommendations_div, recommendations_container_open, - recommendations_store, recommendations_message, - recommendations_added_to_variant_trees, variant_tree_added - ] + elif button_id == "add_to_knowledge_base_button": + recommendations_store = dash.no_update + recommendations_added_to_variant_trees = dash.no_update + recommendations_message = "Not implemented" + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + elif button_id == "add_expert_recommendation": + recommendations_added_to_variant_trees = dash.no_update + + agent_name = "Expert" + # Get action from expert tab + agent_action = self.env.expert_selected_action + + recommendations = self.add_recommendation( + recommendations_store, + agent_name, + agent_action + ) + + recommendations_div = self.fill_recommendations_table(recommendations) + recommendations_container_open = True + recommendations_store = recommendations.to_dict() + + # reset expert_selected_action + self.env.expert_selected_action = None else: raise PreventUpdate @@ -1514,10 +1581,14 @@ def handle_recommendations( variant_tree_added, ] - def loading_recommendations_table(self, n_show_more, n_integrate_manual_action): + def loading_recommendations_table( + self, + n_show_more, + n_integrate_manual_action, + ): button_id = ctx.triggered_id - if not button_id: + if not button_id and not self.env.expert_selected_action: raise PreventUpdate time.sleep(0.1) @@ -1537,7 +1608,15 @@ def select_recommendation( selected_recommendation = recommendations.iloc[selected_recommendation_index] return [selected_recommendation.to_dict()] - def dropdown_mode(self, mode, manual_is_open, auto_is_open): + def dropdown_mode( + self, + mode, + manual_is_open, + auto_is_open, + ): + + button_id = ctx.triggered_id + self.logger.debug(f"dropdown_mode: triggered_id = {button_id} mode = {mode}") self.env.mode = mode @@ -1550,3 +1629,17 @@ def dropdown_mode(self, mode, manual_is_open, auto_is_open): auto_is_open = True return [manual_is_open, auto_is_open] + + def open_tab(self, explore_click, add_expert_recommendation_click): + button_id = ctx.triggered_id + + self.logger.debug(f"open_tab: triggered_id = {button_id} open_tab = {explore_click} close_tab = {add_expert_recommendation_click}") + + if not button_id: + return ['tab-temporal-view'] + elif button_id == "expert_agent_button" and explore_click: + return ['tab-expert-assist'] + elif button_id == "add_expert_recommendation" and add_expert_recommendation_click: + return ['tab-temporal-view'] + else: + return [dash.no_update] \ No newline at end of file diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index 7930140..09e8020 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -71,6 +71,7 @@ def add_callbacks(dash_app, viz_server): Input("go-button", "n_clicks"), Input("gofast-button", "n_clicks"), Input("go_till_game_over-button", "n_clicks"), + Input("go_till_game_over_auto-button", "n_clicks"), ])(viz_server.display_click_data) # handle display of the action, if needed @@ -114,6 +115,7 @@ def add_callbacks(dash_app, viz_server): Output("go-button", "className"), Output("gofast-button", "className"), Output("go_till_game_over-button", "className"), + Output("go_till_game_over_auto-button", "className"), Output("is_computing_left", "style"), Output("is_computing_right", "style"), Output("change_graph_title", "n_clicks"), @@ -127,6 +129,7 @@ def add_callbacks(dash_app, viz_server): Input("go-button", "n_clicks"), Input("gofast-button", "n_clicks"), Input("go_till_game_over-button", "n_clicks"), + Input("go_till_game_over_auto-button", "n_clicks"), Input("untilgo_butt_call_act_on_env", "value"), Input("selfloop_call_act_on_env", "value"), Input("timer", "n_intervals"), @@ -213,18 +216,22 @@ def add_callbacks(dash_app, viz_server): if viz_server._app_heroku is False: # this is deactivated on heroku at the moment ! # load the assistant - dash_app.callback([Output("current_assistant_path", "children"), - Output("clear_assistant_path", "n_clicks"), - Output("loading_assistant_output", "children"), - ], - [Input("load_assistant_button", "n_clicks") - ], - [State("select_assistant", "value")] - )(viz_server.load_assistant) - - dash_app.callback([Output("select_assistant", "value")], - [Input("clear_assistant_path", "n_clicks")] - )(viz_server.clear_loading) + dash_app.callback( + [ + Output("current_assistant_path", "children"), + Output("clear_assistant_path", "n_clicks"), + Output("loading_assistant_output", "children"), + ], + [Input("load_assistant_button", "n_clicks")], + [State("select_assistant", "value")], + prevent_initial_call=True, + )(viz_server.load_assistant) + + dash_app.callback( + [Output("select_assistant", "value")], + [Input("clear_assistant_path", "n_clicks")], + prevent_initial_call=True, + )(viz_server.clear_loading) # save the current experiment dash_app.callback([Output("current_save_path", "children"), @@ -275,7 +282,8 @@ def add_callbacks(dash_app, viz_server): ], [ State("modal_issue", "is_open"), - ] + ], + prevent_initial_call=True, )(viz_server.check_issue) # show the recommendations table @@ -294,13 +302,16 @@ def add_callbacks(dash_app, viz_server): Input("add_to_variant_trees_button", "n_clicks"), Input("apply_recommendation_button", "n_clicks"), Input("integrate_manual_action", "n_clicks"), + Input("add_to_knowledge_base_button", "n_clicks"), + Input("add_expert_recommendation", "n_clicks"), ], [ State("recommendations_container", "is_open"), State("recommendations_store", "data"), State("selected_recommendation_store", "data"), State("recommendations_added_to_variant_trees_store", "data"), - ] + ], + # prevent_initial_call=True, )(viz_server.handle_recommendations) dash_app.callback( @@ -310,21 +321,10 @@ def add_callbacks(dash_app, viz_server): [ Input("show_more_issue", "n_clicks"), Input("integrate_manual_action", "n_clicks"), - ] + ], + # prevent_initial_call=True, )(viz_server.loading_recommendations_table) - # dash_app.callback( - # [ - # Output("recommendations_message", "children"), - # ], - # [ - # Input("apply_recommendation_button", "n_clicks"), - # ], - # [ - # State("selected_recommendation_store", "data"), - # ], - # )(viz_server.apply_recommendation) - dash_app.callback( [ Output("selected_recommendation_store", "data"), @@ -335,6 +335,7 @@ def add_callbacks(dash_app, viz_server): [ State("recommendations_store", "data"), ], + prevent_initial_call=True, )(viz_server.select_recommendation) # dropdown mode @@ -349,5 +350,16 @@ def add_callbacks(dash_app, viz_server): [ State("controls_manual_collapse", "is_open"), State("controls_auto_collapse", "is_open"), - ] + ], )(viz_server.dropdown_mode) + + dash_app.callback( + [ + dash.dependencies.Output('tabs-main-view', 'value'), + ], + [ + dash.dependencies.Input('expert_agent_button', 'n_clicks'), + dash.dependencies.Input('add_expert_recommendation', 'n_clicks'), + ], + prevent_initial_call=True, + )(viz_server.open_tab) diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index 20604a8..b4445b2 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -37,7 +37,8 @@ def setupLayout(viz_server): options=[ {"value": el, "label": el} for el in viz_server.env.list_chronics() - ] + ], + persistence=True, ) ], id="chronics_dropdown", @@ -46,13 +47,17 @@ def setupLayout(viz_server): id="chronics_selector", style={"display": "flex", "minWidth": "20%", "marginRight": "10", "marginLeft": "10"} ), - html.Div([ - dcc.Input(id="set_seed", - type="number", - placeholder="Select a seed", - ), - ], - id="seed_selector"), + html.Div( + [ + dcc.Input( + id="set_seed", + type="number", + placeholder="Select a seed", + persistence=True, + ), + ], + id="seed_selector", + ), html.Div([ html.Div( [ @@ -63,8 +68,9 @@ def setupLayout(viz_server): {"value": value, "label": label} for value, label in viz_server.env.list_modes() ], - # Default value - value=viz_server.env.default_mode, + value=viz_server.env.mode, + # Persist user choice to tab switch or page reload + persistence=True, ) ], id="mode_dropdown", @@ -113,6 +119,12 @@ def setupLayout(viz_server): n_clicks=0, className="btn btn-primary", ) + # Button for MODE_RECOMMAND and MODE_ASSISTANT + go_till_game_over_auto = html.Label("Go to End", + id="go_till_game_over_auto-button", + n_clicks=0, + className="btn btn-primary", + ) # html display # see https://dash.plotly.com/dash-core-components/loading # [dcc.Loading(id="loading_go_fast_", type="circle", children=html.Div(id="loading_go_fast_output"))] @@ -133,72 +145,87 @@ def setupLayout(viz_server): # TODO make that disapearing / appearing based on a button "show options" for example line_info_label = html.Label("Line unit:") - line_info = dcc.Dropdown(id='line-info-dropdown', - options=[ - {'label': 'Capacity', 'value': 'rho'}, - {'label': 'A', 'value': 'a'}, - {'label': 'MW', 'value': 'p'}, - {'label': 'kV', 'value': 'v'}, - {'label': 'MVAr', 'value': 'q'}, - {'label': 'thermal limit', 'value': 'th_lim'}, - {'label': 'cooldown', 'value': 'cooldown'}, - {'label': '# step overflow', 'value': 'timestep_overflow'}, - {'label': 'name', 'value': 'name'}, - {'label': 'None', 'value': 'none'}, - ], - value='none', - clearable=False) + line_info = dcc.Dropdown( + id='line-info-dropdown', + options=[ + {'label': 'Capacity', 'value': 'rho'}, + {'label': 'A', 'value': 'a'}, + {'label': 'MW', 'value': 'p'}, + {'label': 'kV', 'value': 'v'}, + {'label': 'MVAr', 'value': 'q'}, + {'label': 'thermal limit', 'value': 'th_lim'}, + {'label': 'cooldown', 'value': 'cooldown'}, + {'label': '# step overflow', 'value': 'timestep_overflow'}, + {'label': 'name', 'value': 'name'}, + {'label': 'None', 'value': 'none'}, + ], + value='none', + clearable=False, + persistence=True, + ) line_side_label = html.Label("Line side:") - line_side = dcc.Dropdown(id='line-side-dropdown', - options=[ - {'label': 'Origin', 'value': 'or'}, - {'label': 'Extremity', 'value': 'ex'}, - {'label': 'Both', 'value': 'both'}, - {'label': 'None', 'value': 'none'}, - ], - value='or', - clearable=False) + line_side = dcc.Dropdown( + id='line-side-dropdown', + options=[ + {'label': 'Origin', 'value': 'or'}, + {'label': 'Extremity', 'value': 'ex'}, + {'label': 'Both', 'value': 'both'}, + {'label': 'None', 'value': 'none'}, + ], + value='or', + clearable=False, + persistence=True, + ) load_info_label = html.Label("Load unit:") - load_info = dcc.Dropdown(id='load-info-dropdown', - options=[ - {'label': 'MW', 'value': 'p'}, - {'label': 'kV', 'value': 'v'}, - {'label': 'MVar', 'value': 'q'}, - {'label': 'name', 'value': 'name'}, - {'label': 'None', 'value': 'none'}, - ], - value='none', - clearable=False) + load_info = dcc.Dropdown( + id='load-info-dropdown', + options=[ + {'label': 'MW', 'value': 'p'}, + {'label': 'kV', 'value': 'v'}, + {'label': 'MVar', 'value': 'q'}, + {'label': 'name', 'value': 'name'}, + {'label': 'None', 'value': 'none'}, + ], + value='none', + clearable=False, + persistence=True, + ) # load_info_div = html.Div(id="load-info", children=[load_info_label, load_info]) gen_info_label = html.Label("Gen. unit:") - gen_info = dcc.Dropdown(id='gen-info-dropdown', - options=[ - {'label': 'MW', 'value': 'p'}, - {'label': 'kV', 'value': 'v'}, - {'label': 'MVar', 'value': 'q'}, - {'label': 'name', 'value': 'name'}, - {'label': 'type', 'value': 'type'}, - {'label': 'ramp_down', 'value': 'ramp_down'}, - {'label': 'ramp_up', 'value': 'ramp_up'}, - {'label': 'target_dispatch', 'value': 'target_dispatch'}, - {'label': 'actual_dispatch', 'value': 'actual_dispatch'}, - {'label': 'None', 'value': 'none'}, - ], - value='none', - clearable=False) + gen_info = dcc.Dropdown( + id='gen-info-dropdown', + options=[ + {'label': 'MW', 'value': 'p'}, + {'label': 'kV', 'value': 'v'}, + {'label': 'MVar', 'value': 'q'}, + {'label': 'name', 'value': 'name'}, + {'label': 'type', 'value': 'type'}, + {'label': 'ramp_down', 'value': 'ramp_down'}, + {'label': 'ramp_up', 'value': 'ramp_up'}, + {'label': 'target_dispatch', 'value': 'target_dispatch'}, + {'label': 'actual_dispatch', 'value': 'actual_dispatch'}, + {'label': 'None', 'value': 'none'}, + ], + value='none', + clearable=False, + persistence=True, + ) stor_info_label = html.Label("Stor. unit:") - stor_info = dcc.Dropdown(id='stor-info-dropdown', - options=[ - {'label': 'MW', 'value': 'p'}, - {'label': 'MWh', 'value': 'MWh'}, - {'label': 'None', 'value': 'none'}, - ], - value='none', - clearable=False) + stor_info = dcc.Dropdown( + id='stor-info-dropdown', + options=[ + {'label': 'MW', 'value': 'p'}, + {'label': 'MWh', 'value': 'MWh'}, + {'label': 'None', 'value': 'none'}, + ], + value='none', + clearable=False, + persistence=True, + ) button_css_class = "unit_buttons" style_button = {"minWidth": "15%"} lineinfo_col = html.Div(id="lineinfo-col", @@ -238,104 +265,124 @@ def setupLayout(viz_server): save_txt = 'Where do you want to save the current experiment?' btn_assistant_save = "btn btn-primary" - select_assistant = html.Div(id='select_assistant_box', - children=[html.Div(children=[dcc.Input(placeholder=assistant_txt, - id="select_assistant", - type="text", - style={ - 'width': '70%', - 'lineHeight': '55px', - 'verticalAlign': 'middle', - } - ), - html.P(viz_server.format_path(viz_server.assistant_path), - id="current_assistant_path", - style={'width': '28%', - 'textAlign': 'center', - 'verticalAlign': 'middle', - "margin": "0", - } - ), - dcc.Loading(id="loading_assistant", - type="default", - children=html.Div(id="loading_assistant_output") - ) - ], - style={'borderWidth': '1px', - 'borderStyle': 'dashed', - 'borderRadius': '5px', - 'width': '100%', - "display": "flex", - "alignItems":"center", - "paddingTop": "5px", - "paddingBottom": "5px", - "paddingLeft": "2px", - "paddingRight": "2px" - } - ), - html.Label("load", - id="load_assistant_button", - n_clicks=0, - className=btn_assistant_save, - style={ 'width': '100%', } - ), - ] - ) - - save_experiment = html.Div(id='save_expe_box', - children=[ - html.Div(children=[ - dcc.Input(placeholder=save_txt, - id="save_expe", - type="text", - style={ - 'width': '70%', - 'height': '55px', - 'lineHeight': '55px', - 'verticalAlign': 'middle', - # "margin-top": 5, - # "margin-left": 20 - }), - html.P(viz_server.format_path(viz_server.assistant_path), - id="current_save_path", - style={'width': '28%', - 'textAlign': 'center', - 'height': '55px', - 'verticalAlign': 'middle', - "margin": "0", - # "margin-top": 20 - } - ), - dcc.Loading(id="loading_save", - type="default", - children=html.Div(id="loading_save_output") - ) - ], - style={ - 'borderWidth': '1px', - 'borderStyle': 'dashed', - 'borderRadius': '5px', - 'textAlign': 'center', - "display": "flex", - "alignItems":"center", - # "padding": "2px", - "paddingTop": "5px", - "paddingBottom": "5px", - "paddingLeft": "2px", - "paddingRight": "2px" - # 'margin': '10px' - } - ), - html.Label("save", - id="save_expe_button", - n_clicks=0, - className=btn_assistant_save, - style={'height': '35px', - 'width': '100%', - } - ), - ] - ) + select_assistant = html.Div( + id='select_assistant_box', + children=[ + html.Div( + children=[ + dcc.Input( + placeholder=assistant_txt, + id="select_assistant", + type="text", + style={ + 'width': '70%', + 'lineHeight': '55px', + 'verticalAlign': 'middle', + }, + persistence=True, + ), + html.P( + viz_server.format_path(viz_server.assistant_path), + id="current_assistant_path", + style={'width': '28%', + 'textAlign': 'center', + 'verticalAlign': 'middle', + "margin": "0", + } + ), + dcc.Loading( + id="loading_assistant", + type="default", + children=html.Div(id="loading_assistant_output") + ) + ], + style={ + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'width': '100%', + "display": "flex", + "alignItems":"center", + "paddingTop": "5px", + "paddingBottom": "5px", + "paddingLeft": "2px", + "paddingRight": "2px" + } + ), + html.Label( + "load", + id="load_assistant_button", + n_clicks=0, + className=btn_assistant_save, + style={ 'width': '100%', } + ), + ] + ) + + save_experiment = html.Div( + id='save_expe_box', + children=[ + html.Div( + children=[ + dcc.Input( + placeholder=save_txt, + id="save_expe", + type="text", + style={ + 'width': '70%', + 'height': '55px', + 'lineHeight': '55px', + 'verticalAlign': 'middle', + # "margin-top": 5, + # "margin-left": 20 + }, + persistence=True, + ), + html.P( + viz_server.format_path(viz_server.assistant_path), + id="current_save_path", + style={ + 'width': '28%', + 'textAlign': 'center', + 'height': '55px', + 'verticalAlign': 'middle', + "margin": "0", + # "margin-top": 20 + } + ), + dcc.Loading( + id="loading_save", + type="default", + children=html.Div(id="loading_save_output") + ) + ], + style={ + 'borderWidth': '1px', + 'borderStyle': 'dashed', + 'borderRadius': '5px', + 'textAlign': 'center', + "display": "flex", + "alignItems":"center", + # "padding": "2px", + "paddingTop": "5px", + "paddingBottom": "5px", + "paddingLeft": "2px", + "paddingRight": "2px" + # 'margin': '10px' + } + ), + html.Label( + "save", + id="save_expe_button", + n_clicks=0, + className=btn_assistant_save, + style={ + 'height': '35px', + 'width': '100%', + } + ), + ] + ) controls_manual_row = html.Div(id="control-manual-buttons", # className="row", @@ -357,7 +404,7 @@ def setupLayout(viz_server): controls_auto_row = html.Div(id="control-auto-buttons", # className="row", children=[ - go_till_game_over, + go_till_game_over_auto, ], style={'justifyContent': 'space-between', "display": "flex"} @@ -489,15 +536,18 @@ def setupLayout(viz_server): # ### Action widget current_action = html.Pre(id="current_action") - which_action_button = dcc.Dropdown(id='which_action_button', - options=[ - {'label': 'do nothing', 'value': 'dn'}, - {'label': 'previous', 'value': 'prev'}, - {'label': 'assistant', 'value': 'assistant'}, - {'label': 'manual', 'value': 'manual'}, - ], - value='assistant', - clearable=False) + which_action_button = dcc.Dropdown( + id='which_action_button', + options=[ + {'label': 'do nothing', 'value': 'dn'}, + {'label': 'previous', 'value': 'prev'}, + {'label': 'assistant', 'value': 'assistant'}, + {'label': 'manual', 'value': 'manual'}, + ], + value='assistant', + clearable=False, + persistence=True, + ) action_css = "col-12 col-sm-12 col-md-12 col-lg-12 col-xl-5 " \ "order-first order-sm-first order-md-first order-xl-last" action_css = "six columns" @@ -782,7 +832,9 @@ def setupLayout(viz_server): ) # triggering the update of the figures + add_expert_recommendation = html.Label("", id="add_expert_recommendation", n_clicks=0) check_issue = html.Label("", id="check_issue", n_clicks=0) + show_more_issue = html.Label("", id="show_more_issue", n_clicks=0) variant_tree_added = html.Label("", id="variant_tree_added", n_clicks=0) act_on_env_trigger_rt = html.Label("", id="act_on_env_trigger_rt", n_clicks=0) act_on_env_trigger_for = html.Label("", id="act_on_env_trigger_for", n_clicks=0) @@ -815,7 +867,9 @@ def setupLayout(viz_server): trigger_rt_extra_info, trigger_for_extra_info, update_progress_bar_from_act, update_progress_bar_from_figs, check_issue, + show_more_issue, variant_tree_added, + add_expert_recommendation, ], id="hidden_buttons_for_callbacks", style={'display': 'none'}) @@ -920,9 +974,8 @@ def setupLayout(viz_server): html.Div( children=[ dbc.Button( - "Explore", + "Expert Assist", id="expert_agent_button", - className="ml-auto", n_clicks=0 ) ], @@ -948,7 +1001,7 @@ def setupLayout(viz_server): is_open=False, style={ "paddingBottom": "20px" - } + }, ) loading_recommendations = dcc.Loading( @@ -957,16 +1010,6 @@ def setupLayout(viz_server): children=html.Div(id="loading_recommendations_output"), ) - recommendations_store = dcc.Store( - id="recommendations_store" - ) - selected_recommendation_store = dcc.Store( - id="selected_recommendation_store" - ) - recommendations_added_to_variant_trees_store = dcc.Store( - id="recommendations_added_to_variant_trees_store" - ) - # Final page layout_css = "container-fluid h-100 d-md-flex d-xl-flex flex-md-column flex-xl-column" layout = html.Div(id="grid2game", @@ -993,9 +1036,6 @@ def setupLayout(viz_server): hidden_interactions, timer_callbacks, modal_issue, - recommendations_store, - selected_recommendation_store, - recommendations_added_to_variant_trees_store, ]) return layout diff --git a/grid2game/_utils/main_layout.py b/grid2game/_utils/main_layout.py index 2c8666a..f7a035c 100644 --- a/grid2game/_utils/main_layout.py +++ b/grid2game/_utils/main_layout.py @@ -20,6 +20,10 @@ def setupLayout(viz_server, *tabs): # children=tabs children=children ), - html.Div(id='tabs-content-main-view') + html.Div(id='tabs-content-main-view'), + # Stores for persistence through tab navigation + dcc.Store(id="recommendations_store"), + dcc.Store(id="selected_recommendation_store"), + dcc.Store(id="recommendations_added_to_variant_trees_store"), ]) return layout diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index 08498e6..3929996 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -93,6 +93,9 @@ def __init__(self, self._assistant_seed = assistant_seed self.load_assistant(assistant_path) + # expert operator + self.expert_selected_action = None + self.init_state() # to control which action will be done when @@ -102,8 +105,8 @@ def __init__(self, # actions to explore self.all_topo_actions = None - self.default_mode = self.MODE_LEGACY - self.mode = self.default_mode + # Default mode + self.mode = self.MODE_LEGACY def is_assistant_illegal(self): if "is_illegal" in self._sim_info: diff --git a/grid2game/expert/BaseAssistant.py b/grid2game/expert/BaseAssistant.py new file mode 100644 index 0000000..9071d80 --- /dev/null +++ b/grid2game/expert/BaseAssistant.py @@ -0,0 +1,132 @@ +from abc import abstractmethod, ABC +from itertools import chain + +from dash import dcc +from dash import html + + +class BaseAssistant(ABC): + def __init__(self): + self._layout = None + + @abstractmethod + def layout(self, *args): + pass + + @abstractmethod + def register_callbacks(self, app): + pass + + @abstractmethod + def store_to_graph(self, store_data): + """ + Returns a Grid2Op network graph for the data stored in store_data + Parameters + ---------- + store_data + + Returns + ------- + + """ + pass + + def register_layout(self, *layout_args, layout_to_check_against=None): + self._layout = self.layout(*layout_args) + self.check_layout(layout_to_check_against) + return self._layout + + def check_layout(self, layout_to_check_against): + try: + if ( + self._layout.children[0]._type != "Store" + or self._layout.children[0].id != "assistant_store" + ): + raise Exception( + f"The first child of the Assistant layout should be a Store with id assistant_store, found {self._layout.children[0]}" + ) + except: + raise Exception( + f"The first child of the Assistant layout should be a Store with id assistant_store, found {self._layout}" + ) + layouts_conflicts = self.layouts_conflicts( + self._layout, layout_to_check_against + ) + if layouts_conflicts: + raise Exception( + f"The {self.__class__.__name__} layout has ids conflict with the parent layout : {layouts_conflicts}" + ) + + @staticmethod + def layouts_conflicts(layout1, layout2): + """ + Check that two layouts do not share identical ids + Parameters + ---------- + layout1 + layout2 + + Returns + ------- + + """ + ids_layout1 = BaseAssistant.get_layout_ids(layout1) + ids_layout2 = BaseAssistant.get_layout_ids(layout2) + + return set(ids_layout1) & set(ids_layout2) + + def get_layout_ids(): + def get_layout_ids(layout): + """ + Traverse a dash layout to retrieve declared ids + Parameters + ---------- + layout + + Returns + ------- + + """ + + if hasattr(layout, "children") and isinstance(layout.children, list): + children_ids = list( + chain.from_iterable( + [get_layout_ids(child) for child in layout.children] + ) + ) + if hasattr(layout, "id"): + return [layout.id, *children_ids] + else: + return children_ids + else: + if hasattr(layout, "id"): + return [layout.id] + return [] + + return get_layout_ids + + get_layout_ids = staticmethod(get_layout_ids()) + + +class EmptyAssist(BaseAssistant): + def __init__(self): + super().__init__() + + def layout(self, *args): + return html.Div( + [ + dcc.Store(id="assistant_store"), + dcc.Store(id="assistant_actions"), + dcc.Store( + id="assistant-size", data=dict(assist="col-3", graph="col-9") + ), + html.P("No Assistant found.", className="my-2"), + ] + ) + + def register_callbacks(self, app): + pass + + def store_to_graph(self, store_data): + pass + diff --git a/grid2game/expert/ExpertAssist.py b/grid2game/expert/ExpertAssist.py new file mode 100644 index 0000000..1c3e779 --- /dev/null +++ b/grid2game/expert/ExpertAssist.py @@ -0,0 +1,354 @@ +import os +from contextlib import redirect_stdout + +import dash_antd_components as dac +import dash_bootstrap_components as dbc +from dash import dcc +from dash import html +import numpy as np +from dash import callback_context +from dash.dependencies import Input, Output, State +from dash.exceptions import PreventUpdate +from dash.dash_table import DataTable + +from alphaDeesp.core.grid2op.Grid2opSimulation import ( + Grid2opSimulation, +) +from alphaDeesp.expert_operator import expert_operator + +from grid2op.PlotGrid import PlotPlotly +from grid2op.Space import GridObjects +from grid2game.expert.BaseAssistant import BaseAssistant + + +expert_config = { + "totalnumberofsimulatedtopos": 25, + "numberofsimulatedtopospernode": 5, + "maxUnusedLines": 2, + "ratioToReconsiderFlowDirection": 0.75, + "ratioToKeepLoop": 0.25, + "ThersholdMinPowerOfLoop": 0.1, + "ThresholdReportOfLine": 0.2, +} + +reward_type = "MinMargin_reward" + + +def compute_losses(obs): + return (obs.prod_p.sum() - obs.load_p.sum()) / obs.load_p.sum() + +def get_ranked_overloads(observation_space, observation): + timestepsOverflowAllowed = ( + 3 # observation_space.parameters.NB_TIMESTEP_OVERFLOW_ALLOWED + ) + + sort_rho = -np.sort( + -observation.rho + ) # sort in descending order for positive values + sort_indices = np.argsort(-observation.rho) + ltc_list = [sort_indices[i] for i in range(len(sort_rho)) if sort_rho[i] >= 1] + + # now reprioritize ltc if critical or not + ltc_critical = [ + l + for l in ltc_list + if (observation.timestep_overflow[l] == timestepsOverflowAllowed) + ] + ltc_not_critical = [ + l + for l in ltc_list + if (observation.timestep_overflow[l] != timestepsOverflowAllowed) + ] + + ltc_list = ltc_critical + ltc_not_critical + if len(ltc_list) == 0: + ltc_list = [sort_indices[0]] + return ltc_list + + +class Assist(BaseAssistant): + def __init__(self, env): + super().__init__() + self.env = env + self.grid = GridObjects.init_grid(self.env.observation_space) + self.lines = self.get_grid_lines() + + def layout(self): + return html.Div( + children=[ + html.Div( + [ + html.Label("", id="expert_agent_button", n_clicks=0) + ], + id="hidden_buttons_for_callbacks", + style={'display': 'none'} + ), + dcc.Store(id="assistant_actions"), + dcc.Store( + id="assistant-size", data=dict(assist="col-3", graph="col-9") + ), + html.P("Choose a line to study:", className="my-2"), + dac.Select( + id="select_lines_to_cut", + options=[ + {"label": line_name, "value": line_name} + for line_name in self.lines + ], + mode="default", + value=self.lines[0], + ), + html.P("Flow ratio (in %) to get below"), + dbc.Input( + type="number", + min=0, + max=100, + step=1, + id="input_flow_ratio", + value=100, + ), + html.P("Number of simulations to run:", className="my-2"), + dbc.Input( + type="number", + min=0, + max=50, + step=1, + id="input_nb_simulations", + value=15, + ), + dbc.Button( + id="assist-evaluate", + children=["Evaluate with the Expert system"], + color="danger", + className="m-3", + ), + dbc.Button( + id="assist-reset", + children=["Reset"], + color="secondary", + className="m-3", + ), + html.Div(id="expert-results"), + html.Pre( + id="assist-action-info", + className="more-info-table", + children="Select an action in the table above.", + ), + dbc.Button( + id="add_expert_recommendation", + children=["Add to Recommendations"], + color="primary", + className="m-3", + n_clicks=0, + ), + dcc.Link( + "See the ExpertOP4Grid documentation for more information", + href="https://expertop4grid.readthedocs.io/en/latest/DESCRIPTION.html#didactic-example", + ), + ], + id="all_expert_assist" + ) + + def register_callbacks(self, app): + @app.callback( + [ + Output("expert-results", "children"), + Output("assistant_actions", "data"), + Output("assistant-size", "data"), + ], + [ + Input("assist-evaluate", "n_clicks"), + Input("assist-reset", "n_clicks")], + [ + State("input_nb_simulations", "value"), + State("input_flow_ratio", "value"), + State("select_lines_to_cut", "value"), + ], + ) + def evaluate_expert_system( + evaluate_n_clicks, + reset_n_clicks, + nb_simulations, + flow_ratio, + line_to_study, + ): + if evaluate_n_clicks is None: + raise PreventUpdate + + ctx = callback_context + if not ctx.triggered: + raise PreventUpdate + else: + button_id = ctx.triggered[0]["prop_id"].split(".")[0] + + print("evaluate_expert_system") + + if button_id == "assist-evaluate": + print("evaluate_expert_system: assist-evaluate") + assistant_size = dict(assist="col-12", graph="hidden") + else: + assistant_size = dict(assist="col-3", graph="col-9") + return "", [], assistant_size + + thermal_limit = self.env.glop_env.get_thermal_limit() + + if nb_simulations is not None: + expert_config["totalnumberofsimulatedtopos"] = nb_simulations + + if line_to_study is not None: + line_id = self.get_line_id(line_to_study) + ltc = [line_id] + else: + ltc = [get_ranked_overloads(self.env.observation_space, self.env.obs)[0]] + + if flow_ratio is not None and line_to_study is not None: + new_thermal_limit = thermal_limit.copy() + line_id = self.get_line_id(line_to_study) + new_thermal_limit[line_id] = ( + flow_ratio / 100.0 * new_thermal_limit[line_id] + ) + + self.env.glop_env.set_thermal_limit(new_thermal_limit) + + #with redirect_stdout(None): + print("evaluate_expert_system: Grid2opSimulation") + simulator = Grid2opSimulation( + self.env.obs, + self.env.action_space, + self.env.observation_space, + param_options=expert_config, + debug=False, + ltc=ltc, + reward_type=reward_type, + ) + print("evaluate_expert_system: expert_operator") + ranked_combinations, expert_system_results, actions = expert_operator( + simulator, plot=False, debug=False + ) + + # reinitialize proper thermal limits + self.env.glop_env.set_thermal_limit(thermal_limit) + + expert_system_results = expert_system_results.sort_values( + ["Topology simulated score", "Efficacity"], ascending=False + ) + actions = [actions[i] for i in expert_system_results.index] + + # Newest versions of DataTable accept only types: [string, number, boolean]. + # Convert list and numpy arrays to strings: + expert_system_results['Worsened line'] = [','.join(map(str, l)) for l in expert_system_results['Worsened line']] + expert_system_results['Topology applied'] = [','.join(map(str, l)) for l in expert_system_results['Topology applied']] + expert_system_results['Internal Topology applied '] = [','.join(map(str, l)) for l in expert_system_results['Internal Topology applied ']] + + return ( + DataTable( + id="table", + columns=[ + {"name": i, "id": i} for i in expert_system_results.columns + ], + data=expert_system_results.to_dict("records"), + style_table={"overflowX": "auto"}, + sort_action="native", + row_selectable="single", + style_cell={ + "overflow": "hidden", + "textOverflow": "ellipsis", + "maxWidth": 0, + }, + tooltip_data=[ + { + column: {"value": str(value), "type": "markdown"} + for column, value in row.items() + } + for row in expert_system_results.to_dict("rows") + ], + ), + [action.to_vect() for action in actions], + assistant_size, + ) + + @app.callback( + [ + Output("assist-action-info", "children"), + ], + [ + Input("table", "selected_rows"), + Input("assist-reset", "n_clicks") + ], + [State("assistant_actions", "data")], + ) + def select_action(selected_rows, n_clicks, actions): + ctx = callback_context + if not ctx.triggered: + raise PreventUpdate + else: + component_id = ctx.triggered[0]["prop_id"].split(".")[0] + if component_id == "assist-reset": + return [], "" + if selected_rows is None: + raise PreventUpdate + selected_row = selected_rows[0] + action = actions[selected_row] + act = self.env.action_space.from_vect(np.array(action)) + self.env.expert_selected_action = action + return [str(act)] + + def get_line_id(self, line_to_study): + for line_id, name in enumerate(self.grid.name_line): + if name == line_to_study: + return line_id + return None + + def get_grid_lines(self): + lines = [] + for line_id, name in enumerate(self.grid.name_line): + lines.append(name) + return lines + + def store_to_graph( + self, + store_data, + ): + act = self.env.action_space.from_vect(np.array(store_data)) + if self.env.obs is not None: + obs, *_ = self.env.obs.simulate(action=act, time_step=0) + # make sure rho is properly calibrated. Problem could be that obs_reboot thermal limits are not properly initialized + obs.rho = ( + obs.rho + * self.env.obs._obs_env.get_thermal_limit() + / self.env.get_thermal_limit() + ) + try: + network_graph_factory = PlotPlotly( + grid_layout=self.env.observation_space.grid_layout, + observation_space=self.env.observation_space, + responsive=True, + ) + new_network_graph = network_graph_factory.plot_obs(observation=obs) + except ValueError: + import traceback + + new_network_graph = traceback.format_exc() + + return new_network_graph + + def store_to_kpis( + self, + store_data, + ): + act = self.env.action_space.from_vect(np.array(store_data)) + if self.env.obs is not None: + obs, reward, *_ = self.env.obs.simulate(action=act, time_step=0) + # make sure rho is properly calibrated. Problem could be that obs_reboot thermal limits are not properly initialized + obs.rho = ( + obs.rho + * self.env.obs._obs_env.get_thermal_limit() + / self.env.get_thermal_limit() + ) + else: + raise RuntimeError( + f"Assist.store_to_kpis cannot be called before first rebooting the Episode" + ) + rho_max = f"{obs.rho.max() * 100:.0f}%" + nb_overflows = f"{(obs.rho > 1).sum():,.0f}" + losses = f"{compute_losses(obs)*100:.2f}%" + return reward, rho_max, nb_overflows, losses diff --git a/grid2game/expert/__init__.py b/grid2game/expert/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt index 9762936..560ea8e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,10 @@ plotly dash dash_bootstrap_components +dash-antd-components grid2op>=1.6.4 imageio orjson lightsim2grid -gunicorn \ No newline at end of file +gunicorn +pandas \ No newline at end of file diff --git a/setup.py b/setup.py index 7623601..834d08c 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ "plotly", "dash", "dash_bootstrap_components>=1.0", + "dash-antd-components", "grid2op>=1.6.4", "imageio", "orjson",