Spaces:
Sleeping
Sleeping
[FIX/FEAT] Move bottleneck analysis from tab4 to tab5, add chart interpretation captions to both tabs
Browse files- tabs/tab4_optimization.py +24 -47
- tabs/tab5_sensitivity.py +69 -3
tabs/tab4_optimization.py
CHANGED
|
@@ -827,6 +827,12 @@ def show_resource_balance(latest):
|
|
| 827 |
None
|
| 828 |
"""
|
| 829 |
st.markdown('<p class="crra-sub-heading">Resource balance: used vs available</p>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 830 |
resource_caps = latest["resource_caps"]
|
| 831 |
used_total = latest["resource_used_total"]
|
| 832 |
|
|
@@ -884,45 +890,6 @@ def show_resource_balance(latest):
|
|
| 884 |
st.dataframe(df, width='stretch', hide_index=True)
|
| 885 |
register_chart_data("Resource balance (used vs remaining)", df)
|
| 886 |
|
| 887 |
-
def show_bottleneck_analysis(latest):
|
| 888 |
-
"""Render the resource sensitivity table (CDR gain per +1 unit of each resource).
|
| 889 |
-
|
| 890 |
-
Args:
|
| 891 |
-
latest (dict): result dict with keys resource_actual_gain, custom_resources.
|
| 892 |
-
|
| 893 |
-
Returns:
|
| 894 |
-
None
|
| 895 |
-
"""
|
| 896 |
-
st.markdown('<p class="crra-sub-heading">Resource sensitivity</p>', unsafe_allow_html=True)
|
| 897 |
-
st.markdown(
|
| 898 |
-
'<p class="caption">'
|
| 899 |
-
'For every resource: actual CDR gain if you add exactly +1 unit, recomputed with all other constraints in place.'
|
| 900 |
-
'</p>',
|
| 901 |
-
unsafe_allow_html=True,
|
| 902 |
-
)
|
| 903 |
-
|
| 904 |
-
resource_to_group = get_resource_to_group()
|
| 905 |
-
custom_batch_lookup = {b["name"]: b for b in latest.get("custom_resources", [])}
|
| 906 |
-
|
| 907 |
-
actual_gains = latest.get("resource_actual_gain", {})
|
| 908 |
-
if actual_gains:
|
| 909 |
-
sp_rows = []
|
| 910 |
-
for r, gain in sorted(actual_gains.items(), key=lambda x: -x[1]):
|
| 911 |
-
if r in custom_batch_lookup:
|
| 912 |
-
unit = resource_unit_map.get(resource_to_group.get(custom_batch_lookup[r]["group"]), "?")
|
| 913 |
-
else:
|
| 914 |
-
unit = resource_unit_map.get(resource_to_group.get(r), "?")
|
| 915 |
-
sp_rows.append({
|
| 916 |
-
"Resource": r,
|
| 917 |
-
"Unit": unit,
|
| 918 |
-
f"CDR gain for +1 unit ({CO2_UNIT})": round(gain, 4),
|
| 919 |
-
})
|
| 920 |
-
df_sp = pd.DataFrame(sp_rows)
|
| 921 |
-
st.dataframe(df_sp, width='stretch', hide_index=True)
|
| 922 |
-
register_chart_data("Resource sensitivity (CDR gain per +1 unit)", df_sp)
|
| 923 |
-
else:
|
| 924 |
-
st.info("No resources to analyse.", icon=":material/info:")
|
| 925 |
-
|
| 926 |
def save_scenario(latest):
|
| 927 |
"""Render the save-scenario widget and persist the current result to session state.
|
| 928 |
|
|
@@ -1378,11 +1345,10 @@ def render_tab():
|
|
| 1378 |
]:
|
| 1379 |
st.session_state.setdefault(key, default)
|
| 1380 |
|
| 1381 |
-
tab_run, tab_results, tab_resources,
|
| 1382 |
"▶️ Run",
|
| 1383 |
"📊 Method allocation",
|
| 1384 |
"🌿 Resource usage",
|
| 1385 |
-
"🔍 Bottleneck analysis",
|
| 1386 |
"💾 Export & Save",
|
| 1387 |
"🔄 Reset"
|
| 1388 |
])
|
|
@@ -1419,7 +1385,7 @@ def render_tab():
|
|
| 1419 |
|
| 1420 |
latest = st.session_state.get("latest_result")
|
| 1421 |
if not latest:
|
| 1422 |
-
for tab in [tab_results, tab_resources,
|
| 1423 |
with tab:
|
| 1424 |
st.markdown(no_results, unsafe_allow_html=True)
|
| 1425 |
with tab_reset:
|
|
@@ -1431,7 +1397,7 @@ def render_tab():
|
|
| 1431 |
method_costs = latest["method_costs"]
|
| 1432 |
|
| 1433 |
if not allocations or sum(allocations.values()) == 0:
|
| 1434 |
-
for tab in [tab_results, tab_resources,
|
| 1435 |
with tab:
|
| 1436 |
st.markdown(no_results, unsafe_allow_html=True)
|
| 1437 |
with tab_reset:
|
|
@@ -1452,29 +1418,40 @@ def render_tab():
|
|
| 1452 |
st.markdown('<p class="crra-sub-heading">Optimization Summary</p>', unsafe_allow_html=True)
|
| 1453 |
st.dataframe(df_display, width='stretch')
|
| 1454 |
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1455 |
show_donut(df_result)
|
| 1456 |
|
| 1457 |
with tab_resources:
|
| 1458 |
st.markdown('<p class="crra-sub-heading">Resource usage heatmap (% of cap)</p>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1459 |
show_heatmap(df_result, resource_caps, method_costs,
|
| 1460 |
custom_resources=latest.get("custom_resources", []),
|
| 1461 |
resource_usage=latest.get("resource_usage"))
|
| 1462 |
st.markdown('<p class="crra-sub-heading">Absolute resource usage by unit</p>', unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1463 |
show_absolute_usage(allocations, method_costs,
|
| 1464 |
custom_resources=latest.get("custom_resources", []),
|
| 1465 |
resource_usage=latest.get("resource_usage"))
|
| 1466 |
show_resource_balance(latest)
|
| 1467 |
|
| 1468 |
-
with tab_bottleneck:
|
| 1469 |
-
show_bottleneck_analysis(latest)
|
| 1470 |
-
|
| 1471 |
with tab_export:
|
| 1472 |
render_export_tab(latest)
|
| 1473 |
|
| 1474 |
with tab_reset:
|
| 1475 |
reset_scenario("reset")
|
| 1476 |
|
| 1477 |
-
st.markdown("---")
|
| 1478 |
if st.button("Sensitivity analysis →", key="btn-nav-next-optimization", type="primary", help="Explore how resource or method changes affect your portfolio."):
|
| 1479 |
st.session_state["_navigate_to"] = "Sensitivity analysis"
|
| 1480 |
st.rerun()
|
|
|
|
| 827 |
None
|
| 828 |
"""
|
| 829 |
st.markdown('<p class="crra-sub-heading">Resource balance: used vs available</p>', unsafe_allow_html=True)
|
| 830 |
+
st.markdown(
|
| 831 |
+
'<p class="caption">Green bars show quantities consumed by the optimised portfolio; grey bars show the unused remainder. '
|
| 832 |
+
'<br>Resources with almost no grey bar are fully utilised and may be limiting further CDR. '
|
| 833 |
+
'<br>Resources with a large grey bar have spare capacity: either no method needs them or their demand is already saturated elsewhere.</p>',
|
| 834 |
+
unsafe_allow_html=True,
|
| 835 |
+
)
|
| 836 |
resource_caps = latest["resource_caps"]
|
| 837 |
used_total = latest["resource_used_total"]
|
| 838 |
|
|
|
|
| 890 |
st.dataframe(df, width='stretch', hide_index=True)
|
| 891 |
register_chart_data("Resource balance (used vs remaining)", df)
|
| 892 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 893 |
def save_scenario(latest):
|
| 894 |
"""Render the save-scenario widget and persist the current result to session state.
|
| 895 |
|
|
|
|
| 1345 |
]:
|
| 1346 |
st.session_state.setdefault(key, default)
|
| 1347 |
|
| 1348 |
+
tab_run, tab_results, tab_resources, tab_export, tab_reset = st.tabs([
|
| 1349 |
"▶️ Run",
|
| 1350 |
"📊 Method allocation",
|
| 1351 |
"🌿 Resource usage",
|
|
|
|
| 1352 |
"💾 Export & Save",
|
| 1353 |
"🔄 Reset"
|
| 1354 |
])
|
|
|
|
| 1385 |
|
| 1386 |
latest = st.session_state.get("latest_result")
|
| 1387 |
if not latest:
|
| 1388 |
+
for tab in [tab_results, tab_resources, tab_export]:
|
| 1389 |
with tab:
|
| 1390 |
st.markdown(no_results, unsafe_allow_html=True)
|
| 1391 |
with tab_reset:
|
|
|
|
| 1397 |
method_costs = latest["method_costs"]
|
| 1398 |
|
| 1399 |
if not allocations or sum(allocations.values()) == 0:
|
| 1400 |
+
for tab in [tab_results, tab_resources, tab_export]:
|
| 1401 |
with tab:
|
| 1402 |
st.markdown(no_results, unsafe_allow_html=True)
|
| 1403 |
with tab_reset:
|
|
|
|
| 1418 |
st.markdown('<p class="crra-sub-heading">Optimization Summary</p>', unsafe_allow_html=True)
|
| 1419 |
st.dataframe(df_display, width='stretch')
|
| 1420 |
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
| 1421 |
+
st.markdown(
|
| 1422 |
+
'<p class="caption">Share of total CO₂ removal contributed by each active method. '
|
| 1423 |
+
'Larger slices indicate methods that dominate the portfolio.</p>',
|
| 1424 |
+
unsafe_allow_html=True,
|
| 1425 |
+
)
|
| 1426 |
show_donut(df_result)
|
| 1427 |
|
| 1428 |
with tab_resources:
|
| 1429 |
st.markdown('<p class="crra-sub-heading">Resource usage heatmap (% of cap)</p>', unsafe_allow_html=True)
|
| 1430 |
+
st.markdown(
|
| 1431 |
+
'<p class="caption">Each cell shows what percentage of a resource\'s available cap is consumed by a given method.'
|
| 1432 |
+
'<br>Dark cells indicate near-full use: those resource–method pairs are the most constrained.</p>',
|
| 1433 |
+
unsafe_allow_html=True,
|
| 1434 |
+
)
|
| 1435 |
show_heatmap(df_result, resource_caps, method_costs,
|
| 1436 |
custom_resources=latest.get("custom_resources", []),
|
| 1437 |
resource_usage=latest.get("resource_usage"))
|
| 1438 |
st.markdown('<p class="crra-sub-heading">Absolute resource usage by unit</p>', unsafe_allow_html=True)
|
| 1439 |
+
st.markdown(
|
| 1440 |
+
'<p class="caption">Absolute quantities consumed per resource and per method, in their native units. '
|
| 1441 |
+
'Use this view to compare the scale of demand across resources with different units (e.g. ha, m³/yr, t/yr).</p>',
|
| 1442 |
+
unsafe_allow_html=True,
|
| 1443 |
+
)
|
| 1444 |
show_absolute_usage(allocations, method_costs,
|
| 1445 |
custom_resources=latest.get("custom_resources", []),
|
| 1446 |
resource_usage=latest.get("resource_usage"))
|
| 1447 |
show_resource_balance(latest)
|
| 1448 |
|
|
|
|
|
|
|
|
|
|
| 1449 |
with tab_export:
|
| 1450 |
render_export_tab(latest)
|
| 1451 |
|
| 1452 |
with tab_reset:
|
| 1453 |
reset_scenario("reset")
|
| 1454 |
|
|
|
|
| 1455 |
if st.button("Sensitivity analysis →", key="btn-nav-next-optimization", type="primary", help="Explore how resource or method changes affect your portfolio."):
|
| 1456 |
st.session_state["_navigate_to"] = "Sensitivity analysis"
|
| 1457 |
st.rerun()
|
tabs/tab5_sensitivity.py
CHANGED
|
@@ -2,8 +2,8 @@ import copy
|
|
| 2 |
import pandas as pd
|
| 3 |
import streamlit as st
|
| 4 |
import plotly.express as px
|
| 5 |
-
from config.config import CO2_UNIT, hidden_resource_names, method_order, resource_order
|
| 6 |
-
from utils.data_utils import (get_resource_to_unit, run_expanded_optimization,
|
| 7 |
is_other_method, get_other_method_variants, make_variant_name,
|
| 8 |
default_constraint)
|
| 9 |
from utils.ui_helpers import register_chart_data, show_bar
|
|
@@ -65,6 +65,13 @@ def panel_resource_sensitivity(baseline_removed, all_resources, resource_to_unit
|
|
| 65 |
'<p class="crra-sub-heading">Impact of resource availability</p>',
|
| 66 |
unsafe_allow_html=True,
|
| 67 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
selected_resources = st.multiselect(
|
| 69 |
"Select one or more resources:", all_resources, key="crra-select-resource"
|
| 70 |
)
|
|
@@ -144,6 +151,14 @@ def panel_method_sensitivity(baseline_removed):
|
|
| 144 |
'<p class="crra-sub-heading">Impact of max allocation per method</p>',
|
| 145 |
unsafe_allow_html=True,
|
| 146 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
# "Other"-tagged methods with custom batches attached are expanded into
|
| 149 |
# independent variants (e.g. "Enhanced weathering - Other mineral: Ganite")
|
|
@@ -249,6 +264,54 @@ def panel_method_sensitivity(baseline_removed):
|
|
| 249 |
register_chart_data("Sensitivity to method cap", df)
|
| 250 |
|
| 251 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
def render_tab():
|
| 253 |
"""Render the Sensitivity Analysis tab.
|
| 254 |
|
|
@@ -274,7 +337,7 @@ def render_tab():
|
|
| 274 |
|
| 275 |
baseline_removed, all_resources, resource_to_unit = compute_baseline()
|
| 276 |
|
| 277 |
-
tab1, tab2 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity"])
|
| 278 |
|
| 279 |
with tab1:
|
| 280 |
panel_resource_sensitivity(baseline_removed, all_resources, resource_to_unit)
|
|
@@ -282,6 +345,9 @@ def render_tab():
|
|
| 282 |
with tab2:
|
| 283 |
panel_method_sensitivity(baseline_removed)
|
| 284 |
|
|
|
|
|
|
|
|
|
|
| 285 |
if st.button("Scenario comparison →", key="btn-nav-next-sensitivity", type="primary", help="Compare multiple saved scenarios side by side."):
|
| 286 |
st.session_state["_navigate_to"] = "Scenario comparison"
|
| 287 |
st.rerun()
|
|
|
|
| 2 |
import pandas as pd
|
| 3 |
import streamlit as st
|
| 4 |
import plotly.express as px
|
| 5 |
+
from config.config import CO2_UNIT, hidden_resource_names, method_order, resource_order, resource_unit_map
|
| 6 |
+
from utils.data_utils import (get_resource_to_unit, get_resource_to_group, run_expanded_optimization,
|
| 7 |
is_other_method, get_other_method_variants, make_variant_name,
|
| 8 |
default_constraint)
|
| 9 |
from utils.ui_helpers import register_chart_data, show_bar
|
|
|
|
| 65 |
'<p class="crra-sub-heading">Impact of resource availability</p>',
|
| 66 |
unsafe_allow_html=True,
|
| 67 |
)
|
| 68 |
+
st.markdown(
|
| 69 |
+
'<p class="caption">Each line shows how total CO₂ removal changes as one resource\'s availability varies from 0% to 200% of its current cap. '
|
| 70 |
+
'<br>The range goes up to 200% so you can see both the impact of a reduction and the potential gains from doubling availability. '
|
| 71 |
+
'<br>A steep curve means that resource strongly drives portfolio performance: small increases can yield large gains. '
|
| 72 |
+
'<br>A flat line means the portfolio is limited by something else (another resource or a method cap).</p>',
|
| 73 |
+
unsafe_allow_html=True,
|
| 74 |
+
)
|
| 75 |
selected_resources = st.multiselect(
|
| 76 |
"Select one or more resources:", all_resources, key="crra-select-resource"
|
| 77 |
)
|
|
|
|
| 151 |
'<p class="crra-sub-heading">Impact of max allocation per method</p>',
|
| 152 |
unsafe_allow_html=True,
|
| 153 |
)
|
| 154 |
+
st.markdown(
|
| 155 |
+
'<p class="caption">Each line shows how total CO₂ removal changes as one method\'s maximum deployment cap varies from 0% to 100% of its current setting: '
|
| 156 |
+
'whether that cap was defined as a percentage of potential or as an absolute value in the Method constraints tab. '
|
| 157 |
+
'<br>The scale stops at 100% because the cap represents a ceiling you set: going beyond it would mean overriding your own constraint. '
|
| 158 |
+
'<br>A steep curve means the cap is a binding constraint: relaxing it would allow more removal. '
|
| 159 |
+
'<br>A flat line means the method is already limited by resource availability, not by its cap.</p>',
|
| 160 |
+
unsafe_allow_html=True,
|
| 161 |
+
)
|
| 162 |
|
| 163 |
# "Other"-tagged methods with custom batches attached are expanded into
|
| 164 |
# independent variants (e.g. "Enhanced weathering - Other mineral: Ganite")
|
|
|
|
| 264 |
register_chart_data("Sensitivity to method cap", df)
|
| 265 |
|
| 266 |
|
| 267 |
+
def show_bottleneck_analysis():
|
| 268 |
+
"""Render the resource sensitivity table (CDR gain per +1 unit of each resource).
|
| 269 |
+
|
| 270 |
+
Reads the latest optimization result from session state. Shows nothing if no
|
| 271 |
+
result is available yet.
|
| 272 |
+
|
| 273 |
+
Returns:
|
| 274 |
+
None
|
| 275 |
+
"""
|
| 276 |
+
latest = st.session_state.get("latest_result")
|
| 277 |
+
if not latest:
|
| 278 |
+
st.info("Run the optimization first (Portfolio generation tab) to see the bottleneck analysis.", icon=":material/info:")
|
| 279 |
+
return
|
| 280 |
+
|
| 281 |
+
st.markdown('<p class="crra-sub-heading">Resource sensitivity</p>', unsafe_allow_html=True)
|
| 282 |
+
st.markdown(
|
| 283 |
+
'<p class="caption">'
|
| 284 |
+
'For each resource: the additional CO₂ that could be removed by adding exactly +1 unit, '
|
| 285 |
+
'recomputed with all other constraints held fixed. '
|
| 286 |
+
'<br>Resources at the top of the list are the most binding bottlenecks in your current portfolio: '
|
| 287 |
+
'prioritise increasing their availability to unlock the largest gains.'
|
| 288 |
+
'</p>',
|
| 289 |
+
unsafe_allow_html=True,
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
resource_to_group = get_resource_to_group()
|
| 293 |
+
custom_batch_lookup = {b["name"]: b for b in latest.get("custom_resources", [])}
|
| 294 |
+
actual_gains = latest.get("resource_actual_gain", {})
|
| 295 |
+
|
| 296 |
+
if actual_gains:
|
| 297 |
+
sp_rows = []
|
| 298 |
+
for r, gain in sorted(actual_gains.items(), key=lambda x: -x[1]):
|
| 299 |
+
if r in custom_batch_lookup:
|
| 300 |
+
unit = resource_unit_map.get(resource_to_group.get(custom_batch_lookup[r]["group"]), "?")
|
| 301 |
+
else:
|
| 302 |
+
unit = resource_unit_map.get(resource_to_group.get(r), "?")
|
| 303 |
+
sp_rows.append({
|
| 304 |
+
"Resource": r,
|
| 305 |
+
"Unit": unit,
|
| 306 |
+
f"CDR gain for +1 unit ({CO2_UNIT})": round(gain, 4),
|
| 307 |
+
})
|
| 308 |
+
df_sp = pd.DataFrame(sp_rows)
|
| 309 |
+
st.dataframe(df_sp, width='stretch', hide_index=True)
|
| 310 |
+
register_chart_data("Resource sensitivity (CDR gain per +1 unit)", df_sp)
|
| 311 |
+
else:
|
| 312 |
+
st.info("No resources to analyse.", icon=":material/info:")
|
| 313 |
+
|
| 314 |
+
|
| 315 |
def render_tab():
|
| 316 |
"""Render the Sensitivity Analysis tab.
|
| 317 |
|
|
|
|
| 337 |
|
| 338 |
baseline_removed, all_resources, resource_to_unit = compute_baseline()
|
| 339 |
|
| 340 |
+
tab1, tab2, tab3 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity", "🔍 Bottleneck analysis"])
|
| 341 |
|
| 342 |
with tab1:
|
| 343 |
panel_resource_sensitivity(baseline_removed, all_resources, resource_to_unit)
|
|
|
|
| 345 |
with tab2:
|
| 346 |
panel_method_sensitivity(baseline_removed)
|
| 347 |
|
| 348 |
+
with tab3:
|
| 349 |
+
show_bottleneck_analysis()
|
| 350 |
+
|
| 351 |
if st.button("Scenario comparison →", key="btn-nav-next-sensitivity", type="primary", help="Compare multiple saved scenarios side by side."):
|
| 352 |
st.session_state["_navigate_to"] = "Scenario comparison"
|
| 353 |
st.rerun()
|