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 b9c87fe..f5eacb3 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -9,21 +9,40 @@ import os import sys import time +import pandas as pd +import copy +import warnings +import numpy as np + 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 -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 +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 @@ -82,6 +101,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 +127,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,40 +215,62 @@ 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 - + + # 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") 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) - + + # 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) 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" + self._open_tab = 0 + + def _make_glop_env_config(self, build_args): g2op_config = {} cont_ = True @@ -254,11 +304,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 +346,27 @@ 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, + until_game_over_auto, + untilgo_butt, + self_loop, + timer, + state_trigger_rt, + state_trigger_for, + state_trigger_self_loop, + recommendations_container_is_open, + selected_recommendation, + ): """ 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" @@ -329,7 +384,7 @@ def handle_act_on_env(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] @@ -344,6 +399,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 +407,15 @@ def handle_act_on_env(self, self.env.next_computation = "step" self.env.next_computation_kwargs = {} self.need_update_figures = False - elif button_id == "go_till_game_over-button": + self._check_issue += 1 + check_issue = self._check_issue + 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 = {} self.need_update_figures = True + self._check_issue += 1 + check_issue = self._check_issue elif button_id == "reset-button": self.env.start_computation() self.env.next_computation = "reset" @@ -368,6 +428,13 @@ def handle_act_on_env(self, 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" @@ -378,6 +445,8 @@ 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} + self._check_issue += 1 + check_issue = self._check_issue elif button_id == "go-button": self.go_clicks += 1 if self.go_clicks % 2: @@ -390,6 +459,8 @@ def handle_act_on_env(self, self.env.start_computation() self._button_shape = "btn btn-secondary" self._gofast_button_shape = "btn btn-secondary" + 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 @@ -434,7 +505,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, @@ -443,10 +514,12 @@ def handle_act_on_env(self, 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, - update_progress_bar] + update_progress_bar, + check_issue] def _wait_for_computing_over(self): i = 0 @@ -456,7 +529,41 @@ 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, + n_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 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 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: " + 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)) + return [True, issue_text] + else: + raise PreventUpdate + def change_graph_title(self, change_graph_title): # make sure that the environment has done computing @@ -479,28 +586,39 @@ 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 + def computation_wrapper( + self, + display_new_state, + recompute_rt_from_timeline, + variant_tree_added, + ): + button_id = ctx.triggered_id - if display_new_state == 1 or display_new_state == type(self).GO_MODE: + if button_id == "variant_tree_added": 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 @@ -512,8 +630,8 @@ 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() @@ -527,7 +645,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" @@ -573,13 +691,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 @@ -595,7 +713,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] @@ -603,7 +721,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 @@ -620,7 +738,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 @@ -661,7 +779,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, @@ -670,19 +788,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] @@ -693,26 +820,54 @@ 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 # self._last_action = "manual" # dropdown_value = "manual" @@ -747,34 +902,42 @@ 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] + 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): """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]] - 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 = "" @@ -821,7 +984,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 @@ -852,7 +1015,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, @@ -874,7 +1037,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) @@ -893,7 +1056,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): @@ -908,7 +1071,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 @@ -968,11 +1131,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 @@ -980,24 +1143,29 @@ 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_) 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 +1174,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, @@ -1014,12 +1182,12 @@ 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] - + something_clicked = True - + # TODO button color here too ! i_am_computing_state = {'display': 'block'} pbar_value = dash.no_update @@ -1029,25 +1197,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 +1226,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, @@ -1069,3 +1237,409 @@ def main_action_search(self, 1, 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) + + # TODO: handle several human recommendations + self.variant_env_trees.append( + { + "agent_name": agent_name, + "agent_action": agent_action, + "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", + sort_action="native", + 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_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 + + + def handle_recommendations( + self, + # buttons + n_show_more, + n_close, + 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 + recommendations_store, + selected_recommendation, + 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 + + recommendations_div = dash.no_update + recommendations_container_open = dash.no_update + recommendations_message = None + variant_tree_added = dash.no_update + + if button_id == "close_recommendations_button": + + # 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 + recommendations_container_open = False + + elif button_id == "show_more_issue": + recommendations_added_to_variant_trees = dash.no_update + + self.variant_env_trees = [] + + agent_name = self.format_path(os.path.abspath(self.assistant_path)) + agent_action = self.env._assistant_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() + + 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 + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + 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_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 + 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 + ] + + # 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 [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + # 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, + # 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 !" + + # 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 + return [ + recommendations_div, recommendations_container_open, + recommendations_store, recommendations_message, + recommendations_added_to_variant_trees, variant_tree_added + ] + + selected_agent_name = self.get_selected_agent_name(selected_recommendation) + + # 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 [ + 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 + + elif button_id == "integrate_manual_action": + recommendations_added_to_variant_trees = dash.no_update + + # Add only if the recommendations are open + if not is_open: + self.logger.error("Recommendations 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() + + 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 + + return [ + recommendations_div, + recommendations_container_open, + recommendations_store, + recommendations_message, + recommendations_added_to_variant_trees, + variant_tree_added, + ] + + def loading_recommendations_table( + self, + n_show_more, + n_integrate_manual_action, + ): + button_id = ctx.triggered_id + + if not button_id and not self.env.expert_selected_action: + 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, + ): + + button_id = ctx.triggered_id + self.logger.debug(f"dropdown_mode: triggered_id = {button_id} mode = {mode}") + + self.env.mode = mode + + if mode in [self.env.MODE_MANUAL, self.env.MODE_LEGACY]: + 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] + + 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 5203481..09e8020 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -7,248 +7,359 @@ # 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.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"), - - 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"), - - 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"), + 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"), + + 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"), + + Output("line_clicked", "style"), + Output("line-id-hidden", "children"), + Output("line-id-clicked", "children"), + Output("line-status-input", "value"), + Output("line_flow", "children"), + + 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"), + Input("go_till_game_over_auto-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"), + Output("action_buttons", "style"), ], - [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") - ])(viz_server.display_action_fun) + [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"), + 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([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") + 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("go_till_game_over_auto-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("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"), ], - [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"), + State("recommendations_container", "is_open"), + State("selected_recommendation_store", "data"), ] )(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"), + Input("variant_tree_added", "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.dependencies.Input("load_assistant_button", "n_clicks") - ], - [dash.dependencies.State("select_assistant", "value")] - )(viz_server.load_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")], + prevent_initial_call=True, + )(viz_server.load_assistant) - dash_app.callback([dash.dependencies.Output("select_assistant", "value")], - [dash.dependencies.Input("clear_assistant_path", "n_clicks")] - )(viz_server.clear_loading) + 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([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( + [ + Output("modal_issue", "is_open"), + Output("modal_issue_text", "children"), + ], + [ + Input("check_issue", "n_clicks"), + # Close modal when clicking on Show more + Input("show_more_issue", "n_clicks"), + ], + [ + State("modal_issue", "is_open"), + ], + prevent_initial_call=True, + )(viz_server.check_issue) + + # show the recommendations table + dash_app.callback( + [ + 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_recommendations_button", "n_clicks"), + 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( + [ + Output("loading_recommendations_output", "children"), + ], + [ + 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("selected_recommendation_store", "data"), + ], + [ + Input("recommendations_table", "selected_rows"), + ], + [ + State("recommendations_store", "data"), + ], + prevent_initial_call=True, + )(viz_server.select_recommendation) + + # 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) + + 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 f59aa92..b4445b2 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -24,31 +24,68 @@ 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() + ], + persistence=True, + ) + ], + 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", + persistence=True, + ), + ], + 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() + ], + value=viz_server.env.mode, + # Persist user choice to tab switch or page reload + persistence=True, + ) + ], + 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", @@ -71,17 +108,23 @@ 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, 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", ) + # 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"))] @@ -92,25 +135,6 @@ def setupLayout(viz_server): id="is_computing_right", style={'display': 'none'}) - controls_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"} - ) - # Units displayed control # TODO add a button "trust assistant up to" that will play the actions suggested by the # TODO assistant @@ -121,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", @@ -200,18 +239,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" @@ -221,113 +265,177 @@ 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_row = html.Div(id="controls-row", + controls_manual_row = html.Div(id="control-manual-buttons", + # className="row", children=[ - reset_col, - controls_row, - select_assistant, - save_experiment, - change_units - ]) + 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_auto, + ], + style={'justifyContent': 'space-between', + "display": "flex"} + ) + + controls_row = html.Div( + id="controls-row", + children=[ + reset_col, + 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, + ] + ) # progress in the scenario (progress bar and timeline) progress_bar_for_scenario = html.Div(children=[html.Div(dbc.Progress(id="scenario_progression", @@ -428,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" @@ -522,9 +633,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", @@ -533,22 +643,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-button", + 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", @@ -682,6 +832,10 @@ 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) clear_assistant_path = html.Label("", id="clear_assistant_path", n_clicks=0) @@ -711,7 +865,11 @@ 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, + show_more_issue, + variant_tree_added, + add_expert_recommendation, ], id="hidden_buttons_for_callbacks", style={'display': 'none'}) @@ -721,6 +879,137 @@ 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", n_clicks=0) + ), + ], + id="modal_issue", + size="lg", + is_open=False, + ) + + recommendations_container = dbc.Collapse( + html.Div( + children=[ + dbc.Button( + "Close", + id="close_recommendations_button", + className="ml-auto", + n_clicks=0, + style={ + "display": "flex" + } + ), + dbc.Container([ + dbc.Label( + 'Recommendations', + style={ + "padding": "5px" + } + ), + html.Div( + children=[ + html.Div( + # recommendations DataTable is loaded here from the callback + children = [], + id="recommendations_div", + style={ + "padding": "10px" + } + ), + html.Div( + children=[ + dbc.Button( + "Simulate", + id="simulate-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_recommendation_button", + className="ml-auto", + n_clicks=0 + ), + ], + style={ + "alignItems":"left", + "display": "flex", + "padding": "2px", + "justifyContent": "space-between", + "width": "50%" + } + ) + ], + style={ + 'borderWidth': '1px', + 'borderStyle': 'solid', + 'borderRadius': '5px', + 'width': '100%', + "padding": "10px" + } + ), + dbc.Label( + "", + id="recommendations_message", + ), + html.Div( + children=[ + dbc.Button( + "Expert Assist", + id="expert_agent_button", + n_clicks=0 + ) + ], + style={ + "alignItems": "end", + "display": "flex", + "padding": "4px" + } + ) + ]) + ], + style={ + 'borderWidth': '1px', + 'borderStyle': 'solid', + 'borderRadius': '5px', + 'width': '80%', + "alignItems":"center", + "marginLeft": "auto", + "marginRight": "auto" + } + ), + id="recommendations_container", + is_open=False, + style={ + "paddingBottom": "20px" + }, + ) + + loading_recommendations = dcc.Loading( + id="loading_recommendations", + type="default", + children=html.Div(id="loading_recommendations_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", @@ -732,6 +1021,11 @@ def setupLayout(viz_server): html.Br(), progress_bar_for_scenario, html.Br(), + loading_recommendations, + recommendations_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(), @@ -740,7 +1034,8 @@ def setupLayout(viz_server): temporal_graphs, interval_object, hidden_interactions, - timer_callbacks + timer_callbacks, + modal_issue, ]) 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/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/computeWrapper.py b/grid2game/envs/computeWrapper.py index 119df0b..9cd517b 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_recommendations = 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_recommendations(self): + return self.__is_computing_recommendations + + def start_recommendations_computation(self): + self.__is_computing_recommendations = True + + 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 bee34a0..3929996 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 @@ -39,6 +38,11 @@ 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 + MODE_LEGACY = 3 + def __init__(self, env_name, assistant_path=None, @@ -75,6 +79,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 @@ -88,15 +93,21 @@ 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 self.next_computation = None self.next_computation_kwargs = {} - + # actions to explore self.all_topo_actions = None + # Default mode + self.mode = self.MODE_LEGACY + def is_assistant_illegal(self): if "is_illegal" in self._sim_info: return self._sim_info["is_illegal"] @@ -132,6 +143,14 @@ 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"), + (self.MODE_LEGACY, "Legacy") + ] + def load_assistant(self, assistant_path): self.logger.info(f"attempt to load assistant with path : \"{assistant_path}\"") has_been_loaded = False @@ -167,12 +186,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 +206,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 +218,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 +260,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,25 +268,70 @@ 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]: 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 + 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 + + 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 in [self.MODE_ASSISTANT, self.MODE_LEGACY]: + if self._stop_if_alarm(obs): + if self.mode == self.MODE_ASSISTANT: + issues.append("Assistant raised an alarm") + + if len(issues) > 0: + self._current_issues = issues + return True return False @property @@ -349,6 +416,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() @@ -358,7 +427,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: @@ -369,7 +438,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 +468,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 +481,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,11 +539,11 @@ 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() - 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: @@ -486,3 +555,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 recommendations + 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/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 3413403..834d08c 100644 --- a/setup.py +++ b/setup.py @@ -14,9 +14,11 @@ "plotly", "dash", "dash_bootstrap_components>=1.0", + "dash-antd-components", "grid2op>=1.6.4", "imageio", "orjson", + "pandas", # "igraph" # "graphviz", # "networkx"