CarlyneB commited on
Commit
551ebbf
·
1 Parent(s): 4eda649

[FEAT/FIX] tab4: fix variant deactivation on ZIP reload, relocate helpers to utils, add persistent Reset tab

Browse files
Files changed (1) hide show
  1. tabs/tab4_optimization.py +69 -118
tabs/tab4_optimization.py CHANGED
@@ -10,31 +10,16 @@ import numpy as np
10
  import pandas as pd
11
  import streamlit as st
12
  import plotly.express as px
13
- from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
14
  from optimization.optimization import run_optimization
15
  from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
16
- from utils.data_utils import is_valid_scenario, is_complete_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix, get_resource_to_group, get_resource_to_unit, is_other_method, get_other_method_variants, make_variant_name
 
 
 
 
 
17
  from config.config import hidden_resource_names as _hidden
18
- from tabs.tab2_constraints import default_constraint as _dc
19
-
20
- def to_json_safe(obj):
21
- """Recursively convert numpy/pandas types to plain Python so json.dumps works."""
22
- if isinstance(obj, dict):
23
- return {k: to_json_safe(v) for k, v in obj.items()}
24
- if isinstance(obj, list):
25
- return [to_json_safe(v) for v in obj]
26
- if isinstance(obj, pd.Series):
27
- return obj.iloc[0] if len(obj) == 1 else obj.tolist()
28
- if isinstance(obj, pd.DataFrame):
29
- return obj.to_dict(orient="records")
30
- if isinstance(obj, np.integer):
31
- return int(obj)
32
- if isinstance(obj, np.floating):
33
- return float(obj)
34
- if isinstance(obj, np.ndarray):
35
- return obj.tolist()
36
- return obj
37
-
38
 
39
  def panel_run_from_tabs():
40
  """Left sub-panel: run optimization from manually filled tabs."""
@@ -50,19 +35,7 @@ def panel_load_from_excel():
50
  with col_markdown:
51
  st.markdown("**Option 2 - From an Excel file**")
52
 
53
- with col_upload:
54
- with open("data/inputs_template.xlsx", "rb") as f:
55
- st.download_button(
56
- label="Excel Template",
57
- data=f,
58
- file_name="crra_input_template.xlsx",
59
- mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
60
- width='stretch',
61
- icon=":material/download:",
62
- key="download_button_input_template"
63
- )
64
-
65
- st.markdown('<p class="caption">Fill in the template with your values, then import, load inputs and run.</p>', unsafe_allow_html=True)
66
 
67
  if "excel_loaded_file" in st.session_state:
68
  st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="✅")
@@ -214,13 +187,17 @@ def parse_zip_scenario(uploaded) -> dict | None:
214
  for _, row in df_enabled.iterrows():
215
  enabled_methods.setdefault(str(row["resource"]), {})[str(row["method"])] = bool(row["enabled"])
216
 
217
- safe_constraints = neutralize_zero_coefficient_methods(
218
- st.session_state.costs, st.session_state.constraints
219
- )
 
 
 
220
  neutralized = [
221
  m for m in safe_constraints
222
  if safe_constraints[m].get("active") is False
223
- and st.session_state.constraints.get(m, {}).get("active") is True
 
224
  ]
225
 
226
  # Recompute detailed outputs from the reloaded inputs so the scenario
@@ -313,7 +290,7 @@ def panel_load_from_json_or_csv():
313
  data["resource_actual_gain"] = result.get("resource_actual_gain", {})
314
  data.setdefault("enabled_methods", {})
315
  else:
316
- st.warning("Scenario loaded but detailed outputs could not be recomputed.")
317
  else:
318
  data = parse_zip_scenario(uploaded_json)
319
  if data is None:
@@ -333,7 +310,10 @@ def panel_load_from_json_or_csv():
333
  st.session_state.pop("resource_last_loaded_file", None)
334
  st.session_state.resource_caps = data["resource_caps"].copy()
335
  st.session_state.constraints = data["method_constraints"].copy()
336
- st.session_state.costs = data["method_costs"].copy()
 
 
 
337
  # Restore custom resource batches if present in the scenario
338
  st.session_state.custom_resources = data.get("custom_resources", [])
339
  st.session_state.enabled_methods = data.get("enabled_methods", {})
