import io import json import unicodedata import zipfile import matplotlib.pyplot as plt import pandas as pd import seaborn as sns import streamlit as st from config.config import method_order, resource_order, resource_units from optimization.optimization import run_optimization from utils.ui_helpers import format_results_summary from utils.data_utils import is_valid_scenario def render_tab(): st.header("Portfolio generation") st.caption("Tab 4 - Seaborn fallback for vertical histograms") st.info(""" **Objective:** Run the optimization tool, explore results, and save scenarios for comparison or export. """) # Initialize necessary session state variables if they don't exist if "resource_caps" not in st.session_state: st.session_state.resource_caps = {} if "constraints" not in st.session_state: st.session_state.constraints = {} if "costs" not in st.session_state: st.session_state.costs = {} if "scenarios" not in st.session_state: st.session_state.scenarios = {} # Run optimization st.markdown("### Run optimization") col_run, col_load = st.columns(2) with col_run: st.markdown("**From current input values**") run_clicked = st.button("▶️ Run Optimization", use_container_width=False) with col_load: st.markdown("**From a saved JSON scenario**") uploaded_scenario = st.file_uploader("Upload a saved JSON scenario", type="json", key="scenario_loader") if uploaded_scenario is not None: try: data = json.load(uploaded_scenario) if is_valid_scenario(data): if st.button("↩️ Reload this scenario into all tabs", use_container_width=True): st.session_state.resource_caps = data["resource_caps"].copy() st.session_state.constraints = data["method_constraints"].copy() st.session_state.costs = data["method_costs"].copy() keys_to_delete = [k for k in st.session_state if any( k.startswith(prefix) for prefix in ["slider_", "manual_", "toggle_"] )] for k in keys_to_delete: del st.session_state[k] st.session_state.latest_result = data st.success("Scenario loaded. All tabs have been updated.") st.rerun() else: st.error("Invalid scenario format.") except Exception as e: st.error(f"Error loading scenario: {e}") # Vérifications et exécution — uniquement si Run Optimization cliqué if run_clicked: if sum(st.session_state.resource_caps.values()) <= 0: st.warning("⚠️ All resources are currently set to 0. Please increase availability or load a saved scenario.") else: success, result = run_optimization( resource_caps=st.session_state.resource_caps, method_constraints=st.session_state.constraints, method_costs=st.session_state.costs, ) if success: allocations = result["method_usage"] total_removed = result["total_removed"] if total_removed <= 0: st.warning("❌ Optimization removed 0 tCO₂. Relax constraints or increase resources.") else: total_cost = sum( allocations[m] * sum(st.session_state.costs[m].values()) for m in allocations ) st.session_state.latest_result = { "summary": {"total_co2_removed": total_removed, "total_cost": total_cost}, "allocations": allocations, "resource_caps": st.session_state.resource_caps.copy(), "method_constraints": st.session_state.constraints, "method_costs": st.session_state.costs, } st.success(f"✅ Optimization successful: {total_removed:,.0f} tCO₂ removed") else: st.error("❌ Optimization failed") if "message" in result: st.error(result["message"]) if "latest_result" not in st.session_state: return latest = st.session_state.latest_result allocations = latest["allocations"] resource_caps = latest["resource_caps"] method_costs = latest["method_costs"] if not allocations or sum(allocations.values()) == 0: return # Metrics col1, col2 = st.columns(2) col1.metric("Total CO₂ Removed", f"{latest['summary']['total_co2_removed']:,.0f} t") col2.metric("Methods Used", len(allocations)) # Summary table st.subheader("Optimization Summary") df_result = pd.DataFrame.from_dict(allocations, orient="index", columns=["tCO₂ removed"]) df_result["tCO₂ removed"] = pd.to_numeric(df_result["tCO₂ removed"], errors="coerce") total = df_result["tCO₂ removed"].sum() if total > 0: df_result["Share (%)"] = (df_result["tCO₂ removed"] / total * 100).round(2) df_result = df_result.sort_values(by="tCO₂ removed", ascending=False) st.table(format_results_summary(df_result)) # Donut chart st.markdown("### Method allocation breakdown") labels = df_result.index.tolist() values = df_result["tCO₂ removed"].tolist() import plotly.graph_objects as go fig_donut = go.Figure(data=[go.Pie( labels=labels, values=values, hole=0.4, textinfo="label+percent", textposition="outside", marker=dict(line=dict(color="#000000", width=1)), showlegend=False )]) fig_donut.update_layout(margin=dict(t=10, b=10)) st.plotly_chart(fig_donut, use_container_width=True) # Heatmap st.markdown("### Resource usage heatmap (% of cap)") heatmap_matrix, heatmap_y = [], [] available_resources = [r for r in resource_order if resource_caps.get(r, 0) > 0] for method in df_result.index: row = [] for r in available_resources: usage = method_costs.get(method, {}).get(r, 0.0) * allocations.get(method, 0.0) row.append((usage / resource_caps[r]) * 100 if resource_caps[r] > 0 else 0) heatmap_matrix.append(row) heatmap_y.append(method) fig_heatmap = go.Figure(data=go.Heatmap( z=heatmap_matrix, x=available_resources, y=heatmap_y, colorscale="Blues", hovertemplate="Method: %{y}
Resource: %{x}
% Used: %{z:.2f}%", )) fig_heatmap.update_layout( title="Resource Usage vs. Cap", xaxis_title="Resource", yaxis_title="Method", height=400, ) st.plotly_chart(fig_heatmap, use_container_width=True) # Absolute usage (fallback with seaborn) st.markdown("### Absolute resource usage by unit (Seaborn)") df_abs = [] for m, co2_removed in allocations.items(): for unit, resources in resource_units.items(): for r in resources: if r in method_costs.get(m, {}): used = method_costs[m][r] * co2_removed if used > 0: df_abs.append({ "Method": m, "Resource": r, "Used": used, "Unit": unit }) if df_abs: df_abs = pd.DataFrame(df_abs) for unit in df_abs["Unit"].unique(): st.markdown(f"#### Resources in {unit}") df_unit = df_abs[df_abs["Unit"] == unit] fig, ax = plt.subplots(figsize=(8, 4 + 0.3 * df_unit["Resource"].nunique())) sns.barplot(data=df_unit, x="Resource", y="Used", hue="Method", ax=ax) ax.set_ylabel(f"Used ({unit})") ax.set_xlabel("Resource") ax.tick_params(axis='x', rotation=45) ax.legend(title="Method", bbox_to_anchor=(1.05, 1), loc='upper left') st.pyplot(fig) # Save scenario st.markdown("### Save scenario") st.text_input("Scenario name", key="scenario_name") if st.button("Save Scenario"): name = st.session_state.get("scenario_name", "").strip() if not name: st.warning("Please enter a name before saving.") elif name in st.session_state.scenarios: st.warning("Scenario name already exists.") else: st.session_state.scenarios[name] = latest st.success(f"Scenario '{name}' saved.") # Preview saved scenario current_name = st.session_state.get("scenario_name", "").strip() if current_name and current_name in st.session_state.scenarios: scenario = st.session_state.scenarios[current_name] st.markdown(f"#### Scenario: {current_name}") df_saved = pd.DataFrame.from_dict( scenario["allocations"], orient="index", columns=["tCO₂ removed"] ) df_saved["tCO₂ removed"] = pd.to_numeric(df_saved["tCO₂ removed"], errors="coerce") total = df_saved["tCO₂ removed"].sum() if total > 0: df_saved["Share (%)"] = (df_saved["tCO₂ removed"] / total * 100).round(2) df_saved = df_saved.sort_values(by="tCO₂ removed", ascending=False) st.table(format_results_summary(df_saved)) # Export buttons safe_name = unicodedata.normalize("NFKD", current_name).encode("ascii", "ignore").decode("ascii") st.download_button( label="Export JSON", data=json.dumps(scenario, indent=2), file_name=f"{safe_name}_scenario.json", mime="application/json" ) inputs_df = pd.DataFrame.from_dict(scenario["resource_caps"], orient="index", columns=["Available"]) constraints_df = pd.DataFrame.from_dict({m: v.get("resources", {}) for m, v in scenario["method_constraints"].items()}, orient="index").fillna(0) allocations_df = pd.DataFrame.from_dict(scenario["allocations"], orient="index", columns=["tCO₂ removed"]) csv_buffer = io.BytesIO() with zipfile.ZipFile(csv_buffer, "w") as zipf: zipf.writestr("resource_caps.csv", inputs_df.to_csv()) zipf.writestr("method_constraints.csv", constraints_df.to_csv()) zipf.writestr("allocations.csv", allocations_df.to_csv()) st.download_button( label="Export CSV", data=csv_buffer.getvalue(), file_name=f"{safe_name}_scenario.zip", mime="application/zip" )