Spaces:
Sleeping
Sleeping
[FEAT/FIX] tab4: fix variant deactivation on ZIP reload, relocate helpers to utils, add persistent Reset tab
Browse files- 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,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 218 |
-
|
| 219 |
-
|
|
|
|
|
|
|
|
|
|
| 220 |
neutralized = [
|
| 221 |
m for m in safe_constraints
|
| 222 |
if safe_constraints[m].get("active") is False
|
| 223 |
-
and
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 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"
|
| 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"
|
| 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:.
|
| 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="
|
| 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 |
-
|
| 1455 |
|
| 1456 |
latest = st.session_state.get("latest_result")
|
| 1457 |
if not latest:
|
| 1458 |
-
for
|
| 1459 |
-
with
|
| 1460 |
-
st.markdown(
|
|
|
|
|
|
|
| 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
|
| 1469 |
-
with
|
| 1470 |
-
st.markdown(
|
|
|
|
|
|
|
| 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")
|