CarlyneB commited on
Commit
ca86b1a
Β·
1 Parent(s): 6d118db

[FEAT] scenario & input management: Excel/JSON/ZIP(csv) import, save/export, multi-scenario comparison, clear all data

Browse files
app.py CHANGED
@@ -5,6 +5,7 @@ from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
5
  tab3_coefficients, tab4_optimization, tab5_sensitivity,
6
  tab6_scenarios)
7
  from utils.data_utils import load_slider_data
 
8
 
9
  st.set_page_config("Carbon Portfolio Optimizer", layout="wide")
10
  st.title("Toolkit: CDR Portfolio Optimization")
@@ -26,9 +27,24 @@ with st.sidebar:
26
  orientation="vertical"
27
  )
28
 
29
- # Dispatch to appropriate tab
30
- cost_sliders = load_slider_data()
 
 
 
 
 
31
 
 
 
 
 
 
 
 
 
 
 
32
 
33
  if selected == "Welcome":
34
  tab0_welcome.render_tab()
 
5
  tab3_coefficients, tab4_optimization, tab5_sensitivity,
6
  tab6_scenarios)
7
  from utils.data_utils import load_slider_data
8
+ from config.config import method_order, secondary_resources
9
 
10
  st.set_page_config("Carbon Portfolio Optimizer", layout="wide")
11
  st.title("Toolkit: CDR Portfolio Optimization")
 
27
  orientation="vertical"
28
  )
29
 
30
+ # Initialize data to default values
31
+ # Initialize constraints state
32
+ if "constraints" not in st.session_state:
33
+ st.session_state.constraints = {}
34
+ for m in method_order:
35
+ if m not in st.session_state.constraints:
36
+ st.session_state.constraints[m] = {"active": True, "max_share": 1.0}
37
 
38
+ # Initialize cost_sliders
39
+ cost_sliders = load_slider_data()
40
+ if "costs" not in st.session_state or st.session_state.costs == {}:
41
+ st.session_state.costs = {
42
+ m: {
43
+ r: (0.0 if (m, r) in secondary_resources else cost_sliders[m][r]["median"])
44
+ for r in cost_sliders[m]
45
+ }
46
+ for m in cost_sliders
47
+ }
48
 
49
  if selected == "Welcome":
50
  tab0_welcome.render_tab()
data/generate_input_template.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import sys
3
+ import pandas as pd
4
+ import openpyxl
5
+ from openpyxl.styles import Font
6
+
7
+ from pathlib import Path
8
+ sys.path.insert(0, str(Path(__file__).parent.parent))
9
+ from config.config import (resource_order, method_order)
10
+
11
+ # Data
12
+ with open("data/cost_sliders.json") as f:
13
+ cost_sliders = json.load(f)
14
+
15
+ secondary_df = pd.read_csv("data/secondary_resources.csv")
16
+ secondary_resources = set(zip(secondary_df["method"], secondary_df["resource"]))
17
+
18
+ wb = openpyxl.Workbook()
19
+
20
+ # resource_caps
21
+ ws1 = wb.active
22
+ ws1.title = "resource_caps"
23
+ ws1.append(["Resource", "Available Amount"])
24
+ ws1["A1"].font = Font(bold=True)
25
+ ws1["B1"].font = Font(bold=True)
26
+ for r in resource_order:
27
+ ws1.append([r, float("nan")])
28
+
29
+ # constraints
30
+ ws2 = wb.create_sheet("constraints")
31
+ ws2.append(["method", "active", "max_share"])
32
+ ws2["A1"].font = Font(bold=True)
33
+ ws2["B1"].font = Font(bold=True)
34
+ ws2["C1"].font = Font(bold=True)
35
+ for m in method_order:
36
+ ws2.append([m, True, 1.0])
37
+
38
+ ws3 = wb.create_sheet("coefficients")
39
+ ws3.append(["method", "resource", "value", "secondary"])
40
+ ws3["A1"].font = Font(bold=True)
41
+ ws3["B1"].font = Font(bold=True)
42
+ ws3["C1"].font = Font(bold=True)
43
+ ws3["D1"].font = Font(bold=True)
44
+ for m in method_order:
45
+ for r, conf in cost_sliders.get(m, {}).items():
46
+ is_secondary = (m, r) in secondary_resources
47
+ ws3.append([m, r, conf["median"], True if is_secondary else False])
48
+
49
+ wb.save("data/inputs_template.xlsx")
50
+ print("Template generated: data/inputs_template.xlsx")
data/inputs_template.xlsx ADDED
Binary file (9.64 kB). View file
 
tabs/tab1_inputs.py CHANGED
@@ -42,41 +42,57 @@ def render_tab():
42
  st.markdown("Use the template below to fill in your resource availability values and import them directly into the tool.")
43
  with open("data/resource_template.xlsx", "rb") as f:
44
  st.download_button(
45
- label="πŸ“₯ Download Excel Template",
46
  data=f,
47
  file_name="resource_template.xlsx",
48
  use_container_width= True,
49
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
 
50
  )
51
  with col_upload:
52
- uploaded_file = st.file_uploader("Upload your Excel file", type="xlsx")
53
- if uploaded_file is not None:
 
 
 
 
54
  try:
55
  df = pd.read_excel(uploaded_file)
56
  required_cols = {"Resource", "Available Amount"}
57
  if not required_cols.issubset(df.columns):
58
  raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
 
 
 
 
 
 
59
  for _, row in df.iterrows():
60
  r = row["Resource"]
61
  val = row["Available Amount"]
62
  if r not in st.session_state.resource_caps:
63
  continue
64
- if pd.isna(val):
65
- continue
66
  try:
67
- val = float(val)
68
  except (ValueError, TypeError):
69
- continue
70
- if val == 0:
71
- continue
72
  st.session_state.resource_caps[r] = val
73
- st.success(f"Resource values imported successfully.", icon="βœ…")
74
 
75
- for r, v in st.session_state.resource_caps.items():
76
- st.session_state[f"cap_{r}"] = v
 
 
 
 
 
 
77
 
78
  except Exception as e:
79
  st.error(f"Error importing resource values: {e}")
 
 
 
80
 
81
  col_reset, _ = st.columns([1, 5])
82
 
 
42
  st.markdown("Use the template below to fill in your resource availability values and import them directly into the tool.")
43
  with open("data/resource_template.xlsx", "rb") as f:
44
  st.download_button(
45
+ label="Download Excel Template",
46
  data=f,
47
  file_name="resource_template.xlsx",
48
  use_container_width= True,
49
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
50
+ icon=":material/download:"
51
  )
52
  with col_upload:
53
+ uploaded_file = st.file_uploader(
54
+ "Upload your Excel file", type="xlsx",
55
+ key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}"
56
+ )
57
+
58
+ if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
59
  try:
60
  df = pd.read_excel(uploaded_file)
61
  required_cols = {"Resource", "Available Amount"}
62
  if not required_cols.issubset(df.columns):
63
  raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
64
+
65
+ # Reset tout Γ  0 avant d'appliquer le fichier
66
+ for r in st.session_state.resource_caps:
67
+ st.session_state.resource_caps[r] = 0.0
68
+ st.session_state[f"cap_{r}"] = 0.0
69
+
70
  for _, row in df.iterrows():
71
  r = row["Resource"]
72
  val = row["Available Amount"]
