diff --git a/grid2game/VizServer.py b/grid2game/VizServer.py index b9c87fe..2d1c9fb 100644 --- a/grid2game/VizServer.py +++ b/grid2game/VizServer.py @@ -14,12 +14,12 @@ import dash_bootstrap_components as dbc from dash import html, dcc -from grid2game._utils import (add_callbacks_temporal, - setupLayout_temporal, - add_callbacks, +from grid2game._utils import (add_callbacks_temporal, + setupLayout_temporal, + add_callbacks, setupLayout, - add_callbacks_action_search, - setupLayout_action_search, + add_callbacks_action_search, + setupLayout_action_search, ) from grid2game.envs import Env from grid2game.plot import PlotGrids, PlotTemporalSeries @@ -82,6 +82,20 @@ def __init__(self, os.path.join(os.path.dirname(__file__), "assets") ) + # create the dash app + self.my_app = dash.Dash(__name__, + server=server if server is not None else True, + meta_tags=meta_tags, + assets_folder=assets_dir, + external_stylesheets=external_stylesheets, + external_scripts=external_scripts) + + # Configure logging after dash initialization. + # Otherwise dash is resetting logging level to INFO + + if not logging_level and build_args.logging_level: + logging_level = build_args.logging_level + if logger is None: import logging self.logger = logging.getLogger(__name__) @@ -94,19 +108,14 @@ def __init__(self, if logging_level is not None: fh.setLevel(logging_level) ch.setLevel(logging_level) + self.logger.setLevel(logging_level) self.logger.addHandler(fh) self.logger.addHandler(ch) else: self.logger = logger.getChild("VizServer") - # create the dash app - self.my_app = dash.Dash(__name__, - server=server if server is not None else True, - meta_tags=meta_tags, - assets_folder=assets_dir, - external_stylesheets=external_stylesheets, - external_scripts=external_scripts) self.logger.info("Dash app initialized") + # self.app.config.suppress_callback_exceptions = True # create the grid2op related things @@ -187,37 +196,37 @@ def __init__(self, self.plot_grids.init_figs(self.env.obs, self.env.sim_obs) self.real_time = self.plot_grids.figure_rt self.forecast = self.plot_grids.figure_forecat - + # initialize the layout self._layout_temporal = html.Div(setupLayout_temporal(self), id="all_temporal") self._layout_temporal_tab = dcc.Tab(label='Temporal view', value=f'tab-temporal-view', children=self._layout_temporal) - + self._layout_action_search = html.Div(setupLayout_action_search(self), id="all_action_search") self._layout_action_search_tab = dcc.Tab(label='Explore actions', value='tab-explore-action', children=self._layout_action_search) - + tmp_ = setupLayout(self, self._layout_temporal_tab, self._layout_action_search_tab) - + self.my_app.layout = tmp_ - + add_callbacks_temporal(self.my_app, self) add_callbacks_action_search(self.my_app, self) add_callbacks(self.my_app, self) - + self.logger.info("Viz server initialized") # last node id (to not plot twice the same stuff to gain time) self._last_node_id = -1 # last action taken - self._last_action = "assistant" + self._last_action = "assistant" self._do_display_action = True self._dropdown_value = "assistant" @@ -254,11 +263,11 @@ def run_server(self, debug=False): def change_nb_step_go_fast(self, nb_step_go_fast): if nb_step_go_fast is None: - return dash.no_update, + return dash.no_update, nb = int(nb_step_go_fast) self.nb_step_gofast = nb - return f"+ {self.nb_step_gofast}", + return f"+ {self.nb_step_gofast}", def unit_clicked(self, line_unit, line_side, load_unit, gen_unit, stor_unit, trigger_rt_graph, trigger_for_graph): @@ -296,22 +305,24 @@ def unit_clicked(self, line_unit, line_side, load_unit, gen_unit, stor_unit, def _reset_action_to_assistant_if_not_prev(self): if self._last_action != "prev" : self._next_action_is_assistant() - + # handle the interaction with the grid2op environment - def handle_act_on_env(self, - step_butt, - simulate_butt, - back_butt, - reset_butt, - go_butt, - gofast_clicks, - until_game_over, - untilgo_butt, - self_loop, - state_trigger_rt, - state_trigger_for, - state_trigger_self_loop, - timer): + def handle_act_on_env( + self, + step_butt, + simulate_butt, + back_butt, + reset_butt, + go_butt, + gofast_clicks, + until_game_over, + untilgo_butt, + self_loop, + state_trigger_rt, + state_trigger_for, + state_trigger_self_loop, + timer + ): """ dash do not make "synch" callbacks (two callbacks can be called at the same time), however, grid2op environments are not "thread safe": accessing them from different "thread" @@ -344,6 +355,7 @@ def handle_act_on_env(self, self._go_till_go_button_shape = "btn btn-secondary" change_graph_title = dash.no_update update_progress_bar = 1 + check_issue = dash.no_update # now register the next computation to do, based on the button triggerd if button_id == "step-button": @@ -351,11 +363,13 @@ def handle_act_on_env(self, self.env.next_computation = "step" self.env.next_computation_kwargs = {} self.need_update_figures = False + check_issue = 1 elif button_id == "go_till_game_over-button": self.env.start_computation() self.env.next_computation = "step_end" self.env.next_computation_kwargs = {} self.need_update_figures = True + check_issue = 1 elif button_id == "reset-button": self.env.start_computation() self.env.next_computation = "reset" @@ -378,6 +392,7 @@ def handle_act_on_env(self, self.env.start_computation() self.env.next_computation = "step_rec_fast" self.env.next_computation_kwargs = {"nb_step_gofast": self.nb_step_gofast} + check_issue = 1 elif button_id == "go-button": self.go_clicks += 1 if self.go_clicks % 2: @@ -390,6 +405,7 @@ def handle_act_on_env(self, self.env.start_computation() self._button_shape = "btn btn-secondary" self._gofast_button_shape = "btn btn-secondary" + check_issue = 1 self.env.next_computation = "step_rec" self.env.next_computation_kwargs = {} self.need_update_figures = False @@ -434,7 +450,7 @@ def handle_act_on_env(self, self._go_till_go_button_shape = "btn btn-secondary" self._gofast_button_shape = "btn btn-secondary" self._button_shape = "btn btn-secondary" - + return [display_new_state, self._button_shape, self._button_shape, @@ -446,7 +462,8 @@ def handle_act_on_env(self, i_am_computing_state, i_am_computing_state, change_graph_title, - update_progress_bar] + update_progress_bar, + check_issue] def _wait_for_computing_over(self): i = 0 @@ -458,6 +475,29 @@ def _wait_for_computing_over(self): # in this case the user should probably call reset another time ! raise dash.exceptions.PreventUpdate + def check_issue(self, n1, n_clicks): + # make sure the environment has nothing to compute + while self.env.needs_compute(): + time.sleep(0.1) + + issues = self.env._current_issues + if issues: + len_issues = len(issues) + if len_issues == 1: + issue_text = f"There is {len_issues} issue: " + else: + issue_text = f"There are {len_issues} issues: " + for issue in issues: + issue_text += f"{issue}, " + # Replace last ', ' by '.' to end the sentence + issue_text = '.'.join(issue_text.rsplit(', ', 1)) + is_open = True + else: + issue_text = dash.no_update + is_open = False + + return [is_open, issue_text] + def change_graph_title(self, change_graph_title): # make sure that the environment has done computing self._wait_for_computing_over() @@ -483,7 +523,7 @@ def computation_wrapper(self, display_new_state, recompute_rt_from_timeline): # simulate a "state" of the application that depends on the computation if not self.env.is_computing(): self.env.heavy_compute() - + if self.env.is_computing() and display_new_state != type(self).GO_MODE: # environment is computing I do not update anything raise dash.exceptions.PreventUpdate @@ -513,7 +553,7 @@ def update_rt_fig(self, env_act): trigger_for_graph = 1 else: raise dash.exceptions.PreventUpdate - + if trigger_rt_graph == 1: self.fig_timeline = self.env.get_timeline_figure() @@ -661,7 +701,7 @@ def _next_action_is_assistant(self): self._last_action = "assistant" self._do_display_action = True self._dropdown_value = "assistant" - + def display_action_fun(self, which_action_button, do_display, @@ -712,7 +752,7 @@ def display_action_fun(self, # i should not display the action res = [f"{self.env.current_action}", dropdown_value, update_substation_layout_clicked_from_sub] return res - + # i need to display the action # self._last_action = "manual" # dropdown_value = "manual" @@ -748,7 +788,7 @@ def display_action_fun(self, if not is_modif: raise dash.exceptions.PreventUpdate - + # TODO optim here to save that if not needed because nothing has changed res = [f"{self.env.current_action}", self._dropdown_value, update_substation_layout_clicked_from_sub] return res @@ -821,7 +861,7 @@ def display_click_data(self, button_id == "go-button" or button_id == "gofast-button" or\ button_id == "back-button": # i never clicked on simulate, step, go, gofast or back - do_display_action = 0 + do_display_action = 0 self._last_sub_id = None else: # I clicked on the graph of the grid @@ -980,7 +1020,7 @@ def timeline_set_time(self, time_line_graph_clicked): def tab_content_display(self, tab): res = [self._layout_temporal] - + if tab == 'tab-temporal-view': self.need_update_figures = True return [self._layout_temporal] @@ -991,13 +1031,13 @@ def tab_content_display(self, tab): msg_ = f"Unknown tab {tab}" self.logger.error(msg_) return res - + def _aux_tab_as_retrieve_updated_figs(self): progress_pct = 100. * self._last_step / self._last_max_step progress_label = f"{self._last_step} / {self._last_max_step}" self.fig_timeline = self.env.get_timeline_figure() self.update_obs_fig() - + pbar_value = progress_pct pbar_label = progress_label pbar_color = self._progress_color @@ -1006,7 +1046,7 @@ def _aux_tab_as_retrieve_updated_figs(self): fig_rt = self.real_time return (pbar_value, pbar_label, pbar_color, fig_timeline, dt_label, fig_rt) - + def main_action_search(self, refresh_button, explore_butt_pressed, @@ -1017,9 +1057,9 @@ def main_action_search(self, raise dash.exceptions.PreventUpdate else: button_id = ctx.triggered[0]['prop_id'].split('.')[0] - + something_clicked = True - + # TODO button color here too ! i_am_computing_state = {'display': 'block'} pbar_value = dash.no_update @@ -1029,25 +1069,25 @@ def main_action_search(self, dt_label = dash.no_update fig_rt = dash.no_update start_computation = 1 - + if button_id == "refresh-button_as": # (pbar_value, pbar_label, pbar_color, fig_timeline, # dt_label, fig_rt) = self._aux_tab_as_retrieve_updated_figs() start_computation = dash.no_update # hack for it to resynch everything self.need_update_figures = True - elif button_id == "explore-button_as": + elif button_id == "explore-button_as": self.env.next_computation = "explore" self.need_update_figures = True self.env.start_computation() else: something_clicked = False - + if not self.env.needs_compute(): # don't start the computation if not needed i_am_computing_state = {'display': 'none'} # deactivate the "i am computing button" start_computation = dash.no_update # I am NOT computing I DO update the graphs - + if not self.env.needs_compute() and self.need_update_figures and not something_clicked: # in this case, this should be the last call to this function after the "explore" # function is finished @@ -1058,7 +1098,7 @@ def main_action_search(self, (pbar_value, pbar_label, pbar_color, fig_timeline, dt_label, fig_rt) = self._aux_tab_as_retrieve_updated_figs() - + return [start_computation, pbar_value, pbar_label, diff --git a/grid2game/_utils/_temporal_callbacks.py b/grid2game/_utils/_temporal_callbacks.py index 5203481..a3f3fdc 100644 --- a/grid2game/_utils/_temporal_callbacks.py +++ b/grid2game/_utils/_temporal_callbacks.py @@ -112,7 +112,8 @@ def add_callbacks(dash_app, viz_server): dash.dependencies.Output("is_computing_left", "style"), dash.dependencies.Output("is_computing_right", "style"), dash.dependencies.Output("change_graph_title", "n_clicks"), - dash.dependencies.Output("update_progress_bar_from_act", "n_clicks") + dash.dependencies.Output("update_progress_bar_from_act", "n_clicks"), + dash.dependencies.Output("check_issue", "n_clicks"), ], [dash.dependencies.Input("step-button", "n_clicks"), dash.dependencies.Input("simulate-button", "n_clicks"), @@ -123,11 +124,11 @@ def add_callbacks(dash_app, viz_server): dash.dependencies.Input("go_till_game_over-button", "n_clicks"), dash.dependencies.Input("untilgo_butt_call_act_on_env", "value"), dash.dependencies.Input("selfloop_call_act_on_env", "value"), - dash.dependencies.Input("timer", "n_intervals") + dash.dependencies.Input("timer", "n_intervals"), ], [dash.dependencies.State("act_on_env_trigger_rt", "n_clicks"), dash.dependencies.State("act_on_env_trigger_for", "n_clicks"), - dash.dependencies.State("act_on_env_call_selfloop", "value") + dash.dependencies.State("act_on_env_call_selfloop", "value"), ] )(viz_server.handle_act_on_env) @@ -252,3 +253,13 @@ def add_callbacks(dash_app, viz_server): # set the seed dash_app.callback([dash.dependencies.Output("set_seed_dummy_output", "n_clicks")], [dash.dependencies.Input("set_seed", "value")])(viz_server.set_seed) + + # trigger the modal issue + dash_app.callback( + [ + dash.dependencies.Output("modal_issue", "is_open"), + dash.dependencies.Output("modal_issue_text", "children") + ], + [dash.dependencies.Input("check_issue", "n_clicks")], + [dash.dependencies.State("modal_issue", "is_open")] + )(viz_server.check_issue) \ No newline at end of file diff --git a/grid2game/_utils/_temporal_layout.py b/grid2game/_utils/_temporal_layout.py index f59aa92..524d273 100644 --- a/grid2game/_utils/_temporal_layout.py +++ b/grid2game/_utils/_temporal_layout.py @@ -29,7 +29,7 @@ def setupLayout(viz_server): html.Div([# html.P("Chronics: ", style={"marginRight": 5, "marginLeft": 5}), html.Div([dcc.Dropdown(id="chronic_names", placeholder="Select a chronic", - options=[{"value": el, "label": el} + options=[{"value": el, "label": el} for el in viz_server.env.list_chronics()]) ], id="chronics_dropdown", @@ -41,7 +41,7 @@ def setupLayout(viz_server): dcc.Input(id="set_seed", type="number", placeholder="Select a seed", - ), + ), ], id="seed_selector") ], @@ -71,7 +71,7 @@ def setupLayout(viz_server): id="nb_step_go_fast", type="number", placeholder="steps", - ) + ) go_fast = html.Label(children =f"+ {viz_server.nb_step_gofast}", id="gofast-button", n_clicks=0, @@ -266,7 +266,7 @@ def setupLayout(viz_server): ) save_experiment = html.Div(id='save_expe_box', - children=[ + children=[ html.Div(children=[ dcc.Input(placeholder=save_txt, id="save_expe", @@ -538,7 +538,7 @@ def setupLayout(viz_server): children=[current_action], style={'width': '39%'} ) - + # combine both interaction_and_action = html.Div([action_widget_title, html.Div([layout_click, @@ -682,6 +682,7 @@ def setupLayout(viz_server): ) # triggering the update of the figures + check_issue = html.Label("", id="check_issue", n_clicks=0) act_on_env_trigger_rt = html.Label("", id="act_on_env_trigger_rt", n_clicks=0) act_on_env_trigger_for = html.Label("", id="act_on_env_trigger_for", n_clicks=0) clear_assistant_path = html.Label("", id="clear_assistant_path", n_clicks=0) @@ -711,7 +712,8 @@ def setupLayout(viz_server): chronic_names_dummy_output, set_seed_dummy_output, update_substation_layout_clicked_from_sub, update_substation_layout_clicked_from_grid, trigger_rt_extra_info, trigger_for_extra_info, - update_progress_bar_from_act, update_progress_bar_from_figs + update_progress_bar_from_act, update_progress_bar_from_figs, + check_issue ], id="hidden_buttons_for_callbacks", style={'display': 'none'}) @@ -721,6 +723,22 @@ def setupLayout(viz_server): interval=500. # in ms ) + modal_issue = dbc.Modal( + [ + dbc.ModalHeader( + dbc.ModalTitle("There is an issue"), + close_button=True + ), + dbc.ModalBody(dbc.Label("", id='modal_issue_text')), + dbc.ModalFooter( + dbc.Button("Show more", id="show_more_issue", className="ml-auto") + ), + ], + id="modal_issue", + size="lg", + is_open=False, + ) + # Final page layout_css = "container-fluid h-100 d-md-flex d-xl-flex flex-md-column flex-xl-column" layout = html.Div(id="grid2game", @@ -740,7 +758,8 @@ def setupLayout(viz_server): temporal_graphs, interval_object, hidden_interactions, - timer_callbacks + timer_callbacks, + modal_issue ]) return layout diff --git a/grid2game/app.py b/grid2game/app.py index 7cb44d7..50c0c99 100755 --- a/grid2game/app.py +++ b/grid2game/app.py @@ -54,6 +54,12 @@ def cli(): action="store_true", default=False, help="INTERNAL: do not use (inform that the app is running on the heroku server)") + # DEBUG + parser.add_argument( + "--logging_level", required=False, default=20, type=int, + help="Python Logging Levels: CRITICAL=50, ERROR=40, WARNING=30, INFO=20 (default), DEBUG=10" + ) + return parser.parse_args() diff --git a/grid2game/envs/env.py b/grid2game/envs/env.py index bee34a0..f4a9474 100644 --- a/grid2game/envs/env.py +++ b/grid2game/envs/env.py @@ -23,7 +23,6 @@ # TODO: logger here bkClass = PandaPowerBackend - from grid2game.agents import load_assistant from grid2game.envs.computeWrapper import ComputeWrapper from grid2game.tree import EnvTree @@ -75,6 +74,7 @@ def __init__(self, self._sim_done = None self._sim_info = None self._current_assistant_action = None + self._current_issues = None # define variables self._should_display = True @@ -93,7 +93,7 @@ def __init__(self, # to control which action will be done when self.next_computation = None self.next_computation_kwargs = {} - + # actions to explore self.all_topo_actions = None @@ -167,12 +167,15 @@ def do_computation(self): return self.seed(**self.next_computation_kwargs) elif self.next_computation == "step": res = self.step(**self.next_computation_kwargs) + obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() + if self._stop_if_issue(obs): + self.logger.info("step_end: An alarm is raised, I stop") self.stop_computation() # this is a "one time" call return res elif self.next_computation == "step_rec": # this is the "go" button res = self.step() obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() - if self._stop_if_alarm(obs): + if self._stop_if_issue(obs): # I stop the computation if the agent sends an alarm self.logger.info("step_rec: An alarm is raised, I stop") self.stop_computation() @@ -184,7 +187,7 @@ def do_computation(self): res = self.step() obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() # print(f"do_computation: {self._assistant_action.raise_alarm}") - if self._stop_if_alarm(obs): + if self._stop_if_issue(obs): self.logger.info("step_rec_fast: An alarm is raised, I stop") break self.stop_computation() # this is a "one time" call @@ -196,7 +199,7 @@ def do_computation(self): while not done: res = self.step() obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() - if self._stop_if_alarm(obs): + if self._stop_if_issue(obs): self.logger.info("step_end: An alarm is raised, I stop") break self.stop_computation() # this is a "one time" call @@ -238,7 +241,7 @@ def explore(self): if self.all_topo_actions is None: self.all_topo_actions = self.glop_env.action_space.get_all_unitary_line_change(self.glop_env.action_space) self.all_topo_actions += self.glop_env.action_space.get_all_unitary_topologies_set(self.glop_env.action_space) - + obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() res = [] for act in self.all_topo_actions: @@ -246,7 +249,7 @@ def explore(self): sim_reward = sim_obs.rho.max() if not sim_done else 1000. res.append((act, sim_reward)) res.sort(key=lambda x: x[1]) - + init_node = self.env_tree.current_node till_the_end = False for act, rew in res[:5]: @@ -254,17 +257,55 @@ def explore(self): if not till_the_end: till_the_end = self._donothing_until_end() self.env_tree.go_to_node(init_node) - + def _donothing_until_end(self): obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() while not done: obs, reward, done, info = self.step(self.glop_env.action_space()) return obs.current_step == obs.max_step - + + def _get_current_action(self): + res = None + current_id = self.env_tree.current_node.id + for el in self.env_tree.current_node.father._act_to_sons: + if el.son.id == current_id: + res = el.action + return res + def _stop_if_alarm(self, obs): - if self.do_stop_if_alarm: - if np.any(obs.time_since_last_alarm == 0): - return True + if np.any(obs.time_since_last_alarm == 0): + self.logger.info("Assistant raised an alarm") + return True + return False + + def _stop_if_action(self, act): + if act.can_affect_something(): + self.logger.info("The current action has a chance to change the grid") + return True + return False + + def _stop_if_bad_kpi(self, obs): + # Check if overload + if obs.rho.max() >= 1.0: + self.logger.info("Overload") + return True + return False + + def _stop_if_issue(self, obs): + issues = [] + self._current_issues = None + act = self._get_current_action() + self.logger.debug(act) + self.logger.debug(type(act)) + if self._stop_if_alarm(obs): + issues.append("Assistant raised an alarm") + if self._stop_if_action(act): + issues.append("The current action has a chance to change the grid") + if self._stop_if_bad_kpi(obs): + issues.append("Overload") + if len(issues) > 0: + self._current_issues = issues + return True return False @property @@ -369,7 +410,7 @@ def step(self, action=None): self._sim_reward = self.glop_env.reward_range[0] self._sim_info = {} self._sim_obs.set_game_over(self.glop_env) - # print(f"step: {np.any(self._assistant_action.raise_alarm)}") + # print(f"step: {np.any(self._assistant_action.raise_alarm)}") return obs, reward, done, info def choose_next_assistant_action(self): @@ -399,7 +440,7 @@ def back(self): def reset(self, chronics_id=None, seed=None): if chronics_id is not None: - + chron = self.glop_env.chronics_handler if hasattr(chron, "available_chronics"): # "proxy" for "grid2op >= 1.6.5" self.glop_env.set_id(chronics_id) @@ -412,7 +453,7 @@ def reset(self, chronics_id=None, seed=None): def init_state(self): self.env_tree.clear() - obs = self.glop_env.reset() + obs = self.glop_env.reset() self.env_tree.root(assistant=self.assistant, obs=obs, env=self.glop_env) self._current_action = self.glop_env.action_space() @@ -470,7 +511,7 @@ def handle_click_timeline(self, time_line_graph_clcked) -> int: self.is_computing() res = self.env_tree.move_from_click(time_line_graph_clcked) self._current_action = copy.deepcopy(self.env_tree.get_last_action()) - + obs, reward, done, info = self.env_tree.current_node.get_obs_rewar_done_info() if not done: self.choose_next_assistant_action()