From ed244223c0220ce06d04a7a06cd317c180b322d3 Mon Sep 17 00:00:00 2001 From: Maxime PERALTA Date: Thu, 27 Aug 2026 15:46:53 +0200 Subject: [PATCH] (update) Add heatmap result and panel, scenario panel uses all params by default --- apparun/gui/modules.py | 4 +- apparun/gui/panels/base.py | 147 ++++++++++++++++++++++++--- apparun/gui/panels/output_dynamic.py | 34 ++++++- apparun/results.py | 51 ++++++++++ samples/conf/sample_gui.yaml | 31 ++---- samples/scripts/python_api_usage.py | 13 +++ 6 files changed, 242 insertions(+), 38 deletions(-) diff --git a/apparun/gui/modules.py b/apparun/gui/modules.py index 9dd91cb..992996c 100644 --- a/apparun/gui/modules.py +++ b/apparun/gui/modules.py @@ -87,7 +87,9 @@ def run(self): ) if self.input_panel is not None: with self.input_col: - self.input_panel.run() + self.input_panel.run( + impact_model=self.impact_model, lca_data=self.lca_data + ) with self.output_col: for output_panel in self.output_panels: output_panel.run( diff --git a/apparun/gui/panels/base.py b/apparun/gui/panels/base.py index 79bf1d1..a72c27c 100644 --- a/apparun/gui/panels/base.py +++ b/apparun/gui/panels/base.py @@ -76,9 +76,6 @@ class Panel(BaseModel): st_component: Callable = None - def spawn(self): - return - @property def state(self): return self._state @@ -95,6 +92,14 @@ class DynamicOutputPanel(OutputPanel): type: Literal["dynamic_output_panel"] result: Optional[ImpactModelResult] = None + def run( + self, + entry_data, + impact_model: Optional[ImpactModel] = None, + lca_data: Optional[pd.DataFrame] = None, + ): + return + def compute_from_impact_model(self, entry_data, impact_model): return @@ -103,7 +108,7 @@ def fetch_from_lca_data(self, entry_data, lca_data): def get_results( self, - entry_data, + entry_data: Dict, impact_model: ImpactModel = None, lca_data: pd.DataFrame = None, ): @@ -119,7 +124,11 @@ def get_results( class StaticOutputPanel(OutputPanel): type: Literal["static_output_panel"] - def run(self, impact_model: ImpactModel = None, lca_data: pd.DataFrame = None): + def run( + self, + impact_model: Optional[ImpactModel] = None, + lca_data: Optional[pd.DataFrame] = None, + ): return @@ -131,13 +140,17 @@ def __init__(self, **args): super().__init__(**args) self._uuid = uuid.uuid4().hex - def submit(self): + def run( + self, + impact_model: Optional[ImpactModel] = None, + lca_data: Optional[pd.DataFrame] = None, + ): return @register_panel("input_scenario_form_panel") class InputScenarioFormPanel(InputPanel): - fields: Optional[List[Dict[str, Any]]] = [] + fields: Optional[List[Dict[str, Any]]] = None type: Literal["input_scenario_form_panel"] def __init__(self, **args): @@ -145,7 +158,11 @@ def __init__(self, **args): self._state["parameters"] = {} self._state["action"] = None - def run(self): + def run( + self, + impact_model: Optional[ImpactModel] = None, + lca_data: Optional[pd.DataFrame] = None, + ): self.st_component = st.form(self._uuid) if self.name is not None: @@ -154,19 +171,32 @@ def run(self): self._state["scenario_name"] = self.st_component.text_input( label="Scenario name" ) - for input_field in self.fields: - if input_field["type"] == "float": + selected_params = ( + self.fields + if self.fields is not None + else impact_model.parameters.to_list() + ) + + for selected_param in selected_params: + if selected_param["type"] == "float": self._state["parameters"][ - input_field["name"] + selected_param["name"] ] = self.st_component.text_input( - label=input_field["name"], value=input_field["default"] + label=selected_param["name"], value=selected_param["default"] + ) + if selected_param["type"] == "enum": + options = ( + selected_param["options"] + if self.fields is not None + else selected_param["weights"].keys() ) - if input_field["type"] == "enum": + self._state["parameters"][ - input_field["name"] + selected_param["name"] ] = self.st_component.selectbox( - label=input_field["name"], options=input_field["options"] + label=selected_param["name"], options=options ) + col_button1, col_button2 = st.columns(2) with col_button1: scenarios_add = self.st_component.form_submit_button("Add") @@ -176,3 +206,90 @@ def run(self): self._state["action"] = ACTION_ADD if scenarios_clear: self._state["action"] = ACTION_CLEAR + + +@register_panel("selectable_input_range_form_panel") +class SelectableInputRangeFormPanel(InputPanel): + dimensions: Optional[int] = 2 + type: Literal["selectable_input_range_form_panel"] + + def __init__(self, **args): + super().__init__(**args) + self._state["parameters"] = {} + self._state["action"] = None + + def run( + self, + impact_model: Optional[ImpactModel] = None, + lca_data: Optional[pd.DataFrame] = None, + ): + self.st_component = st.form(self._uuid) + + if self.name is not None: + self.st_component.markdown(f"### {self.name}") + + for i in range(self.dimensions): + col_button1, col_button2, col_button3 = self.st_component.columns( + [0.5, 0.25, 0.25] + ) + selected_param = {} + selected_param["name"] = col_button1.selectbox( + label=f"Param {i + 1}", + options=[ + parameter.name + for parameter in impact_model.parameters + if parameter.type == "float" + ], + ) + selected_param["min"] = float( + col_button2.text_input(label="Min", value=0, key=f"{i}-min") + ) + selected_param["max"] = float( + col_button3.text_input(label="Max", value=0, key=f"{i}-max") + ) + self._state["parameters"][str(i)] = selected_param + + scenarios_add = self.st_component.form_submit_button("Compute") + + if scenarios_add: + self._state["action"] = ACTION_ADD + + +@register_panel("input_range_form_panel") +class InputRangeFormPanel(InputPanel): + fields: Dict[str, Dict[str, Any]] + type: Literal["input_range_form_panel"] + + def __init__(self, **args): + super().__init__(**args) + self._state["parameters"] = self.fields + self._state["action"] = None + + def run( + self, + impact_model: Optional[ImpactModel] = None, + lca_data: Optional[pd.DataFrame] = None, + ): + self.st_component = st.form(self._uuid) + + if self.name is not None: + self.st_component.markdown(f"### {self.name}") + + for param_axis, param in self.fields.items(): + self.st_component.markdown(f'{param["name"]}') + + self._state["parameters"][param_axis]["min"] = float( + self.st_component.text_input( + label="Min", value=param["min"], key=f"{param_axis}-min" + ) + ) + self._state["parameters"][param_axis]["max"] = float( + self.st_component.text_input( + label="Max", value=param["max"], key=f"{param_axis}-max" + ) + ) + + scenarios_add = self.st_component.form_submit_button("Compute") + + if scenarios_add: + self._state["action"] = ACTION_ADD diff --git a/apparun/gui/panels/output_dynamic.py b/apparun/gui/panels/output_dynamic.py index 339db36..f38333b 100644 --- a/apparun/gui/panels/output_dynamic.py +++ b/apparun/gui/panels/output_dynamic.py @@ -10,7 +10,7 @@ register_panel, ) from apparun.impact_model import ImpactModel -from apparun.results import ImpactModelResult, ScenarioComparisonResult +from apparun.results import HeatmapResult, ImpactModelResult, ScenarioComparisonResult @register_panel("scenario_comparison_dynamic_output_panel") @@ -54,3 +54,35 @@ def run( st.plotly_chart(fig) if entry_data["action"] == ACTION_CLEAR: self._state["scenario_parameters"] = {} + + +@register_panel("heatmap_dynamic_output_panel") +class HeatmapDynamicOutputPanel(DynamicOutputPanel): + type: Literal["heatmap_dynamic_output_panel"] + impact_method: str + resolution: int = 64 + + def compute_from_impact_model(self, entry_data, impact_model): + self.result = HeatmapResult( + impact_model=impact_model, + x_parameter=entry_data["0"], + y_parameter=entry_data["1"], + impact_method=self.impact_method, + resolution=self.resolution, + ) + result_table = self.result.get_table() + return result_table + + def fetch_from_lca_data(self, entry_data, lca_data): + raise NotImplementedError() + + def run( + self, + entry_data, + impact_model: ImpactModel = None, + lca_data: pd.DataFrame = None, + ): + if entry_data["action"] == ACTION_ADD: + scores = self.get_results(entry_data["parameters"], impact_model, lca_data) + fig = self.result.get_figure(scores) + st.plotly_chart(fig) diff --git a/apparun/results.py b/apparun/results.py index f04c8f9..cd773fa 100644 --- a/apparun/results.py +++ b/apparun/results.py @@ -1,8 +1,10 @@ from __future__ import annotations +import itertools import os from typing import Any, Dict, List, Optional, Union +import numpy as np import pandas as pd import plotly.express as px import plotly.graph_objects as go @@ -413,3 +415,52 @@ def get_figure(self, table: pd.DataFrame, save: bool = False): if save: self.save_figure(fig) return fig + + +@register_result("heatmap") +class HeatmapResult(ImpactModelResult): + x_parameter: Dict[str, Union[str, float]] + y_parameter: Dict[str, Union[str, float]] + resolution: Optional[int] = 64 + impact_method: str + + def get_table(self) -> pd.DataFrame: + df = list( + itertools.product( + list( + np.arange( + self.x_parameter["min"], + self.x_parameter["max"], + (self.x_parameter["max"] - self.x_parameter["min"]) + / self.resolution, + ) + ), + list( + np.arange( + self.y_parameter["min"], + self.y_parameter["max"], + (self.y_parameter["max"] - self.y_parameter["min"]) + / self.resolution, + ) + ), + ) + ) + df = pd.DataFrame( + df, columns=[self.x_parameter["name"], self.y_parameter["name"]] + ) + scores = self.impact_model.get_scores(**df.to_dict(orient="list")) + df["score"] = scores.scores[self.impact_method] + df = df.pivot( + index=self.x_parameter["name"], + columns=self.y_parameter["name"], + values="score", + ) + return df + + def get_figure(self, table: pd.DataFrame, save: bool = False): + fig = px.imshow( + table, text_auto=False, aspect="auto", color_continuous_scale="RdBu_r" + ) + if save: + self.save_figure(fig) + return fig diff --git a/samples/conf/sample_gui.yaml b/samples/conf/sample_gui.yaml index 49a4083..5b13cb5 100644 --- a/samples/conf/sample_gui.yaml +++ b/samples/conf/sample_gui.yaml @@ -6,27 +6,6 @@ modules: input_panel: type: input_scenario_form_panel name: "GPU parameters" - fields: - - type: float - name: cuda_core - min: 0 - max: 1024 - default: 512 - - type: enum - name: architecture - options: - - Pascal - - Maxwell - - type: float - name: lifespan - min: 0 - max: 5 - default: 2 - - type: enum - name: usage_location - options: - - FR - - EU output_panels: - type: scenario_comparison_dynamic_output_panel y: EFV3_CLIMATE_CHANGE @@ -57,3 +36,13 @@ modules: - type: scenario_comparison_dynamic_output_panel y: EFV3_CLIMATE_CHANGE hue: component + - impact_model_path: "samples/impact_models/nvidia_ai_gpu_chip.yaml" + name: "Heatmap" + input_panel: + type: selectable_input_range_form_panel + name: "Select parameters" + dimension: 2 + output_panels: + - type: heatmap_dynamic_output_panel + impact_method: EFV3_CLIMATE_CHANGE + resolution: 64 \ No newline at end of file diff --git a/samples/scripts/python_api_usage.py b/samples/scripts/python_api_usage.py index 0de99d4..d445426 100644 --- a/samples/scripts/python_api_usage.py +++ b/samples/scripts/python_api_usage.py @@ -112,6 +112,19 @@ scenario_comparison_table = scenario_comparison_result.get_table() scenario_comparison_result.get_figure(scenario_comparison_table, save=True) +heatmap_result = get_result("heatmap")( + impact_model=impact_model, + impact_method="EFV3_CLIMATE_CHANGE", + x_parameter={"name": "cuda_core", "min": 256, "max": 2048}, + y_parameter={"name": "lifespan", "min": 1, "max": 5}, + output_name="heatmap", + pdf_save_path=os.path.join(OUTPUT_FILES_PATH, "figures/"), + table_save_path=os.path.join(OUTPUT_FILES_PATH, "tables/"), + html_save_path=os.path.join(OUTPUT_FILES_PATH, "figures/"), +) +heatmap_table = heatmap_result.get_table() +heatmap_result.get_figure(heatmap_table, save=True) + # New types of results can be generated in user script, without modifying Appa Run # source code, thanks to register_result decorator.