@@ -357,23 +337,9 @@ def panel_load_from_json_or_csv():
357
  st.session_state["excel_uploader_key"] += 1 # force reload in excel uploader
358
  if st.session_state.custom_resources:
359
  n = len(st.session_state.custom_resources)
360
- st.info(f"ℹ️ {n} new resource(s) restored from scenario.")
361
  st.rerun()
362
 
363
- def clear_widget_keys():
364
- """Delete all Streamlit widget state keys for sliders, manual inputs, and toggles.
365
-
366
- Called after loading a new file or scenario so widgets re-initialise from
367
- fresh session-state values instead of showing stale cached values.
368
-
369
- Returns:
370
- None
371
- """
372
- prefixes = ("toggle_", "cap_type_", "slider_", "manual_")
373
- for k in [k for k in st.session_state if k.startswith(prefixes)]:
374
- del st.session_state[k]
375
-
376
-
377
  def run_and_store_result():
378
  """Run the LP optimisation from current session-state inputs and store the result.
379
 
@@ -395,7 +361,7 @@ def run_and_store_result():
395
 
396
  custom_total = sum(float(b["amount"]) for b in custom_resources)
397
  if std_total <= 0 and custom_total <= 0:
398
- st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
399
  return
400
 
401
  # Apply enabled_methods overrides to a temporary copy — session state is NOT
@@ -445,15 +411,15 @@ def run_and_store_result():
445
  safe_constraints.setdefault(m, {"active": True, "cap_type": "percent", "cap_value": 100})
446
  safe_constraints[m] = {**safe_constraints[m], "active": False}
447
 
448
- neutralized = [
449
- m for m in safe_constraints
450
- if safe_constraints[m].get("active") is False
451
- and st.session_state.constraints.get(m, {}).get("active") is True
452
- ]
453
-
454
  # Expand "Other" methods: replace each with one variant per assigned batch.
455
  # Each variant uses the batch as its primary resource (standard LP constraint)
456
  # instead of the z-variable substitution mechanism.
 
 
 
 
 
 
457
  resource_caps_for_lp = dict(st.session_state.resource_caps)
458
  custom_resources_for_lp = []
459
  expanded_batch_names: set[str] = set()
@@ -492,6 +458,17 @@ def run_and_store_result():
492
  del applied_costs[m]
493
  del safe_constraints[m]
494
 
 
 
 
 
 
 
 
 
 
 
 
495
  # LP receives only non-expanded batches. Expanded batches are now standard
496
  # resources (in resource_caps_for_lp) so they get a type-A constraint — if
497
  # they stayed in custom_resources they'd land in custom_batch_names, be
@@ -550,7 +527,7 @@ def run_and_store_result():
550
  total_removed = result["total_removed"]
551
 
552
  if total_removed <= 0:
553
- st.warning(f"❌ Optimization removed 0 {CO2_UNIT}. Relax constraints or increase resources.")
554
  return
555
 
556
  total_cost = sum(
@@ -567,11 +544,12 @@ def run_and_store_result():
567
  "resource_caps": resource_caps_for_lp,
568
  "method_constraints": safe_constraints,
569
  "method_costs": applied_costs,
 
570
  "custom_resources": custom_resources_for_display,
571
  "enabled_methods": enabled_methods,
572
  }
573
  if custom_resources:
574
- st.info(f"ℹ️ {len(custom_resources)} new resource(s) included in optimisation.")
575
 
576
  # Warn when an active method has a primary resource whose coefficient is so
577
  # small (< 1e-7) that it is effectively unconstrained — the LP will run the
@@ -633,40 +611,6 @@ def show_metrics(latest):
633
  })
634
  st.dataframe(pd.DataFrame(rows), width='stretch', hide_index=True)
635
 
636
- def build_results_df(allocations, method_constraints=None):
637
- """Build a DataFrame of per-method CO₂ allocations with share and cap columns.
638
-
639
- Args:
640
- allocations (dict): {method: CO₂ removed (float)}.
641
- method_constraints (dict | None): {method: {cap_type, cap_value}} for the Cap column.
642
-
643
- Returns:
644
- pd.DataFrame: indexed by method, columns CO₂ removed, Total share (%), Cap.
645
- Sorted descending by CO₂ removed.
646
- """
647
- df = pd.DataFrame.from_dict(allocations, orient="index", columns=[f"{CO2_UNIT} removed"])
648
-
649
- df[f"{CO2_UNIT} removed"] = pd.to_numeric(df[f"{CO2_UNIT} removed"], errors="coerce")
650
- total = df[f"{CO2_UNIT} removed"].sum()
651
- if total > 0:
652
- df["Total share (%)"] = (df[f"{CO2_UNIT} removed"] / total * 100).round(2)
653
-
654
- if method_constraints:
655
- caps = {}
656
- for m in df.index:
657
- c = method_constraints.get(m, {})
658
-
659
- if c.get("cap_type") == "absolute":
660
- caps[m] = f"{c['cap_value']:,.2f} {CO2_UNIT}/yr"
661
- elif c.get("cap_type") == "percent":
662
- caps[m] = f"{int(c.get('cap_value', 100))}% of potential"
663
- else:
664
- caps[m] = f"⚠️ Invalid type ({c.get('cap_type')!r}) — fix in Tab 2"
665
- df["Cap"] = pd.Series(caps)
666
-
667
- return df.sort_values(f"{CO2_UNIT} removed", ascending=False)
668
-
669
-
670
  def show_donut(df_result):
671
  """Render a donut chart of CO₂ removal by method and register its data.