73
  if r not in st.session_state.resource_caps:
74
  continue
 
 
75
  try:
76
+ val = 0.0 if pd.isna(val) else float(val)
77
  except (ValueError, TypeError):
78
+ val = 0.0
 
 
79
  st.session_state.resource_caps[r] = val
80
+ st.session_state[f"cap_{r}"] = val
81
 
82
+ st.session_state["resource_last_loaded_file"] = uploaded_file.name
83
+ st.success("Resource values imported successfully.", icon="βœ…")
84
+ st.session_state.pop("excel_loaded_file", None)
85
+ st.session_state.pop("scenario_name_suggestion", None)
86
+ st.session_state.pop("scenario_loaded_file", None)
87
+ st.session_state.pop("input_file_name_suggestion", None)
88
+ st.session_state.pop("latest_result", None)
89
+ st.rerun()
90
 
91
  except Exception as e:
92
  st.error(f"Error importing resource values: {e}")
93
+
94
+ elif st.session_state.get("resource_last_loaded_file"):
95
+ st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="βœ…")
96
 
97
  col_reset, _ = st.columns([1, 5])
98
 
tabs/tab2_constraints.py CHANGED
@@ -20,13 +20,6 @@ def render_tab():
20
  - Set each method's **max share** (as % of the total portfolio)
21
  """)
22
 
23
- # Initialize constraints state
24
- if "constraints" not in st.session_state:
25
- st.session_state.constraints = {}
26
- for m in method_order:
27
- if m not in st.session_state.constraints:
28
- st.session_state.constraints[m] = {"active": True, "max_share": 1.0}
29
-
30
  # UI controls
31
  with st.expander("βš™οΈ Manage methods (Open/Close)", expanded=False):
32
  st.subheader("Activate/deactivate methods and set max allocation")
 
20
  - Set each method's **max share** (as % of the total portfolio)
21
  """)
22
 
 
 
 
 
 
 
 
23
  # UI controls
24
  with st.expander("βš™οΈ Manage methods (Open/Close)", expanded=False):
25
  st.subheader("Activate/deactivate methods and set max allocation")
tabs/tab3_coefficients.py CHANGED
@@ -9,16 +9,6 @@ from utils.ui_helpers import format_efficiency_summary
9
  def render_tab(cost_sliders):
10
  st.header("Resource efficiency coefficients")
11
 
12
- # === Initialization ===
13
- if "costs" not in st.session_state or st.session_state.costs == {}:
14
- st.session_state.costs = {
15
- m: {
16
- r: (0.0 if (m, r) in secondary_resources else cost_sliders[m][r]["median"])
17
- for r in cost_sliders[m]
18
- }
19
- for m in cost_sliders
20
- }
21
-
22
  # === Info ===
