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 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. """) # Validate necessary inputs if "constraints" not in st.session_state or "costs" not in st.session_state or "resource_caps" not in st.session_state: st.error("Missing inputs. Please complete previous steps.") return if "scenarios" not in st.session_state: st.session_state.scenarios = {} if sum(st.session_state.resource_caps.values()) <= 0: st.warning("\u26a0\ufe0f All resources are currently set to 0. Please increase availability.") return # Run optimization if st.button("Run Optimization"): 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("\u274c Optimization removed 0 tCO₂. Relax constraints or increase resources.") return 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"\u2705 Optimization successful: {total_removed:,.0f} tCO₂ removed") else: st.error("\u274c 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" )