672
 
@@ -715,7 +659,7 @@ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None,
715
  available = standard_available + extra_available
716
 
717
  if not available:
718
- st.info("No resources with non-zero availability to display.")
719
  return
720
 
721
  matrix_pct, matrix_exact, y_labels = [], [], []
@@ -744,7 +688,7 @@ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None,
744
  df_heat, color_continuous_scale=PLOTLY_HEATMAP_SCALE, text_auto=".1f",
745
  labels=dict(x="Resource", y="Method", color="% Used"),
746
  )
747
- matrix_exact_fmt = [[f"{v:.15f}".rstrip("0").rstrip(".") for v in row] for row in matrix_exact]
748
  fig.update_traces(
749
  customdata=matrix_exact_fmt,
750
  hovertemplate="<b>%{y}</b><br>%{x}<br>% of cap: %{z:.1f}%<br>Exact: %{customdata}<extra></extra>",
@@ -956,7 +900,7 @@ def show_bottleneck_analysis(latest):
956
  st.dataframe(df_sp, width='stretch', hide_index=True)
957
  register_chart_data("Resource sensitivity (CDR gain per +1 unit)", df_sp)
958
  else:
959
- st.info("No resources to analyse.")
960
 
961
  def save_scenario(latest):
962
  """Render the save-scenario widget and persist the current result to session state.
@@ -990,9 +934,9 @@ def save_scenario(latest):
990
  if save_clicked:
991
  name = st.session_state.get("scenario_name", "").strip()
992
  if not name:
993
- st.warning("Please enter a name.")
994
  elif name in st.session_state.scenarios:
995
- st.warning("Name already exists.")
996
  else:
997
  st.session_state.scenarios[name] = latest
998
  st.success(f"Scenario '{name}' saved for comparison.")
@@ -1265,7 +1209,7 @@ def export_all_charts():
1265
 
1266
  registry = st.session_state.get("chart_data_registry", {})
1267
  if not registry:
1268
- st.info("No charts have been generated yet in this session.")
1269
  return
1270
 
1271
  base_name = st.text_input(
@@ -1352,17 +1296,16 @@ def confirm_clear_all():
1352
  if st.button("Cancel", width='stretch', type="secondary", key="btn-cancel"):
1353
  st.rerun()
1354
 
1355
- def reset_scenario():
1356
  """Render the reset section with a button that opens the confirm_clear_all dialog.
1357
 
1358
  Returns:
1359
  None
1360
  """
1361
- st.divider()
1362
  st.markdown('<p class="crra-sub-heading">Reset</p>', unsafe_allow_html=True)
1363
  st.markdown('<p class="caption">Clear all current inputs, results and saved scenarios from the session.</p>', unsafe_allow_html=True)
1364
 
1365
- if st.button("Clear all data", key="reset_scenario", type="primary", icon=":material/delete:"):
1366
  confirm_clear_all()
1367
 
1368
 
@@ -1373,7 +1316,7 @@ def render_export_tab(latest):
1373
  export_scenario()
1374
  export_inputs_only()
1375
  export_all_charts()
1376
- reset_scenario()
1377
 
1378
 
1379
  def render_tab():
@@ -1415,12 +1358,13 @@ def render_tab():
1415
  if "enabled_methods" not in st.session_state:
1416
  st.session_state.enabled_methods = {}
1417
 
1418
- tab_run, tab_results, tab_resources, tab_bottleneck, tab_export = st.tabs([
1419
  "▶️ Run",
1420
  "📊 Method allocation",
1421
  "🌿 Resource usage",
1422
  "🔍 Bottleneck analysis",
1423
  "💾 Export & Save",
 
1424
  ])
1425
 
1426
  with tab_run:
@@ -1451,13 +1395,15 @@ def render_tab():
1451
  with run_msg_area:
1452
  run_and_store_result()
1453
 
1454
- _no_results = '<p class="caption">Run the optimization first to see results here.</p>'
1455
 
1456
  latest = st.session_state.get("latest_result")
1457
  if not latest:
1458
- for _tab in [tab_results, tab_resources, tab_bottleneck, tab_export]:
1459
- with _tab:
1460
- st.markdown(_no_results, unsafe_allow_html=True)
 
 
1461
  return
1462
 
1463
  allocations = latest["allocations"]
@@ -1465,9 +1411,11 @@ def render_tab():
1465
  method_costs = latest["method_costs"]
1466
 
1467
  if not allocations or sum(allocations.values()) == 0:
1468
- for _tab in [tab_results, tab_resources, tab_bottleneck, tab_export]:
1469
- with _tab:
1470
- st.markdown(_no_results, unsafe_allow_html=True)
 
 
1471
  return
1472
 
1473
  df_result = build_results_df(
@@ -1501,4 +1449,7 @@ def render_tab():
1501
  show_bottleneck_analysis(latest)
1502
 
1503
  with tab_export:
1504
- render_export_tab(latest)
 
 
 
 
10
  import pandas as pd
11
  import streamlit as st
12
  import plotly.express as px
13
+ from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT, method_order
14
  from optimization.optimization import run_optimization
15
  from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
16
+ from utils.data_utils import (is_valid_scenario, is_complete_scenario, load_inputs_from_excel,
17
+ neutralize_zero_coefficient_methods, strip_unit_suffix,
18
+ get_resource_to_group, get_resource_to_unit,
19
+ is_other_method, get_other_method_variants, make_variant_name,
20
+ default_constraint as _dc, to_json_safe, build_results_df)
21
+ from utils.state_utils import clear_widget_keys
22
  from config.config import hidden_resource_names as _hidden
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
  def panel_run_from_tabs():
25
  """Left sub-panel: run optimization from manually filled tabs."""
 
35
  with col_markdown:
36
  st.markdown("**Option 2 - From an Excel file**")
37
 
38
+ st.markdown('<p class="caption">Upload an Excel file previously exported from the tool to reload all inputs at once.</p>', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  if "excel_loaded_file" in st.session_state:
41
  st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="✅")
 
187
  for _, row in df_enabled.iterrows():
188
  enabled_methods.setdefault(str(row["resource"]), {})[str(row["method"])] = bool(row["enabled"])
189
 
190
+ # Use the loaded costs/constraints, not the old session state.
191
+ # Old state may contain variant entries (e.g. "Method: Batch") from a
192
+ # previous scenario; using them here causes false deactivation.
193
+ # Guard: only report standard methods so variants never appear.
194
+ _std = set(method_order)
195
+ safe_constraints = neutralize_zero_coefficient_methods(costs, constraints)
196
  neutralized = [
197
  m for m in safe_constraints
198
  if safe_constraints[m].get("active") is False
199
+ and constraints.get(m, {}).get("active") is True
200
+ and m in _std
201
  ]
202
 
203
  # Recompute detailed outputs from the reloaded inputs so the scenario
 
290
  data["resource_actual_gain"] = result.get("resource_actual_gain", {})
291
  data.setdefault("enabled_methods", {})
292
  else:
293
+ st.warning("Scenario loaded but detailed outputs could not be recomputed.", icon=":material/warning:")
294
  else:
295
  data = parse_zip_scenario(uploaded_json)
296
  if data is None:
 
310
  st.session_state.pop("resource_last_loaded_file", None)
311
  st.session_state.resource_caps = data["resource_caps"].copy()
312
  st.session_state.constraints = data["method_constraints"].copy()
313
+ # Use pre-expansion costs (has originals, no variants) so the next run
314
+ # can expand "Other" methods correctly without skipping the resource check.
315
+ # Fall back to method_costs for scenarios saved before this fix.
316
+ st.session_state.costs = data.get("method_costs_base", data["method_costs"]).copy()
317
  # Restore custom resource batches if present in the scenario
318
  st.session_state.custom_resources = data.get("custom_resources", [])
319
  st.session_state.enabled_methods = data.get("enabled_methods", {})
 
337
  st.session_state["excel_uploader_key"] += 1 # force reload in excel uploader
338
  if st.session_state.custom_resources:
339
  n = len(st.session_state.custom_resources)
340
+ st.info(f"{n} new resource(s) restored from scenario.", icon=":material/info:")
341
  st.rerun()
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  def run_and_store_result():
344
  """Run the LP optimisation from current session-state inputs and store the result.
345
 
 
361
 
362
  custom_total = sum(float(b["amount"]) for b in custom_resources)
363
  if std_total <= 0 and custom_total <= 0:
364
+ st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.", icon=":material/warning:")
365
  return
366
 
367
  # Apply enabled_methods overrides to a temporary copy — session state is NOT
 
411
  safe_constraints.setdefault(m, {"active": True, "cap_type": "percent", "cap_value": 100})
412
  safe_constraints[m] = {**safe_constraints[m], "active": False}
413
 
 
 
 
 
 
 
414
  # Expand "Other" methods: replace each with one variant per assigned batch.
415
  # Each variant uses the batch as its primary resource (standard LP constraint)
416
  # instead of the z-variable substitution mechanism.
417
+ #
418
+ # Snapshot taken before expansion: originals are present, variants absent.
419
+ # Stored in the result so scenario reload can restore the original method costs
420
+ # (post-expansion applied_costs deletes originals and adds variants, which would
421
+ # cause the variants to bypass the missing-resources check on the next load).
422
+ method_costs_base = {m: dict(v) for m, v in applied_costs.items()}
423
  resource_caps_for_lp = dict(st.session_state.resource_caps)
424
  custom_resources_for_lp = []
425
  expanded_batch_names: set[str] = set()
 
458
  del applied_costs[m]
459
  del safe_constraints[m]
460
 
461
+ # Compute after expansion so "Other" methods (deleted above) don't appear.
462
+ # Guard: only include standard methods so dynamically created variants (not in
463
+ # method_order) never show up in this message even in edge cases.
464
+ _standard_methods = set(method_order)
465
+ neutralized = [
466
+ m for m in safe_constraints
467
+ if safe_constraints[m].get("active") is False
468
+ and st.session_state.constraints.get(m, {}).get("active") is True
469
+ and m in _standard_methods
470
+ ]
471
+
472
  # LP receives only non-expanded batches. Expanded batches are now standard
473
  # resources (in resource_caps_for_lp) so they get a type-A constraint — if
474
  # they stayed in custom_resources they'd land in custom_batch_names, be
 
527
  total_removed = result["total_removed"]
528
 
529
  if total_removed <= 0:
530
+ st.warning(f"❌ Optimization removed 0 {CO2_UNIT}. Relax constraints or increase resources.", icon=":material/warning:")
531
  return
532
 
533
  total_cost = sum(
 
544
  "resource_caps": resource_caps_for_lp,
545
  "method_constraints": safe_constraints,
546
  "method_costs": applied_costs,
547
+ "method_costs_base": method_costs_base,
548
  "custom_resources": custom_resources_for_display,
549
  "enabled_methods": enabled_methods,
550
  }
551
  if custom_resources:
552
+ st.info(f"{len(custom_resources)} new resource(s) included in optimisation.", icon=":material/info:")
553
 
554
  # Warn when an active method has a primary resource whose coefficient is so
555
  # small (< 1e-7) that it is effectively unconstrained — the LP will run the
 
611
  })