23
  st.info("""
24
  **Objective:**
 
9
  def render_tab(cost_sliders):
10
  st.header("Resource efficiency coefficients")
11
 
 
 
 
 
 
 
 
 
 
 
12
  # === Info ===
13
  st.info("""
14
  **Objective:**
tabs/tab4_optimization.py CHANGED
@@ -2,255 +2,505 @@ import io
2
  import json
3
  import unicodedata
4
  import zipfile
 
5
 
6
  import matplotlib.pyplot as plt
7
  import pandas as pd
8
  import seaborn as sns
9
  import streamlit as st
 
10
 
11
- from config.config import method_order, resource_order, resource_units
12
  from optimization.optimization import run_optimization
13
  from utils.ui_helpers import format_results_summary
14
- from utils.data_utils import is_valid_scenario
15
 
16
 
17
- def render_tab():
18
- st.header("Portfolio generation")
19
- st.caption("Tab 4 - Seaborn fallback for vertical histograms")
20
-
21
- st.info("""
22
- **Objective:**
23
- Run the optimization tool, explore results, and save scenarios for comparison or export.
24
- """)
25
-
26
- # Initialize necessary session state variables if they don't exist
27
- if "resource_caps" not in st.session_state:
28
- st.session_state.resource_caps = {}
29
- if "constraints" not in st.session_state:
30
- st.session_state.constraints = {}
31
- if "costs" not in st.session_state:
32
- st.session_state.costs = {}
33
- if "scenarios" not in st.session_state:
34
- st.session_state.scenarios = {}
35
-
36
- # Run optimization
37
- st.markdown("### Run optimization")
38
- col_run, col_load = st.columns(2)
39
-
40
- with col_run:
41
- st.markdown("**From current input values**")
42
- run_clicked = st.button("▢️ Run Optimization", use_container_width=False)
43
-
44
- with col_load:
45
- st.markdown("**From a saved JSON scenario**")
46
- uploaded_scenario = st.file_uploader("Upload a saved JSON scenario", type="json", key="scenario_loader")
47
- if uploaded_scenario is not None:
48
- try:
49
- data = json.load(uploaded_scenario)
50
- if is_valid_scenario(data):
51
- if st.button("↩️ Reload this scenario into all tabs", use_container_width=True):
52
- st.session_state.resource_caps = data["resource_caps"].copy()
53
- st.session_state.constraints = data["method_constraints"].copy()
54
- st.session_state.costs = data["method_costs"].copy()
55
- keys_to_delete = [k for k in st.session_state if any(
56
- k.startswith(prefix) for prefix in ["slider_", "manual_", "toggle_"]
57
- )]
58
- for k in keys_to_delete:
59
- del st.session_state[k]
60
- st.session_state.latest_result = data
61
- st.success("Scenario loaded. All tabs have been updated.")
62
- st.rerun()
63
- else:
64
- st.error("Invalid scenario format.")
65
- except Exception as e:
66
- st.error(f"Error loading scenario: {e}")
67
-
68
- # VΓ©rifications et exΓ©cution β€” uniquement si Run Optimization cliquΓ©
69
- if run_clicked:
70
- if sum(st.session_state.resource_caps.values()) <= 0:
71
- st.warning("⚠️ All resources are currently set to 0. Please increase availability or load a saved scenario.")
72
- else:
73
- success, result = run_optimization(
74
- resource_caps=st.session_state.resource_caps,
75
- method_constraints=st.session_state.constraints,
76
- method_costs=st.session_state.costs,
77
  )
78
- if success:
79
- allocations = result["method_usage"]
80
- total_removed = result["total_removed"]
81
- if total_removed <= 0:
82
- st.warning("❌ Optimization removed 0 tCOβ‚‚. Relax constraints or increase resources.")
83
- else:
84
- total_cost = sum(
85
- allocations[m] * sum(st.session_state.costs[m].values())
86
- for m in allocations
87
- )
88
- st.session_state.latest_result = {
89
- "summary": {"total_co2_removed": total_removed, "total_cost": total_cost},
90
- "allocations": allocations,
91
- "resource_caps": st.session_state.resource_caps.copy(),
92
- "method_constraints": st.session_state.constraints,
93
- "method_costs": st.session_state.costs,
94
- }
95
- st.success(f"βœ… Optimization successful: {total_removed:,.0f} tCOβ‚‚ removed")
96
- else:
97
- st.error("❌ Optimization failed")
98
- if "message" in result:
99
- st.error(result["message"])
100
 
101
- if "latest_result" not in st.session_state:
 
 
 
 
 
 
 
102
  return
103
- latest = st.session_state.latest_result
104
- allocations = latest["allocations"]
105
- resource_caps = latest["resource_caps"]
106
- method_costs = latest["method_costs"]
107
 
108
- if not allocations or sum(allocations.values()) == 0:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
 
111
- # Metrics
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  col1, col2 = st.columns(2)
113
  col1.metric("Total COβ‚‚ Removed", f"{latest['summary']['total_co2_removed']:,.0f} t")
114
- col2.metric("Methods Used", len(allocations))
115
 
116
- # Summary table
117
- st.subheader("Optimization Summary")
118
- df_result = pd.DataFrame.from_dict(allocations, orient="index", columns=["tCOβ‚‚ removed"])
119
- df_result["tCOβ‚‚ removed"] = pd.to_numeric(df_result["tCOβ‚‚ removed"], errors="coerce")
120
- total = df_result["tCOβ‚‚ removed"].sum()
121
  if total > 0:
122
- df_result["Share (%)"] = (df_result["tCOβ‚‚ removed"] / total * 100).round(2)
123
- df_result = df_result.sort_values(by="tCOβ‚‚ removed", ascending=False)
124
- st.table(format_results_summary(df_result))
125
 
126
- # Donut chart
127
- st.markdown("### Method allocation breakdown")
128
- labels = df_result.index.tolist()
129
- values = df_result["tCOβ‚‚ removed"].tolist()
130
 
131
- import plotly.graph_objects as go
132
- fig_donut = go.Figure(data=[go.Pie(
133
- labels=labels,
134
- values=values,
135
  hole=0.4,
136
  textinfo="label+percent",
137
  textposition="outside",
138
  marker=dict(line=dict(color="#000000", width=1)),
139
- showlegend=False
140
  )])
141
- fig_donut.update_layout(margin=dict(t=10, b=10))
142
- st.plotly_chart(fig_donut, use_container_width=True)
143
 
144
- # Heatmap
145
- st.markdown("### Resource usage heatmap (% of cap)")
146
- heatmap_matrix, heatmap_y = [], []
147
- available_resources = [r for r in resource_order if resource_caps.get(r, 0) > 0]
148
 
 
 
 
149
  for method in df_result.index:
150
- row = []
151
- for r in available_resources:
152
- usage = method_costs.get(method, {}).get(r, 0.0) * allocations.get(method, 0.0)
153
- row.append((usage / resource_caps[r]) * 100 if resource_caps[r] > 0 else 0)
154
- heatmap_matrix.append(row)
155
- heatmap_y.append(method)
156
-
157
- fig_heatmap = go.Figure(data=go.Heatmap(
158
- z=heatmap_matrix,
159
- x=available_resources,
160
- y=heatmap_y,
161
  colorscale="Blues",
162
  hovertemplate="Method: %{y}<br>Resource: %{x}<br>% Used: %{z:.2f}%<extra></extra>",
163
  ))
164
- fig_heatmap.update_layout(
165
  title="Resource Usage vs. Cap",
166
- xaxis_title="Resource",
167
- yaxis_title="Method",
168
- height=400,
169
  )
170
- st.plotly_chart(fig_heatmap, use_container_width=True)
171
-
172
- # Absolute usage (fallback with seaborn)
173
- st.markdown("### Absolute resource usage by unit (Seaborn)")
174
- df_abs = []
175
- for m, co2_removed in allocations.items():
176
- for unit, resources in resource_units.items():
177
- for r in resources:
178
- if r in method_costs.get(m, {}):
179
- used = method_costs[m][r] * co2_removed
180
- if used > 0:
181
- df_abs.append({
182
- "Method": m,
183
- "Resource": r,
184
- "Used": used,
185
- "Unit": unit
186
- })
187
-
188
- if df_abs:
189
- df_abs = pd.DataFrame(df_abs)
190
- for unit in df_abs["Unit"].unique():
191
- st.markdown(f"#### Resources in {unit}")
192
- df_unit = df_abs[df_abs["Unit"] == unit]
193
-
194
- fig, ax = plt.subplots(figsize=(8, 4 + 0.3 * df_unit["Resource"].nunique()))
195
- sns.barplot(data=df_unit, x="Resource", y="Used", hue="Method", ax=ax)
196
- ax.set_ylabel(f"Used ({unit})")
197
- ax.set_xlabel("Resource")
198
- ax.tick_params(axis='x', rotation=45)
199
- ax.legend(title="Method", bbox_to_anchor=(1.05, 1), loc='upper left')
200
- st.pyplot(fig)
201
-
202
- # Save scenario
203
  st.markdown("### Save scenario")
204
- st.text_input("Scenario name", key="scenario_name")
205
-
206
- if st.button("Save Scenario"):
207
- name = st.session_state.get("scenario_name", "").strip()
208
- if not name:
209
- st.warning("Please enter a name before saving.")
210
- elif name in st.session_state.scenarios:
211
- st.warning("Scenario name already exists.")
212
- else:
213
- st.session_state.scenarios[name] = latest
214
- st.success(f"Scenario '{name}' saved.")
215
-
216
- # Preview saved scenario
217
- current_name = st.session_state.get("scenario_name", "").strip()
218
- if current_name and current_name in st.session_state.scenarios:
219
- scenario = st.session_state.scenarios[current_name]
220
- st.markdown(f"#### Scenario: {current_name}")
221
- df_saved = pd.DataFrame.from_dict(
222
- scenario["allocations"], orient="index", columns=["tCOβ‚‚ removed"]
223
  )
224
- df_saved["tCOβ‚‚ removed"] = pd.to_numeric(df_saved["tCOβ‚‚ removed"], errors="coerce")
225
- total = df_saved["tCOβ‚‚ removed"].sum()
226
- if total > 0:
227
- df_saved["Share (%)"] = (df_saved["tCOβ‚‚ removed"] / total * 100).round(2)
228
- df_saved = df_saved.sort_values(by="tCOβ‚‚ removed", ascending=False)
229
- st.table(format_results_summary(df_saved))
 
 
 
 
 
 
 
 
 
230
 
231
- # Export buttons
232
- safe_name = unicodedata.normalize("NFKD", current_name).encode("ascii", "ignore").decode("ascii")
 
 
233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  st.download_button(
235
- label="Export JSON",
236
- data=json.dumps(scenario, indent=2),
237
- file_name=f"{safe_name}_scenario.json",
238
- mime="application/json"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  )
240
 
241
- inputs_df = pd.DataFrame.from_dict(scenario["resource_caps"], orient="index", columns=["Available"])
242
- constraints_df = pd.DataFrame.from_dict({m: v.get("resources", {}) for m, v in scenario["method_constraints"].items()}, orient="index").fillna(0)
243
- allocations_df = pd.DataFrame.from_dict(scenario["allocations"], orient="index", columns=["tCOβ‚‚ removed"])
 
 
 
 
 
 
 
 
244
 
245
- csv_buffer = io.BytesIO()
246
- with zipfile.ZipFile(csv_buffer, "w") as zipf:
247
- zipf.writestr("resource_caps.csv", inputs_df.to_csv())
248
- zipf.writestr("method_constraints.csv", constraints_df.to_csv())
249
- zipf.writestr("allocations.csv", allocations_df.to_csv())
250
 
 
 
 
 
 
 
251
  st.download_button(
252
- label="Export CSV",
253
- data=csv_buffer.getvalue(),
254
- file_name=f"{safe_name}_scenario.zip",
255
- mime="application/zip"
 
 
256
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import json
3
  import unicodedata
4
  import zipfile
5
+ import openpyxl
6
 
7
  import matplotlib.pyplot as plt
8
  import pandas as pd
9
  import seaborn as sns
10
  import streamlit as st
11
+ import plotly.graph_objects as go
12
 
13
+ from config.config import method_order, resource_order, resource_units, secondary_resources
14
  from optimization.optimization import run_optimization
15
  from utils.ui_helpers import format_results_summary
16
+ from utils.data_utils import is_valid_scenario, load_inputs_from_excel
17
 
18
 
19
+ # ─── Input panels ────────────────────────────────────────────────────────────
20
+
21
+ def panel_run_from_tabs():
22
+ """Left sub-panel: run optimization from manually filled tabs."""
23
+ st.markdown("**Option 1 - From current tab values**")
24
+ st.caption("Set resource availability and method constraints in the tabs above, then run.")
25
+ st.divider()
26
+
27
+
28
+ def panel_load_from_excel():
29
+ """Left sub-panel: load inputs from an Excel file, then run."""
30
+ col_markdown, col_upload = st.columns([1, 1], vertical_alignment="center")
31
+
32
+ with col_markdown:
33
+ st.markdown("**Option 2 - From an Excel file**")
34
+
35
+ with col_upload:
36
+ with open("data/inputs_template.xlsx", "rb") as f:
37
+ st.download_button(
38
+ label="Download Excel Template",
39
+ data=f,
40
+ file_name="crra_input_template.xlsx",
41
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
42
+ use_container_width=True,
43
+ icon=":material/download:",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ st.caption("Fill in the template with your values, then import and run.")
47
+
48
+ if "excel_loaded_file" in st.session_state:
49
+ st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="βœ…")
50
+
51
+ uploaded = st.file_uploader("Upload inputs (.xlsx)", type=["xlsx"], key=f"inputs_loader_{st.session_state.excel_uploader_key}")
52
+
53
+ if uploaded is None:
54
  return
 
 
 
 
55
 
56
+ default_name = uploaded.name.rsplit(".", 1)[0]
57
+
58
+ if st.button("↩️ Load inputs into all tabs", use_container_width=True):
59
+ st.session_state.setdefault("input_file_name_suggestion", default_name)
60
+ try:
61
+ resource_caps, constraints, costs = load_inputs_from_excel(uploaded)
62
+ st.session_state.resource_caps = resource_caps
63
+ st.session_state.constraints = constraints
64
+ st.session_state.costs = costs
65
+ clear_widget_keys()
66
+ st.session_state.pop("latest_result", None)
67
+ st.session_state.pop("scenario_loaded_file", None)
68
+ st.session_state.pop("scenario_name_suggestion", None)
69
+ st.session_state.pop("resource_last_loaded_file", None)
70
+ st.session_state["excel_loaded_file"] = uploaded.name
71
+ st.session_state["input_file_name"] = default_name
72
+ st.session_state["scenario_uploader_key"] += 1 # force reload in scenario uploader
73
+ st.rerun()
74
+ except Exception as e:
75
+ st.error(f"Error loading file: {e}")
76
+
77
+ def parse_zip_scenario(uploaded) -> dict | None:
78
+ """Reconstruct a scenario dict from a ZIP containing the 4 CSVs."""
79
+ try:
80
+ with zipfile.ZipFile(io.BytesIO(uploaded.read())) as zf:
81
+ names = zf.namelist()
82
+
83
+ def _read(fname):
84
+ return pd.read_csv(io.BytesIO(zf.read(fname)), index_col=0)
85
+
86
+ required = {"resource_caps.csv", "method_constraints.csv",
87
+ "allocations.csv", "method_costs.csv"}
88
+ if not required.issubset(names):
89
+ return None
90
+
91
+ resource_caps = _read("resource_caps.csv")["Available Amount"].to_dict()
92
+
93
+ df_c = _read("method_constraints.csv")
94
+ constraints = {
95
+ m: {"active": bool(row["active"]), "max_share": float(row["max_share"])}
96
+ for m, row in df_c.iterrows()
97
+ }
98
+
99
+ df_costs = _read("method_costs.csv") # index=method, columns=resources
100
+ costs = {m: row.dropna().to_dict() for m, row in df_costs.iterrows()}
101
+
102
+ allocations = _read("allocations.csv")["tCOβ‚‚ removed"].to_dict()
103
+ total = sum(allocations.values())
104
+
105
+ return {
106
+ "summary": {"total_co2_removed": total, "total_cost": 0},
107
+ "allocations": allocations,
108
+ "resource_caps": resource_caps,
109
+ "method_constraints": constraints,
110
+ "method_costs": costs,
111
+ }
112
+ except Exception:
113
+ return None
114
+
115
+ def panel_load_from_json_or_csv():
116
+ """Right column: reload a full scenario from JSON or ZIP."""
117
+ st.caption("Accepts `.json` or `.zip` (CSV export)")
118
+
119
+ if "scenario_loaded_file" in st.session_state:
120
+ st.success(
121
+ f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated - Optimization results displayed.",
122
+ icon="βœ…"
123
+ )
124
+ uploaded_json = st.file_uploader(
125
+ "Upload scenario (.json or .zip)", type=["json", "zip"], key=f"scenario_loader_{st.session_state.scenario_uploader_key}"
126
+ )
127
+
128
+ if uploaded_json is None:
129
  return
130
+
131
+ if uploaded_json.name.endswith(".json"):
132
+ try:
133
+ data = json.load(uploaded_json)
134
+ except Exception as e:
135
+ st.error(f"Cannot read JSON: {e}")
136
+ return
137
+ if not is_valid_scenario(data):
138
+ st.error("Invalid scenario format.")
139
+ return
140
+ else:
141
+ data = parse_zip_scenario(uploaded_json)
142
+ if data is None:
143
+ st.error(
144
+ "Invalid ZIP. Expected: resource_caps.csv, method_constraints.csv, "
145
+ "method_costs.csv, allocations.csv"
146
+ )
147
+ return
148
+
149
+ default_name = uploaded_json.name.rsplit(".", 1)[0]
150
+
151
+ if st.button("↩️ Reload scenario and run optimization", use_container_width=True):
152
+ st.session_state.setdefault("scenario_name_suggestion", default_name)
153
+ st.session_state.pop("latest_result", None)
154
+ st.session_state.pop("excel_loaded_file", None)
155
+ st.session_state.pop("input_file_name_suggestion", None)
156
+ st.session_state.pop("resource_last_loaded_file", None)
157
+ st.session_state.resource_caps = data["resource_caps"].copy()
158
+ st.session_state.constraints = data["method_constraints"].copy()
159
+ st.session_state.costs = data["method_costs"].copy()
160
+ clear_widget_keys()
161
+ st.session_state["scenario_loaded_file"] = uploaded_json.name
162
+ st.session_state["scenario_name"] = default_name
163
+ st.session_state["latest_result"] = data
164
+ st.session_state["excel_uploader_key"] += 1 # force reload in excel uploader
165
+ st.rerun()
166
 
167
+ def clear_widget_keys():
168
+ prefixes = ("slider_", "manual_", "toggle_")
169
+ for k in [k for k in st.session_state if k.startswith(prefixes)]:
170
+ del st.session_state[k]
171
+
172
+
173
+ # ─── Optimization execution ──────────────────────────────────────────────────
174
+
175
+ def run_and_store_result():
176
+ if sum(st.session_state.resource_caps.values()) <= 0:
177
+ st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
178
+ return
179
+
180
+ success, result = run_optimization(
181
+ resource_caps=st.session_state.resource_caps,
182
+ method_constraints=st.session_state.constraints,
183
+ method_costs=st.session_state.costs,
184
+ )
185
+
186
+ if not success:
187
+ st.error("❌ Optimization failed")
188
+ if "message" in result:
189
+ st.error(result["message"])
190
+ return
191
+
192
+ allocations = result["method_usage"]
193
+ total_removed = result["total_removed"]
194
+
195
+ if total_removed <= 0:
196
+ st.warning("❌ Optimization removed 0 tCOβ‚‚. Relax constraints or increase resources.")
197
+ return
198
+
199
+ total_cost = sum(
200
+ allocations[m] * sum(st.session_state.costs[m].values())
201
+ for m in allocations
202
+ )
203
+ st.session_state.latest_result = {
204
+ "summary": {"total_co2_removed": total_removed, "total_cost": total_cost},
205
+ "allocations": allocations,
206
+ "resource_caps": st.session_state.resource_caps.copy(),
207
+ "method_constraints": st.session_state.constraints,
208
+ "method_costs": st.session_state.costs,
209
+ }
210
+ st.success(f"βœ… {total_removed:,.0f} tCOβ‚‚ removed")
211
+
212
+
213
+ # ─── Results display ─────────────────────────────────────────────────────────
214
+
215
+ def show_metrics(latest):
216
  col1, col2 = st.columns(2)
217
  col1.metric("Total COβ‚‚ Removed", f"{latest['summary']['total_co2_removed']:,.0f} t")
218
+ col2.metric("Methods Used", len(latest["allocations"]))
219
 
220
+
221
+ def build_results_df(allocations):
222
+ df = pd.DataFrame.from_dict(allocations, orient="index", columns=["tCOβ‚‚ removed"])
223
+ df["tCOβ‚‚ removed"] = pd.to_numeric(df["tCOβ‚‚ removed"], errors="coerce")
224
+ total = df["tCOβ‚‚ removed"].sum()
225
  if total > 0:
226
+ df["Share (%)"] = (df["tCOβ‚‚ removed"] / total * 100).round(2)
227
+ return df.sort_values("tCOβ‚‚ removed", ascending=False)
 
228
 
 
 
 
 
229
 
230
+ def show_donut(df_result):
231
+ fig = go.Figure(data=[go.Pie(
232
+ labels=df_result.index.tolist(),
233
+ values=df_result["tCOβ‚‚ removed"].tolist(),
234
  hole=0.4,
235
  textinfo="label+percent",
236
  textposition="outside",
237
  marker=dict(line=dict(color="#000000", width=1)),
238
+ showlegend=False,
239
  )])
240
+ fig.update_layout(margin=dict(t=10, b=10))
241
+ st.plotly_chart(fig, use_container_width=True)
242
 
 
 
 
 
243
 
244
+ def show_heatmap(df_result, resource_caps, method_costs):
245
+ available = [r for r in resource_order if resource_caps.get(r, 0) > 0]
246
+ matrix, y_labels = [], []
247
  for method in df_result.index:
248
+ row = [
249
+ (method_costs.get(method, {}).get(r, 0.0) * df_result.at[method, "tCOβ‚‚ removed"]
250
+ / resource_caps[r] * 100) if resource_caps[r] > 0 else 0
251
+ for r in available
252
+ ]
253
+ matrix.append(row)
254
+ y_labels.append(method)
255
+
256
+ fig = go.Figure(data=go.Heatmap(
257
+ z=matrix, x=available, y=y_labels,
 
258
  colorscale="Blues",
259
  hovertemplate="Method: %{y}<br>Resource: %{x}<br>% Used: %{z:.2f}%<extra></extra>",
260
  ))
261
+ fig.update_layout(
262
  title="Resource Usage vs. Cap",
263
+ xaxis_title="Resource", yaxis_title="Method", height=400,
 
 
264
  )
265
+ st.plotly_chart(fig, use_container_width=True)
266
+
267
+
268
+ def show_absolute_usage(allocations, method_costs):
269
+ rows = [
270
+ {"Method": m, "Resource": r, "Used": method_costs[m][r] * co2, "Unit": unit}
271
+ for m, co2 in allocations.items()
272
+ for unit, resources in resource_units.items()
273
+ for r in resources
274
+ if r in method_costs.get(m, {}) and method_costs[m][r] * co2 > 0
275
+ ]
276
+ if not rows:
277
+ return
278
+
279
+ df = pd.DataFrame(rows)
280
+ for unit in df["Unit"].unique():
281
+ st.markdown(f"#### Resources in {unit}")
282
+ df_unit = df[df["Unit"] == unit]
283
+ fig, ax = plt.subplots(figsize=(8, 4 + 0.3 * df_unit["Resource"].nunique()))
284
+ sns.barplot(data=df_unit, x="Resource", y="Used", hue="Method", ax=ax)
285
+ ax.set_ylabel(f"Used ({unit})")
286
+ ax.tick_params(axis="x", rotation=45)
287
+ ax.legend(title="Method", bbox_to_anchor=(1.05, 1), loc="upper left")
288
+ st.pyplot(fig)
289
+
290
+
291
+ # ─── Save / export / reset ───────────────────────────────────────────────────────────
292
+
293
+ def save_scenario(latest):
 
 
 
 
294
  st.markdown("### Save scenario")
295
+ st.caption("Save the current scenario with inputs and outputs for comparison and/or export.")
296
+
297
+ suggested = st.session_state.get("scenario_loaded_file", "").rsplit(".", 1)[0]
298
+ current = st.session_state.get("scenario_name", "").strip()
299
+
300
+ col_name, col_btn = st.columns([3, 1])
301
+ with col_name:
302
+ st.text_input(
303
+ "Scenario name", key="scenario_name",
304
+ value=current or suggested,
305
+ label_visibility="collapsed",
306
+ placeholder="Enter a scenario name"
 
 
 
 
 
 
 
307
  )
308
+ with col_btn:
309
+ if st.button("Save", use_container_width=True, icon=":material/save:"):
310
+ name = st.session_state.get("scenario_name", "").strip()
311
+ if not name:
312
+ st.warning("Please enter a name.")
313
+ elif name in st.session_state.scenarios:
314
+ st.warning("Name already exists.")
315
+ else:
316
+ st.session_state.scenarios[name] = latest
317
+ st.success(f"Scenario '{name}' saved for comparison.")
318
+
319
+ def export_scenario():
320
+ name = st.session_state.get("scenario_name", "").strip()
321
+ if not name or name not in st.session_state.scenarios:
322
+ return
323
 
324
+ scenario = st.session_state.scenarios[name]
325
+ st.markdown(f"#### Scenario: {name}")
326
+ df_saved = build_results_df(scenario["allocations"])
327
+ st.table(format_results_summary(df_saved))
328
 
329
+ safe = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
330
+ col_json, col_csv = st.columns(2)
331
+
332
+ with col_json:
333
+ st.download_button(
334
+ "Export JSON", data=json.dumps(scenario, indent=2),
335
+ file_name=f"{safe}.json", mime="application/json",
336
+ use_container_width=True,
337
+ icon=":material/download:"
338
+ )
339
+
340
+ buf = io.BytesIO()
341
+ with zipfile.ZipFile(buf, "w") as zf:
342
+ zf.writestr("resource_caps.csv",
343
+ pd.DataFrame.from_dict(scenario["resource_caps"], orient="index",
344
+ columns=["Available Amount"]).to_csv())
345
+ zf.writestr("method_constraints.csv",
346
+ pd.DataFrame.from_dict(scenario["method_constraints"], orient="index",
347
+ columns=["active", "max_share"]).to_csv())
348
+ zf.writestr("method_costs.csv", # ← nouveau
349
+ pd.DataFrame.from_dict(scenario["method_costs"], orient="index").to_csv())
350
+ zf.writestr("allocations.csv",
351
+ pd.DataFrame.from_dict(scenario["allocations"], orient="index",
352
+ columns=["tCOβ‚‚ removed"]).to_csv())
353
+ with col_csv:
354
  st.download_button(
355
+ "Export CSV (zip)", data=buf.getvalue(),
356
+ file_name=f"{safe}.zip", mime="application/zip",
357
+ use_container_width=True,
358
+ icon=":material/download:"
359
+ )
360
+
361
+ def export_inputs_only():
362
+ st.markdown("### Export inputs")
363
+ st.caption("Export only the resource capabilities, method constraints and coefficients β€” without results.")
364
+
365
+ suggested = st.session_state.get("excel_loaded_file", "").rsplit(".", 1)[0]
366
+
367
+ col_text, col_button = st.columns([3,1])
368
+ with col_text:
369
+ st.text_input(
370
+ "File name", label_visibility="collapsed", key="input_file_name",
371
+ value=suggested,
372
+ placeholder="Enter a file name",
373
  )
374
 
375
+ wb = openpyxl.Workbook()
376
+ ws1 = wb.active
377
+ ws1.title = "resource_caps"
378
+ ws1.append(["Resource", "Available Amount"])
379
+ for r, v in st.session_state.resource_caps.items():
380
+ ws1.append([r, v])
381
+
382
+ ws2 = wb.create_sheet("constraints")
383
+ ws2.append(["method", "active", "max_share"])
384
+ for m, c in st.session_state.constraints.items():
385
+ ws2.append([m, c.get("active", True), c.get("max_share", 1.0)])
386
 
387
+ ws3 = wb.create_sheet("coefficients")
388
+ ws3.append(["method", "resource", "value", "secondary"])
389
+ for m, resources in st.session_state.costs.items():
390
+ for r, v in resources.items():
391
+ ws3.append([m, r, v, (m, r) in secondary_resources])
392
 
393
+ buf = io.BytesIO()
394
+ wb.save(buf)
395
+ buf.seek(0)
396
+
397
+ safe = st.session_state.get("input_file_name", "").strip() or suggested or "crra_inputs"
398
+ with col_button:
399
  st.download_button(
400
+ f"Export Excel",
401
+ data=buf,
402
+ file_name=f"{safe}.xlsx",
403
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
404
+ icon=":material/download:",
405
+ use_container_width=True
406
  )
407
+
408
+ def clear_all_data():
409
+ for key in ["resource_caps", "constraints", "costs", "scenarios",
410
+ "latest_result", "scenario_loaded_file", "scenario_name",
411
+ "excel_loaded_file", "input_file_name", "resource_last_loaded_file", "resource_uploader_key"]:
412
+ st.session_state.pop(key, None)
413
+
414
+ for uploader_key in ["resource_uploader_key", "excel_uploader_key", "scenario_uploader_key"]:
415
+ st.session_state[uploader_key] = st.session_state.get(uploader_key, 0) + 1
416
+
417
+ clear_widget_keys()
418
+ st.success("All data cleared.")
419
+ st.rerun()
420
+
421
+ @st.dialog("Confirm deletion")
422
+ def confirm_clear_all():
423
+ st.error("This action will delete all data from the current scenario(s).", icon=":material/warning:")
424
+ col1, col2 = st.columns([2, 1])
425
+ with col1:
426
+ if st.button("Yes, delete everything", type="primary", use_container_width=True, icon=":material/delete:"):
427
+ clear_all_data()
428
+ st.rerun()
429
+ with col2:
430
+ if st.button("Cancel",use_container_width=True):
431
+ st.rerun()
432
+
433
+ def reset_scenario():
434
+ st.divider()
435
+ st.markdown("### Reset")
436
+ st.caption("Clear all current inputs, results and saved scenarios from the session.")
437
+ if st.button("Clear all data", key="reset_scenario", use_container_width=False, type="primary", icon=":material/delete:"):
438
+ confirm_clear_all()
439
+
440
+ # ─── Main tab entry point ─────────────────────────────────────────────────────
441
+
442
+ def render_tab():
443
+ st.header("Portfolio generation")
444
+ st.info(
445
+ "Run the optimization tool, explore results and save scenarios for comparison or export."
446
+ )
447
+
448
+ # Session state defaults
449
+ for key, default in [
450
+ ("resource_caps", {}), ("constraints", {}),
451
+ ("costs", {}), ("scenarios", {}),
452
+ ("excel_uploader_key", 0),
453
+ ("scenario_uploader_key", 0)
454
+ ]:
455
+ st.session_state.setdefault(key, default)
456
+
457
+ # ── Two-column input area ──────────────────────────────────────────────
458
+ st.markdown("### Run optimization")
459
+ col_inputs, col_scenario = st.columns(2, gap="large")
460
+
461
+ with col_inputs:
462
+ st.markdown("#### From inputs")
463
+ run_clicked = st.button("▢️ Run Optimization", use_container_width=True)
464
+ panel_run_from_tabs()
465
+ panel_load_from_excel()
466
+
467
+ with col_scenario:
468
+ st.markdown("#### From a saved scenario")
469
+ panel_load_from_json_or_csv()
470
+
471
+ # ── Execute if button pressed ──────────────────────────────────────────
472
+ if run_clicked:
473
+ run_and_store_result()
474
+
475
+ if "latest_result" not in st.session_state:
476
+ return
477
+
478
+ latest = st.session_state.latest_result
479
+ allocations = latest["allocations"]
480
+ resource_caps = latest["resource_caps"]
481
+ method_costs = latest["method_costs"]
482
+
483
+ if not allocations or sum(allocations.values()) == 0:
484
+ return
485
+
486
+ # ── Results ────────────────────────────────────────────────────────────
487
+ show_metrics(latest)
488
+ df_result = build_results_df(allocations)
489
+
490
+ st.subheader("Optimization Summary")
491
+ st.table(format_results_summary(df_result))
492
+
493
+ st.markdown("### Method allocation breakdown")
494
+ show_donut(df_result)
495
+
496
+ st.markdown("### Resource usage heatmap (% of cap)")
497
+ show_heatmap(df_result, resource_caps, method_costs)
498
+
499
+ st.markdown("### Absolute resource usage by unit")
500
+ show_absolute_usage(allocations, method_costs)
501
+
502
+ # ── Save & export & reset ───────────────────────────────────────────────────────
503
+ save_scenario(latest)
504
+ export_scenario()
505
+ export_inputs_only()
506
+ reset_scenario()
tabs/tab6_scenarios.py CHANGED
@@ -5,125 +5,167 @@ import pandas as pd
5
  import seaborn as sns
6
  import streamlit as st
7
  from utils.data_utils import is_valid_scenario
 
8
 
9
- def render_tab():
10
- st.header("Scenario Comparison")
11
 
12
- # Load scenario from JSON
13
- uploaded_file = st.file_uploader("Load scenario(s) from JSON", type="json")
14
- if uploaded_file is not None:
15
- try:
16
- data = json.load(uploaded_file)
17
- if isinstance(data, dict) and all(is_valid_scenario(s) for s in data.values()):
18
- st.session_state.scenarios.update(data)
19
- st.success("Scenarios loaded successfully.")
20
- else:
21
- st.error("Invalid file format.")
22
- except Exception as e:
23
- st.error(f"Error loading file: {e}")
24
-
25
- if "scenarios" not in st.session_state:
26
- st.session_state.scenarios = {}
27
-
28
- if not st.session_state.scenarios:
29
- st.info("No scenarios have been saved yet.")
30
- return
31
 
32
- # Select scenarios from current workspace
33
- st.markdown("### Select Scenarios to Compare")
34
- scenario_names = list(st.session_state.scenarios.keys())
35
- selected = st.multiselect("Select scenarios", scenario_names, default=scenario_names)
36
 
37
- if not selected:
38
- st.warning("Please select at least one scenario to display the comparison.")
39
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
 
41
- # Remove buttons
42
- for name in selected:
43
- col1, col2 = st.columns([10, 1])
44
- with col1:
45
- st.markdown(f"**{name}**")
46
- with col2:
47
- if st.button("❌", key=f"remove_{name}"):
48
- st.session_state.scenarios.pop(name, None)
49
- st.rerun()
50
-
51
- # Prepare data
52
- all_methods, all_resources = set(), set()
53
  for name in selected:
54
  scenario = st.session_state.scenarios[name]
55
- all_methods.update(scenario.get("allocations", {}).keys())
56
- all_resources.update(scenario.get("resource_caps", {}).keys())
57
- all_methods, all_resources = sorted(all_methods), sorted(all_resources)
58
-
59
- summary_rows, method_alloc_df, resource_usage_df = [], [], []
60
- for name in selected:
61
- data = st.session_state.scenarios[name]
62
- allocations = data.get("allocations", {})
63
- resource_caps = data.get("resource_caps", {})
64
- constraints = data.get("method_constraints", {})
65
 
66
  row = {
67
  "Scenario": name,
68
- "COβ‚‚ Removed (t)": data.get("summary", {}).get("total_co2_removed", 0),
69
  }
70
 
71
  for method in all_methods:
72
  val = allocations.get(method, 0)
73
  row[f"{method} (tCOβ‚‚)"] = val
74
- method_alloc_df.append({"Scenario": name, "Method": method, "tCOβ‚‚": val})
75
 
76
  for resource in all_resources:
77
  total_used = 0
78
  for method, tco2 in allocations.items():
79
- coeffs = constraints.get(method, {}).get("resources", {})
80
- if not isinstance(coeffs, dict):
81
- continue
82
- unit_use = coeffs.get(resource, 0)
83
- used = unit_use * tco2
84
  total_used += used
85
  if used > 0:
86
- resource_usage_df.append({"Scenario": name, "Method": method, "Resource": resource, "Used": used})
 
 
 
87
  row[f"{resource} used"] = total_used
88
 
89
  summary_rows.append(row)
90
 
91
- # Comparison table
92
- df_summary = pd.DataFrame(summary_rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  st.subheader("Summary Table")
94
  st.dataframe(df_summary.style.format(na_rep="-", thousands=","), use_container_width=True)
95
- st.download_button("Download Comparison CSV", df_summary.to_csv(index=False).encode("utf-8"), "scenario_comparison.csv", "text/csv")
96
-
97
- # Charts
98
- if method_alloc_df:
99
- st.subheader("Method Allocations")
100
- df1 = pd.DataFrame(method_alloc_df)
101
- fig1, ax1 = plt.subplots(figsize=(10, 4))
102
- sns.barplot(data=df1, x="Method", y="tCOβ‚‚", hue="Scenario", ax=ax1)
103
- ax1.set_xlabel("Method")
104
- ax1.set_ylabel("tCOβ‚‚")
105
- plt.xticks(rotation=45, ha="right")
106
- st.pyplot(fig1)
107
-
108
- if resource_usage_df:
109
- st.subheader("Resource Use per Method")
110
- df2 = pd.DataFrame(resource_usage_df)
111
- methods = df2["Method"].unique()
112
- for method in methods:
113
- df_method = df2[df2["Method"] == method]
114
- fig2, ax2 = plt.subplots(figsize=(8, 3))
115
- sns.barplot(data=df_method, x="Resource", y="Used", hue="Scenario", ax=ax2)
116
- ax2.set_title(method)
117
- ax2.set_xlabel("Resource")
118
- ax2.set_ylabel("Used")
119
- plt.xticks(rotation=45, ha="right")
120
- st.pyplot(fig2)
121
-
122
- st.subheader("Total Resource Use per Scenario")
123
- df_total = df2.groupby(["Scenario", "Resource"]).sum(numeric_only=True).reset_index()
124
- fig3, ax3 = plt.subplots(figsize=(10, 4))
125
- sns.barplot(data=df_total, x="Scenario", y="Used", hue="Resource", ax=ax3)
126
- ax3.set_xlabel("Scenario")
127
- ax3.set_ylabel("Total Used")
128
- plt.xticks(rotation=45, ha="right")
129
- st.pyplot(fig3)
 
5
  import seaborn as sns
6
  import streamlit as st
7
  from utils.data_utils import is_valid_scenario
8
+ from tabs.tab4_optimization import parse_zip_scenario
9
 
 
 
10
 
11
+ # ─── Load from file ───────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ def panel_load_scenario():
14
+ st.markdown("### Load a scenario from file")
15
+ st.caption("Accepts `.json` or `.zip` (CSV export)")
 
16
 
17
+ uploaded = st.file_uploader("Upload scenario", type=["json", "zip"], key="comparison_loader", accept_multiple_files=True)
18
+ if uploaded is None:
19
  return
20
+ for uploaded_file in uploaded:
21
+ if uploaded_file is not None:
22
+ try:
23
+ if uploaded_file.name.endswith(".json"):
24
+ data = json.load(uploaded_file)
25
+ if not is_valid_scenario(data):
26
+ st.error(f"Invalid scenario format in {uploaded_file.name}.")
27
+ continue
28
+ else:
29
+ data = parse_zip_scenario(uploaded_file)
30
+ if data is None:
31
+ st.error(f"Invalid ZIP format in {uploaded_file.name}. Expected: resource_caps.csv, method_constraints.csv, method_costs.csv, allocations.csv")
32
+ continue
33
+ st.session_state.scenarios[uploaded_file.name] = data
34
+ except Exception as e:
35
+ st.error(f"Error loading {uploaded_file.name}: {e}")
36
+
37
+ # ─── Data preparation ─────────────────────────────────────────────────────────
38
+
39
+ def _prepare_comparison_data(selected):
40
+ all_methods = sorted({
41
+ m for name in selected
42
+ for m in st.session_state.scenarios[name].get("allocations", {})
43
+ })
44
+ all_resources = sorted({
45
+ r for name in selected
46
+ for r in st.session_state.scenarios[name].get("resource_caps", {})
47
+ })
48
+
49
+ summary_rows, method_alloc_rows, resource_usage_rows = [], [], []
50
 
 
 
 
 
 
 
 
 
 
 
 
 
51
  for name in selected:
52
  scenario = st.session_state.scenarios[name]
53
+ allocations = scenario.get("allocations", {})
54
+ method_costs = scenario.get("method_costs", {}) # ← fix: was using method_constraints
 
 
 
 
 
 
 
 
55
 
56
  row = {
57
  "Scenario": name,
58
+ "COβ‚‚ Removed (t)": scenario.get("summary", {}).get("total_co2_removed", 0),
59
  }
60
 
61
  for method in all_methods:
62
  val = allocations.get(method, 0)
63
  row[f"{method} (tCOβ‚‚)"] = val
64
+ method_alloc_rows.append({"Scenario": name, "Method": method, "tCOβ‚‚": val})
65
 
66
  for resource in all_resources:
67
  total_used = 0
68
  for method, tco2 in allocations.items():
69
+ used = method_costs.get(method, {}).get(resource, 0) * tco2
 
 
 
 
70
  total_used += used
71
  if used > 0:
72
+ resource_usage_rows.append({
73
+ "Scenario": name, "Method": method,
74
+ "Resource": resource, "Used": used,
75
+ })
76
  row[f"{resource} used"] = total_used
77
 
78
  summary_rows.append(row)
79
 
80
+ return (
81
+ pd.DataFrame(summary_rows),
82
+ pd.DataFrame(method_alloc_rows),
83
+ pd.DataFrame(resource_usage_rows),
84
+ )
85
+
86
+
87
+ # ─── Charts ───────────────────────────────────────────────────────────────────
88
+
89
+ def _show_method_allocations(df):
90
+ if df.empty:
91
+ return
92
+ st.subheader("Method Allocations")
93
+ fig, ax = plt.subplots(figsize=(10, 4))
94
+ sns.barplot(data=df, x="Method", y="tCOβ‚‚", hue="Scenario", ax=ax)
95
+ ax.set_xlabel("Method")
96
+ ax.set_ylabel("tCOβ‚‚")
97
+ plt.xticks(rotation=45, ha="right")
98
+ st.pyplot(fig)
99
+
100
+
101
+ def _show_resource_usage(df):
102
+ if df.empty:
103
+ st.info("No resource usage data available (method_costs missing from scenarios?).")
104
+ return
105
+
106
+ st.subheader("Resource Use per Method")
107
+ for method in df["Method"].unique():
108
+ df_m = df[df["Method"] == method]
109
+ fig, ax = plt.subplots(figsize=(8, 3))
110
+ sns.barplot(data=df_m, x="Resource", y="Used", hue="Scenario", ax=ax)
111
+ ax.set_title(method)
112
+ ax.set_xlabel("Resource")
113
+ ax.set_ylabel("Used")
114
+ plt.xticks(rotation=45, ha="right")
115
+ st.pyplot(fig)
116
+
117
+ st.subheader("Total Resource Use per Scenario")
118
+ df_total = df.groupby(["Scenario", "Resource"]).sum(numeric_only=True).reset_index()
119
+ fig, ax = plt.subplots(figsize=(10, 4))
120
+ sns.barplot(data=df_total, x="Scenario", y="Used", hue="Resource", ax=ax)
121
+ ax.set_xlabel("Scenario")
122
+ ax.set_ylabel("Total Used")
123
+ plt.xticks(rotation=45, ha="right")
124
+ st.pyplot(fig)
125
+
126
+
127
+ # ─── Main tab entry point ─────────────────────────────────────────────────────
128
+
129
+ def render_tab():
130
+ st.header("Scenario Comparison")
131
+
132
+ st.session_state.setdefault("scenarios", {})
133
+
134
+ panel_load_scenario()
135
+
136
+ if not st.session_state.scenarios:
137
+ st.info("No scenarios saved yet. Run an optimization in tab 4 or load a file above.")
138
+ return
139
+
140
+ # Select & manage scenarios
141
+ st.markdown("### Select Scenarios to Compare")
142
+ scenario_names = list(st.session_state.scenarios.keys())
143
+ selected = st.multiselect("Select scenarios", scenario_names, default=scenario_names)
144
+
145
+ for name in selected:
146
+ col1, col2 = st.columns([10, 1])
147
+ with col1:
148
+ st.markdown(f"**{name}**")
149
+ with col2:
150
+ if st.button("❌", key=f"remove_{name}"):
151
+ st.session_state.scenarios.pop(name, None)
152
+ st.rerun()
153
+
154
+ if not selected:
155
+ st.warning("Please select at least one scenario.")
156
+ return
157
+
158
+ # Comparison
159
+ df_summary, df_methods, df_resources = _prepare_comparison_data(selected)
160
+
161
  st.subheader("Summary Table")
162
  st.dataframe(df_summary.style.format(na_rep="-", thousands=","), use_container_width=True)
163
+ st.download_button(
164
+ "Export Comparison CSV",
165
+ df_summary.to_csv(index=False).encode("utf-8"),
166
+ "scenario_comparison.csv", "text/csv",
167
+ icon=":material/download:"
168
+ )
169
+
170
+ _show_method_allocations(df_methods)
171
+ _show_resource_usage(df_resources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
utils/data_utils.py CHANGED
@@ -1,7 +1,10 @@
 
1
  import json
2
  import pandas as pd
3
  import streamlit as st
4
-
 
 
5
 
6
  @st.cache_data
7
  def load_slider_data():
@@ -42,4 +45,29 @@ def is_valid_scenario(scenario):
42
  and "method_costs" in scenario
43
  and "method_constraints" in scenario
44
  and "summary" in scenario
45
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
  import json
3
  import pandas as pd
4
  import streamlit as st
5
+ import openpyxl
6
+ from openpyxl.styles import PatternFill, Font
7
+ from config.config import resource_order, method_order, secondary_resources
8
 
9
  @st.cache_data
10
  def load_slider_data():
 
45
  and "method_costs" in scenario
46
  and "method_constraints" in scenario
47
  and "summary" in scenario
48
+ )
49
+
50
+ def load_inputs_from_excel(file):
51
+ xl = pd.read_excel(file, sheet_name=None)
52
+
53
+ resource_caps = {}
54
+ if "resource_caps" in xl:
55
+ for _, row in xl["resource_caps"].iterrows():
56
+ resource_caps[row["Resource"]] = float(row["Available Amount"])
57
+
58
+ constraints = {}
59
+ if "constraints" in xl:
60
+ for _, row in xl["constraints"].iterrows():
61
+ constraints[row["method"]] = {
62
+ "active": bool(row["active"]),
63
+ "max_share": float(row["max_share"])
64
+ }
65
+
66
+ costs = {}
67
+ if "coefficients" in xl:
68
+ for _, row in xl["coefficients"].iterrows():
69
+ m, r, v = row["method"], row["resource"], float(row["value"])
70
+ costs.setdefault(m, {})[r] = v
71
+
72
+ return resource_caps, constraints, costs
73
+