diff --git a/app/components/context.py b/app/components/context.py index ccab63c..333ef50 100644 --- a/app/components/context.py +++ b/app/components/context.py @@ -153,7 +153,7 @@ def create_context_div(self): # TODO: Make the box big enough to fit the text html.Div( id="ssp-desc", - children=[self.construct_ssp_desc(0)], + children=[html.H4("Select a Scenario")], className="flex-grow-1 overflow-auto border rounded-3 p-2", style={"height": "275px"} ) @@ -164,7 +164,7 @@ def create_context_div(self): dbc.Button( "AI Generate Policies for Scenario", id="presc-button", - className="me-1", + className="me-1 mb-2", n_clicks=0 ) ] @@ -194,19 +194,15 @@ def register_callbacks(self, app): """ @app.callback( [Output(f"context-slider-{i}", "value") for i in range(4)], - Input("context-scatter", "clickData") + Input("context-scatter", "clickData"), + prevent_initial_call=True ) def click_context(click_data): """ Updates context sliders when a context point is clicked. - TODO: Sometimes this function lags, not sure why. """ - if click_data: - # TODO: This assumes the SSPs in the ssps.csv file are in order which they are - scenario = int(click_data["points"][0]["pointNumber"]) - else: - scenario = 0 - + # TODO: This assumes the SSPs in the ssps.csv file are in order which they are + scenario = int(click_data["points"][0]["pointNumber"]) scenario = f"SSP{scenario+1}-Baseline" row = self.context_df[self.context_df["scenario"] == scenario].iloc[0] return [row[self.context_cols[i]] for i in range(4)] diff --git a/app/components/filter.py b/app/components/filter.py index bf2da96..34da9a4 100644 --- a/app/components/filter.py +++ b/app/components/filter.py @@ -1,6 +1,8 @@ """ Component in charge of filtering out prescriptors by metric. """ +import json + from dash import html, dcc, Input, Output, State import dash_bootstrap_components as dbc import pandas as pd @@ -22,9 +24,14 @@ def __init__(self, metrics: list[str]): self.metric_ids = [metric.replace(" ", "-").replace(".", "_") for metric in self.metrics] self.updated_params = ["min", "max", "value", "marks"] + with open("app/units.json", "r", encoding="utf-8") as f: + self.units = json.load(f) + def create_metric_sliders(self): """ Creates initial metric sliders and lines them up with their labels. + TODO: We need to stop hard-coding their names and adjustments. + TODO: Add a tooltip to the sliders to show their units. """ sliders = [] for metric in self.metrics: @@ -36,16 +43,22 @@ def create_metric_sliders(self): value=[0, 1], marks={0: f"{0:.2f}", 1: f"{1:.2f}"}, tooltip={"placement": "bottom", "always_visible": True}, - allowCross=False + allowCross=False, + disabled=True ) sliders.append(slider) + names_map = dict(zip(self.metrics, ["Temperature change from 1850", + "Highest cost of energy", + "Government spending", + "Reduction in energy demand"])) + # w-25 and flex-grow-1 ensures they line up div = html.Div( children=[ html.Div( className="d-flex flex-row mb-2", children=[ - html.Label(self.metrics[i], className="w-25"), # w-25 and flex-grow-1 ensures they line up + html.Label(f"{names_map[self.metrics[i]]} ({self.units[self.metrics[i]]})", className="w-25"), html.Div(sliders[i], className="flex-grow-1") ] ) @@ -161,7 +174,7 @@ def create_filter_div(self): className="me-1", style={"width": "200px"} # TODO: We hard-code the width here because of text size ), - dbc.Button("Reset Filters", id="reset-button") + dbc.Button("Reset Filters", id="reset-button", disabled=True) ] ), html.Div( @@ -183,14 +196,18 @@ def register_callbacks(self, app): """ @app.callback( [Output(f"{metric_id}-slider", param) for metric_id in self.metric_ids for param in self.updated_params], + [Output(f"{metric_id}-slider", "disabled") for metric_id in self.metric_ids], + Output("reset-button", "disabled"), Input("metrics-store", "data"), - Input("reset-button", "n_clicks") + Input("reset-button", "n_clicks"), + prevent_initial_call=True ) def update_filter_sliders(metrics_jsonl: list[dict[str, list]], _) -> list: """ Update the filter slider min/max/value/marks based on the incoming metrics data. The output of this function is a list of the updated parameters for each slider concatenated. This also happens whenever we click the reset button. + The reset button starts disabled but once the sliders are updated for the first time it becomes enabled. """ metrics_df = pd.DataFrame(metrics_jsonl) total_output = [] @@ -208,13 +225,15 @@ def update_filter_sliders(metrics_jsonl: list[dict[str, list]], _) -> list: {min_val_rounded: f"{min_val_rounded:.2f}", max_val_rounded: f"{max_val_rounded:.2f}"} ] total_output.extend(metric_output) - + total_output.extend([False] * len(self.metric_ids)) # Enable all sliders + total_output.append(False) # Enable reset button return total_output @app.callback( Output("parcoords-figure", "figure"), State("metrics-store", "data"), - [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids] + [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids], + prevent_initial_call=True ) def filter_parcoords_figure(metrics_json: dict[str, list], *metric_ranges) -> go.Figure: """ @@ -225,7 +244,8 @@ def filter_parcoords_figure(metrics_json: dict[str, list], *metric_ranges) -> go @app.callback( Output("cand-counter", "children"), State("metrics-store", "data"), - [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids] + [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids], + prevent_initial_call=True ) def count_selected_cands(metrics_json: dict[str, list], *metric_ranges) -> str: """ diff --git a/app/components/intro.py b/app/components/intro.py index 0e7a51a..e989081 100644 --- a/app/components/intro.py +++ b/app/components/intro.py @@ -19,13 +19,34 @@ def create_intro_div(self): html.H2("Decision Making for Climate Change", className="display-4 w-50 mx-auto text-center mb-3") ), dbc.Row( - html.P("Immediate action is required to combat climate change. \ - The technology behind Cognizant NeuroAI brings automatic decision-making to the En-ROADS \ - platform, a powerful climate change simulator. A decision-maker can be ready for any \ - scenario: choosing an automatically generated policy that suits their needs best, with the \ - ability to manually modify the policy and see its results. This tool is brought together \ - under Project Resilience, a United Nations initiative to use AI for good.", - className="lead w-50 mx-auto text-center") + html.P( + [ + "Immediate action is required to combat climate change. The technology behind ", + html.A( + "Cognizant NeuroAI", + href="https://www.cognizant.com/us/en/services/ai/ai-lab", + style={"color": "black"} + ), + " brings automatic decision-making to the ", + html.A( + "En-ROADS platform", + href="https://www.climateinteractive.org/en-roads/", + style={"color": "black"} + ), + ", a powerful climate change simulator. A decision-maker can be ready for any \ + scenario: choosing an automatically generated policy that suits their needs best, with the \ + ability to manually modify the policy and see its results. This tool is brought together \ + under ", + html.A( + "Project Resilience", + href="https://www.itu.int/en/ITU-T/extcoop/ai-data-commons/\ + Pages/project-resilience.aspx", + style={"color": "black"} + ), + ", a United Nations initiative to use AI for good." + ], + className="lead w-50 mx-auto text-center" + ) ), dbc.Row( style={"height": "60vh"} diff --git a/app/components/link.py b/app/components/link.py index b09bc17..ba1e824 100644 --- a/app/components/link.py +++ b/app/components/link.py @@ -121,12 +121,18 @@ def create_link_div(self): Then click on the link to explore and fine-tune the policy in En-ROADS.", className=DESC_TEXT), html.Div( - dcc.Dropdown( - id="cand-link-select", - options=[], - placeholder="Select a policy", - ), - className="w-25 flex-grow-1" + className="d-flex flex-row w-25 justify-content-center", + children=[ + html.Label("Policy: ", className="pt-1 me-1"), + html.Div( + dcc.Dropdown( + id="cand-link-select", + options=[], + placeholder="Select a policy", + ), + className="flex-grow-1" + ) + ] ), dcc.Loading( type="circle", diff --git a/app/components/outcome.py b/app/components/outcome.py index b04d8be..c19b38f 100644 --- a/app/components/outcome.py +++ b/app/components/outcome.py @@ -1,7 +1,9 @@ """ OutcomeComponent class for the outcome section of the app. """ -from dash import Input, Output, State, html, dcc +import json + +from dash import Input, Output, State, html, dcc, MATCH import dash_bootstrap_components as dbc import pandas as pd import plotly.express as px @@ -30,6 +32,9 @@ def __init__(self, evolution_handler: EvolutionHandler): self.metric_ids = [metric.replace(" ", "-").replace(".", "_") for metric in self.evolution_handler.outcomes] + with open("app/units.json", "r", encoding="utf-8") as f: + self.units = json.load(f) + def plot_outcome_over_time(self, outcome: str, outcomes_jsonl: list[list[dict[str, float]]], cand_idxs: list[int]): """ Plots all the candidates' prescribed actions' outcomes for a given context. @@ -120,7 +125,8 @@ def plot_outcome_over_time(self, outcome: str, outcomes_jsonl: list[list[dict[st "text": f"{outcome} Over Time", "x": 0.5, "xanchor": "center"}, - yaxis_range=[y_min, y_max] + yaxis_range=[y_min, y_max], + yaxis_title=outcome + f" ({self.units[outcome]})" ) return fig @@ -142,33 +148,71 @@ def create_outcomes_div(self): children=[ html.Div( dcc.Dropdown( - id="outcome-dropdown-1", + id={"type": "outcome-dropdown", "index": 0}, options=self.plot_outcomes, - value=self.plot_outcomes[0] + value=self.plot_outcomes[0], + disabled=True ), className="flex-fill" ), html.Div( dcc.Dropdown( - id="outcome-dropdown-2", + id={"type": "outcome-dropdown", "index": 1}, options=self.plot_outcomes, - value=self.plot_outcomes[1] + value=self.plot_outcomes[1], + disabled=True ), className="flex-fill" ) ] ), dcc.Loading( - target_components={"context-actions-store": "*", "outcomes-store": "*"}, + target_components={"context-actions-store": "*"}, type="circle", children=[ dbc.Row( className="g-0", children=[ dcc.Store(id="context-actions-store"), + dbc.Col(dcc.Graph(id={"type": "outcome-graph", "index": 0}), width=6), + dbc.Col(dcc.Graph(id={"type": "outcome-graph", "index": 1}), width=6) + ] + ) + ] + ), + html.Div( + className="d-flex flex-row w-100", + children=[ + html.Div( + dcc.Dropdown( + id={"type": "outcome-dropdown", "index": 2}, + options=self.plot_outcomes, + value=self.plot_outcomes[2], + disabled=True + ), + className="flex-fill" + ), + html.Div( + dcc.Dropdown( + id={"type": "outcome-dropdown", "index": 3}, + options=self.plot_outcomes, + value=self.plot_outcomes[3], + disabled=True + ), + className="flex-fill" + ) + ] + ), + dcc.Loading( + target_components={"outcomes-store": "*"}, + type="circle", + children=[ + dbc.Row( + className="g-0", + children=[ dcc.Store(id="outcomes-store"), - dbc.Col(dcc.Graph(id="outcome-graph-1"), width=6), - dbc.Col(dcc.Graph(id="outcome-graph-2"), width=6) + dbc.Col(dcc.Graph(id={"type": "outcome-graph", "index": 2}), width=6), + dbc.Col(dcc.Graph(id={"type": "outcome-graph", "index": 3}), width=6) ] ) ] @@ -190,7 +234,8 @@ def register_callbacks(self, app): Output("metrics-store", "data"), Output("energy-policy-store", "data"), Input("presc-button", "n_clicks"), - [State(f"context-slider-{i}", "value") for i in range(4)] + [State(f"context-slider-{i}", "value") for i in range(4)], + prevent_initial_call=True ) def update_results_stores(_, *context_values): """ @@ -223,29 +268,29 @@ def update_results_stores(_, *context_values): return context_actions_dicts, outcomes_jsonl, metrics_json, energy_policy_jsonl @app.callback( - Output("outcome-graph-1", "figure"), - Output("outcome-graph-2", "figure"), + Output({"type": "outcome-graph", "index": MATCH}, "figure"), + Output({"type": "outcome-dropdown", "index": MATCH}, "disabled"), State("metrics-store", "data"), - Input("outcome-dropdown-1", "value"), - Input("outcome-dropdown-2", "value"), + Input({"type": "outcome-dropdown", "index": MATCH}, "value"), Input("outcomes-store", "data"), [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids], + prevent_initial_call=True ) - def update_outcomes_plots(metrics_json, outcome1, outcome2, outcomes_jsonl, *metric_ranges): + def update_outcomes_plots(metrics_json, outcome, outcomes_jsonl, *metric_ranges): """ Updates outcome plot when specific outcome is selected or context scatter point is clicked. + We also un-disable the dropdowns when the user selects a context. """ metrics_df = filter_metrics_json(metrics_json, metric_ranges) cand_idxs = list(metrics_df.index)[:-1] # So we don't include the baseline - - fig1 = self.plot_outcome_over_time(outcome1, outcomes_jsonl, cand_idxs) - fig2 = self.plot_outcome_over_time(outcome2, outcomes_jsonl, cand_idxs) - return fig1, fig2 + fig = self.plot_outcome_over_time(outcome, outcomes_jsonl, cand_idxs) + return fig, False @app.callback( Output("cand-link-select", "options"), State("metrics-store", "data"), - [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids] + [Input(f"{metric_id}-slider", "value") for metric_id in self.metric_ids], + prevent_initial_call=True ) def update_cand_link_select(metrics_json: dict[str, list], *metric_ranges: list[tuple[float, float]]) -> list[int]: diff --git a/app/components/references.py b/app/components/references.py index 0a3ed5c..f6ea767 100644 --- a/app/components/references.py +++ b/app/components/references.py @@ -29,6 +29,12 @@ def create_references_div(self): href="https://www.itu.int/en/ITU-T/extcoop/ai-data-commons/Pages/project-resilience.\ aspx") ]), + html.P([ + "Project Resilience is a collaboration between the United Nations and Cognizant Advanced \ + AI Labs. More info about the lab can be found here: ", + html.A("https://www.cognizant.com/us/en/services/ai/ai-lab", + href="https://www.cognizant.com/us/en/services/ai/ai-lab") + ]), html.P([ "The code for Project Resilience can be found on ", html.A("Github", href="https://github.com/project-resilience") diff --git a/app/units.json b/app/units.json new file mode 100644 index 0000000..dc8cc10 --- /dev/null +++ b/app/units.json @@ -0,0 +1,10 @@ +{ + "Temperature above 1.5C": "°C", + "Max cost of energy": "$/GJ", + "Government net revenue below zero": "T$", + "Total energy below baseline": "EJ", + "Temperature change from 1850": "°C", + "Adjusted cost of energy per GJ": "$/GJ", + "Government net revenue from adjustments": "T$", + "Total Primary Energy Demand": "EJ" +} \ No newline at end of file diff --git a/app/utils.py b/app/utils.py index 34a958f..a8eaa4d 100644 --- a/app/utils.py +++ b/app/utils.py @@ -37,6 +37,8 @@ def filter_metrics_json(metrics_json: dict[str, list], class EvolutionHandler(): """ Handles evolution results and running of prescriptors for the app. + TODO: Currently we hard-code some metrics to make the app prettier. Later we should just create more app-friendly + metrics to optimize in pymoo. """ def __init__(self): save_path = "app/results" @@ -52,6 +54,11 @@ def __init__(self): self.X = np.load(save_path + "/X.npy") self.F = np.load(save_path + "/F.npy") + # TODO: Make this not hard-coded + self.F[:, 0] += 1.5 + self.F[:, 2] *= -1 + self.F[:, 3] *= -1 + context_df = pd.read_csv("experiments/scenarios/gdp_context.csv") self.context_df = context_df.drop(columns=["F", "scenario"]) self.scaler = StandardScaler() @@ -60,43 +67,6 @@ def __init__(self): self.runner = EnroadsRunner() self.outcome_manager = OutcomeManager(list(self.outcomes.keys())) - def load_initial_metrics_df(self): - """ - Takes the F results matrix and converts it into a DataFrame the way pandas parcoords wants it. We also attach - the average of the baseline over all the contexts to this DataFrame. - """ - # Convert F to DataFrame - metrics_df = pd.DataFrame(self.F, columns=list(self.outcomes.keys())) - for outcome, ascending in self.outcomes.items(): - if not ascending: - metrics_df[outcome] *= -1 - metrics_df["cand_id"] = range(len(self.F)) - - # Run En-ROADS on baseline over all contexts - baseline_metrics_avg = {outcome: 0 for outcome in self.outcomes} - for _, row in self.context_df.iterrows(): - context_dict = row.to_dict() - baseline_outcomes = self.runner.evaluate_actions(context_dict) - baseline_metrics = self.outcome_manager.process_outcomes(context_dict, baseline_outcomes) - for outcome, val in baseline_metrics.items(): - baseline_metrics_avg[outcome] += val - - # Finish preprocessing baseline metrics - for outcome in self.outcomes: - baseline_metrics_avg[outcome] /= len(self.context_df) - baseline_metrics_avg["cand_id"] = "baseline" - - # Attach baseline to metrics_df - metrics_df = pd.concat([metrics_df, pd.DataFrame([baseline_metrics_avg])], axis=0, ignore_index=True) - - # TODO: Eventually don't hard-code this. Flip the net revenue below 0 to be something we minimize - if "Government net revenue below zero" in metrics_df.columns: - metrics_df["Government net revenue below zero"] *= -1 - if "Total energy below baseline" in metrics_df.columns: - metrics_df["Total energy below baseline"] *= -1 - - return metrics_df - def prescribe_all(self, context_dict: dict[str, float]): """ Takes a dict containing a single context and prescribes actions for it using all the candidates. @@ -132,6 +102,7 @@ def outcomes_to_metrics(self, """ Takes parallel lists of context_actions_dicts and outcomes_dfs and processes them into a metrics dict. All of these metrics dicts are then concatenated into a single DataFrame. + TODO: We hard-code some metrics to be more app-friendly """ metrics_dicts = [] for context_actions_dict, outcomes_df in zip(context_actions_dicts, outcomes_dfs): @@ -139,6 +110,11 @@ def outcomes_to_metrics(self, metrics_dicts.append(metrics) metrics_df = pd.DataFrame(metrics_dicts) + + metrics_df["Temperature above 1.5C"] += 1.5 + metrics_df["Government net revenue below zero"] *= -1 + metrics_df["Total energy below baseline"] *= -1 + return metrics_df def context_baseline_outcomes(self, context_dict: dict[str, float]):