612
  st.dataframe(pd.DataFrame(rows), width='stretch', hide_index=True)
613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  def show_donut(df_result):
615
  """Render a donut chart of CO₂ removal by method and register its data.
616
 
 
659
  available = standard_available + extra_available
660
 
661
  if not available:
662
+ st.info("No resources with non-zero availability to display.", icon=":material/info:")
663
  return
664
 
665
  matrix_pct, matrix_exact, y_labels = [], [], []
 
688
  df_heat, color_continuous_scale=PLOTLY_HEATMAP_SCALE, text_auto=".1f",
689
  labels=dict(x="Resource", y="Method", color="% Used"),
690
  )
691
+ matrix_exact_fmt = [[f"{v:.10g}" for v in row] for row in matrix_exact]
692
  fig.update_traces(
693
  customdata=matrix_exact_fmt,
694
  hovertemplate="<b>%{y}</b><br>%{x}<br>% of cap: %{z:.1f}%<br>Exact: %{customdata}<extra></extra>",
 
900
  st.dataframe(df_sp, width='stretch', hide_index=True)
901
  register_chart_data("Resource sensitivity (CDR gain per +1 unit)", df_sp)
902
  else:
903
+ st.info("No resources to analyse.", icon=":material/info:")
904
 
905
  def save_scenario(latest):
906
  """Render the save-scenario widget and persist the current result to session state.
 
934
  if save_clicked:
935
  name = st.session_state.get("scenario_name", "").strip()
936
  if not name:
937
+ st.warning("Please enter a name.", icon=":material/warning:")
938
  elif name in st.session_state.scenarios:
939
+ st.warning("Name already exists.", icon=":material/warning:")
940
  else:
941
  st.session_state.scenarios[name] = latest
942
  st.success(f"Scenario '{name}' saved for comparison.")
 
1209
 
1210
  registry = st.session_state.get("chart_data_registry", {})
1211
  if not registry:
1212
+ st.info("No charts have been generated yet in this session.", icon=":material/info:")
1213
  return
1214
 
1215
  base_name = st.text_input(
 
1296
  if st.button("Cancel", width='stretch', type="secondary", key="btn-cancel"):
1297
  st.rerun()
1298
 
1299
+ def reset_scenario(tab_name):
1300
  """Render the reset section with a button that opens the confirm_clear_all dialog.
1301
 
1302
  Returns:
1303
  None
1304
  """
 
1305
  st.markdown('<p class="crra-sub-heading">Reset</p>', unsafe_allow_html=True)
1306
  st.markdown('<p class="caption">Clear all current inputs, results and saved scenarios from the session.</p>', unsafe_allow_html=True)
1307
 
1308
+ if st.button("Clear all data", key=f"reset_scenario_{tab_name}", type="primary", icon=":material/delete:"):
1309
  confirm_clear_all()
1310
 
1311
 
 
1316
  export_scenario()
1317
  export_inputs_only()
1318
  export_all_charts()
1319
+ reset_scenario("export")
1320
 
1321
 
1322
  def render_tab():
 
1358
  if "enabled_methods" not in st.session_state:
1359
  st.session_state.enabled_methods = {}
1360
 
1361
+ tab_run, tab_results, tab_resources, tab_bottleneck, tab_export, tab_reset = st.tabs([
1362
  "▶️ Run",
1363
  "📊 Method allocation",
1364
  "🌿 Resource usage",
1365
  "🔍 Bottleneck analysis",
1366
  "💾 Export & Save",
1367
+ "🔄 Reset"
1368
  ])
1369
 
1370
  with tab_run:
 
1395
  with run_msg_area:
1396
  run_and_store_result()
1397
 
1398
+ no_results = '<p class="caption">Run the optimization first to see results here.</p>'
1399
 
1400
  latest = st.session_state.get("latest_result")
1401
  if not latest:
1402
+ for tab in [tab_results, tab_resources, tab_bottleneck, tab_export]:
1403
+ with tab:
1404
+ st.markdown(no_results, unsafe_allow_html=True)
1405
+ with tab_reset:
1406
+ reset_scenario("reset")
1407
  return
1408
 
1409
  allocations = latest["allocations"]
 
1411
  method_costs = latest["method_costs"]
1412
 
1413
  if not allocations or sum(allocations.values()) == 0:
1414
+ for tab in [tab_results, tab_resources, tab_bottleneck, tab_export]:
1415
+ with tab:
1416
+ st.markdown(no_results, unsafe_allow_html=True)
1417
+ with tab_reset:
1418
+ reset_scenario("reset")
1419
  return
1420
 
1421
  df_result = build_results_df(
 
1449
  show_bottleneck_analysis(latest)
1450
 
1451
  with tab_export:
1452
+ render_export_tab(latest)
1453
+
1454
+ with tab_reset:
1455
+ reset_scenario("reset")