Spaces:
Sleeping
Sleeping
[FEAT/FIX] tab2: unify method activatability check, fix hidden resource coverage by batch group, move default_constraint to data_utils
Browse files- tabs/tab2_constraints.py +148 -101
tabs/tab2_constraints.py
CHANGED
|
@@ -3,17 +3,7 @@ import streamlit as st
|
|
| 3 |
|
| 4 |
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips, secondary_resources, hidden_resource_names
|
| 5 |
from utils.ui_helpers import show_bar
|
| 6 |
-
from utils.data_utils import strip_unit_suffix, is_other_method, get_other_method_variants, make_variant_name
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
def default_constraint():
|
| 11 |
-
"""Return the default constraint dict for a CDR method (active, 100% cap).
|
| 12 |
-
|
| 13 |
-
Returns:
|
| 14 |
-
dict: {"active": True, "cap_type": "percent", "cap_value": 100}.
|
| 15 |
-
"""
|
| 16 |
-
return {"active": True, "cap_type": "percent", "cap_value": int(100)}
|
| 17 |
|
| 18 |
|
| 19 |
def _make_slider_cb(m):
|
|
@@ -51,6 +41,37 @@ def get_hidden_resources_for_method(m, method_to_primary_resources, cost_sliders
|
|
| 51 |
return required, optional
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
def get_uncovered_hidden_resources(m, cost_sliders_norm):
|
| 55 |
"""Return (required, optional) hidden resources for method m that leave it unconstrained.
|
| 56 |
|
|
@@ -67,7 +88,13 @@ def get_uncovered_hidden_resources(m, cost_sliders_norm):
|
|
| 67 |
Returns ([], []) when nothing is uncovered.
|
| 68 |
"""
|
| 69 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
required, optional = [], []
|
| 73 |
for r, coeff_dict in cost_sliders_norm.get(m, {}).items():
|
|
@@ -75,7 +102,7 @@ def get_uncovered_hidden_resources(m, cost_sliders_norm):
|
|
| 75 |
continue
|
| 76 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 77 |
continue
|
| 78 |
-
if r in
|
| 79 |
continue
|
| 80 |
if (m, r) in secondary_resources:
|
| 81 |
optional.append(r)
|
|
@@ -93,7 +120,11 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 93 |
"""
|
| 94 |
resource_caps = st.session_state.get("resource_caps", {})
|
| 95 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 96 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
all_res = cost_sliders_norm.get(m, {})
|
| 99 |
required_rows = []
|
|
@@ -103,14 +134,18 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 103 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 104 |
continue
|
| 105 |
if r in hidden_resource_names:
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
| 107 |
source = "Add as new resource in Resource inputs"
|
| 108 |
else:
|
| 109 |
available = resource_caps.get(r, 0.0) > 0
|
|
|
|
| 110 |
source = "Set quantity in Resource inputs"
|
| 111 |
|
| 112 |
icon = "✅" if available else "❌"
|
| 113 |
-
row = (icon,
|
| 114 |
|
| 115 |
if (m, r) in secondary_resources:
|
| 116 |
optional_rows.append(row)
|
|
@@ -134,9 +169,10 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 134 |
if hint:
|
| 135 |
label += f' <span style="color:#888;font-style:italic;font-weight:400">: {hint}</span>'
|
| 136 |
st.markdown(label, unsafe_allow_html=True)
|
|
|
|
| 137 |
|
| 138 |
|
| 139 |
-
def render_method(m, cols, i,
|
| 140 |
"""Render the constraint widget group (toggle + cap slider/input) for one method.
|
| 141 |
|
| 142 |
Writes the result into st.session_state.constraints[m].
|
|
@@ -160,14 +196,13 @@ def render_method(m, cols, i, method_to_primary_resources, cost_sliders_norm):
|
|
| 160 |
toggle_key = f"toggle_{m}"
|
| 161 |
cap_type_key = f"cap_type_{m}"
|
| 162 |
|
| 163 |
-
#
|
| 164 |
-
#
|
| 165 |
-
#
|
| 166 |
-
#
|
| 167 |
-
#
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
if uncovered_required:
|
| 171 |
st.session_state.constraints[m]["active"] = False
|
| 172 |
st.session_state.pop(toggle_key, None)
|
| 173 |
st.toggle(m, value=False, key=toggle_key, disabled=True, help=tooltip)
|
|
@@ -196,7 +231,8 @@ def render_method(m, cols, i, method_to_primary_resources, cost_sliders_norm):
|
|
| 196 |
if constraint.get("_cap_type_invalid") or constraint.get("cap_type") not in ("percent", "absolute"):
|
| 197 |
st.warning(
|
| 198 |
f"Invalid **cap_type** value (`{constraint.get('cap_type')}`) loaded from file. "
|
| 199 |
-
"Select a constraint type below to fix it."
|
|
|
|
| 200 |
)
|
| 201 |
constraint["cap_type"] = "percent"
|
| 202 |
constraint["cap_value"] = 100
|
|
@@ -210,16 +246,6 @@ def render_method(m, cols, i, method_to_primary_resources, cost_sliders_norm):
|
|
| 210 |
# directly from the constraint — no stale session-state value possible.
|
| 211 |
st.session_state.setdefault(cap_type_key, constraint["cap_type"])
|
| 212 |
|
| 213 |
-
required, optional = get_hidden_resources_for_method(m, method_to_primary_resources, cost_sliders_norm)
|
| 214 |
-
if required is not None:
|
| 215 |
-
req_list = "\n".join(f"- **{r}** *(required)*" for r in required)
|
| 216 |
-
opt_list = "\n".join(f"- **{r}** *(optional)*" for r in optional) if optional else ""
|
| 217 |
-
body = f"To use this method, add a new resource in the **Resource inputs** tab:\n\n{req_list}"
|
| 218 |
-
if opt_list:
|
| 219 |
-
body += f"\n{opt_list}"
|
| 220 |
-
st.info(body, icon=":material/info:")
|
| 221 |
-
return
|
| 222 |
-
|
| 223 |
# Capture the cap_type that was active in the previous render so we can
|
| 224 |
# detect a mode switch (absolute → percent) below.
|
| 225 |
old_cap_type = constraint.get("cap_type", "percent")
|
|
@@ -248,7 +274,7 @@ def render_method(m, cols, i, method_to_primary_resources, cost_sliders_norm):
|
|
| 248 |
|
| 249 |
if cap_type == "absolute" and old_cap_type == "percent":
|
| 250 |
constraint["cap_value"] = 0.0
|
| 251 |
-
st.session_state
|
| 252 |
st.session_state.pop(f"slider_{m}", None)
|
| 253 |
|
| 254 |
if cap_type == "percent":
|
|
@@ -272,12 +298,13 @@ def render_method(m, cols, i, method_to_primary_resources, cost_sliders_norm):
|
|
| 272 |
|
| 273 |
else:
|
| 274 |
|
| 275 |
-
if f"manual_{m}" not in st.session_state:
|
| 276 |
-
st.session_state[f"manual_{m}"] = float(constraint["cap_value"])
|
| 277 |
|
| 278 |
value = st.number_input(
|
| 279 |
f"Max cap ({CO2_UNIT}/yr)",
|
| 280 |
min_value=0.0,
|
|
|
|
| 281 |
format="%.2f",
|
| 282 |
key=f"manual_{m}",
|
| 283 |
placeholder="0.00",
|
|
@@ -339,41 +366,27 @@ def build_summary_df():
|
|
| 339 |
|
| 340 |
|
| 341 |
|
| 342 |
-
def
|
| 343 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
Args:
|
| 346 |
-
|
| 347 |
|
| 348 |
Returns:
|
| 349 |
None
|
| 350 |
"""
|
| 351 |
-
cost_sliders_norm = {
|
| 352 |
-
m: {strip_unit_suffix(name): conf for name, conf in resources.items()}
|
| 353 |
-
for m, resources in cost_sliders.items()
|
| 354 |
-
}
|
| 355 |
-
method_to_primary_resources = {
|
| 356 |
-
m: [r for r in resources if (m, r) not in secondary_resources]
|
| 357 |
-
for m, resources in cost_sliders_norm.items()
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
# Pre-render sync: drive alzzl widget keys from the constraint dict before
|
| 361 |
-
# any widget is drawn.
|
| 362 |
-
#
|
| 363 |
-
# Two modes:
|
| 364 |
-
# force mode – after a file/scenario load (constraints_just_loaded flag)
|
| 365 |
-
# or for freshly activated methods (just_activated_methods).
|
| 366 |
-
# All keys are set unconditionally from the constraint.
|
| 367 |
-
# normal mode – setdefault only; never overrides a value the user already
|
| 368 |
-
# set by interacting with the widget this session.
|
| 369 |
just_loaded = st.session_state.pop("constraints_just_loaded", False)
|
| 370 |
just_activated = st.session_state.pop("just_activated_methods", set())
|
|
|
|
| 371 |
|
| 372 |
for m, c in st.session_state.constraints.items():
|
| 373 |
-
|
| 374 |
-
# skip the normal active-branch so the toggle key is never set to True.
|
| 375 |
-
uncov_req, _ = get_uncovered_hidden_resources(m, cost_sliders_norm)
|
| 376 |
-
if uncov_req:
|
| 377 |
st.session_state.constraints[m]["active"] = False
|
| 378 |
st.session_state.pop(f"toggle_{m}", None)
|
| 379 |
st.session_state.pop(f"cap_type_{m}", None)
|
|
@@ -381,6 +394,11 @@ def render_tab(cost_sliders):
|
|
| 381 |
st.session_state.pop(f"manual_{m}", None)
|
| 382 |
continue
|
| 383 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
force = just_loaded or (m in just_activated)
|
| 385 |
if c["active"]:
|
| 386 |
cap_type = c.get("cap_type", "percent")
|
|
@@ -391,7 +409,11 @@ def render_tab(cost_sliders):
|
|
| 391 |
st.session_state[f"slider_{m}"] = int(c.get("cap_value", 100))
|
| 392 |
st.session_state.pop(f"manual_{m}", None)
|
| 393 |
else:
|
| 394 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
st.session_state.pop(f"slider_{m}", None)
|
| 396 |
else:
|
| 397 |
st.session_state.setdefault(f"toggle_{m}", True)
|
|
@@ -400,7 +422,9 @@ def render_tab(cost_sliders):
|
|
| 400 |
st.session_state.setdefault(f"slider_{m}", int(c.get("cap_value", 100)))
|
| 401 |
st.session_state.pop(f"manual_{m}", None)
|
| 402 |
else:
|
| 403 |
-
|
|
|
|
|
|
|
| 404 |
st.session_state.pop(f"slider_{m}", None)
|
| 405 |
else:
|
| 406 |
if force:
|
|
@@ -411,6 +435,54 @@ def render_tab(cost_sliders):
|
|
| 411 |
st.session_state.pop(f"slider_{m}", None)
|
| 412 |
st.session_state.pop(f"manual_{m}", None)
|
| 413 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 414 |
st.markdown('<h1 class="crra-heading">Method constraints</h1>', unsafe_allow_html=True)
|
| 415 |
show_bar()
|
| 416 |
|
|
@@ -435,6 +507,11 @@ def render_tab(cost_sliders):
|
|
| 435 |
unsafe_allow_html=True,
|
| 436 |
)
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 439 |
|
| 440 |
# Build the full list of display names (variants replace "Other" methods with batches)
|
|
@@ -445,16 +522,11 @@ def render_tab(cost_sliders):
|
|
| 445 |
for display_m, _ in all_display
|
| 446 |
)
|
| 447 |
# Activatable: variants are always activatable; standard methods only if they
|
| 448 |
-
#
|
| 449 |
-
# hidden resource that would block them.
|
| 450 |
activatable = []
|
| 451 |
for display_m, orig_m in all_display:
|
| 452 |
-
if
|
| 453 |
activatable.append(display_m)
|
| 454 |
-
elif get_hidden_resources_for_method(orig_m, method_to_primary_resources, cost_sliders_norm)[0] is None:
|
| 455 |
-
uncov_req, _ = get_uncovered_hidden_resources(orig_m, cost_sliders_norm)
|
| 456 |
-
if not uncov_req:
|
| 457 |
-
activatable.append(orig_m)
|
| 458 |
|
| 459 |
if any_active:
|
| 460 |
if st.button("Deactivate all", icon=":material/remove_done:", key="btn-deactivate-all"):
|
|
@@ -482,35 +554,10 @@ def render_tab(cost_sliders):
|
|
| 482 |
if variants:
|
| 483 |
for batch in variants:
|
| 484 |
vname = make_variant_name(m, batch["name"])
|
| 485 |
-
render_method(vname, cols, col_idx,
|
| 486 |
col_idx += 1
|
| 487 |
else:
|
| 488 |
-
render_method(m, cols, col_idx,
|
| 489 |
col_idx += 1
|
| 490 |
|
| 491 |
-
|
| 492 |
-
if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
|
| 493 |
-
reset_all_constraints()
|
| 494 |
-
|
| 495 |
-
st.markdown("""
|
| 496 |
-
<p class="crra-sub-heading">Summary of Active methods and caps</p>""",
|
| 497 |
-
unsafe_allow_html=True,
|
| 498 |
-
)
|
| 499 |
-
df = build_summary_df()
|
| 500 |
-
if df is not None:
|
| 501 |
-
df_display = df.reset_index().rename(columns={"index": "CDR Method"})
|
| 502 |
-
df_display = df_display[["CDR Method", "Cap value"]]
|
| 503 |
-
num_rows = len(df_display)
|
| 504 |
-
st.dataframe(
|
| 505 |
-
df_display,
|
| 506 |
-
width='stretch',
|
| 507 |
-
hide_index=True,
|
| 508 |
-
height=(num_rows * 36 + 36),
|
| 509 |
-
column_config={
|
| 510 |
-
"CDR Method": st.column_config.TextColumn("CDR Method", width="large"),
|
| 511 |
-
"Cap value": st.column_config.TextColumn("Cap value"),
|
| 512 |
-
},
|
| 513 |
-
row_height=36
|
| 514 |
-
)
|
| 515 |
-
else:
|
| 516 |
-
st.warning("No active methods selected.")
|
|
|
|
| 3 |
|
| 4 |
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips, secondary_resources, hidden_resource_names
|
| 5 |
from utils.ui_helpers import show_bar
|
| 6 |
+
from utils.data_utils import strip_unit_suffix, is_other_method, get_other_method_variants, make_variant_name, default_constraint
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def _make_slider_cb(m):
|
|
|
|
| 41 |
return required, optional
|
| 42 |
|
| 43 |
|
| 44 |
+
def get_all_missing_required(m, cost_sliders_norm):
|
| 45 |
+
"""Return all required resources for method m that are not yet available.
|
| 46 |
+
|
| 47 |
+
Standard resource: available if resource_caps.get(r, 0.0) > 0.
|
| 48 |
+
Hidden resource: available if any custom batch with group == r is assigned to m.
|
| 49 |
+
Secondary resources and resources with median <= 0 are excluded.
|
| 50 |
+
|
| 51 |
+
Returns:
|
| 52 |
+
list[str]: missing required resource names (standard and hidden combined).
|
| 53 |
+
"""
|
| 54 |
+
resource_caps = st.session_state.get("resource_caps", {})
|
| 55 |
+
custom_resources = st.session_state.get("custom_resources", [])
|
| 56 |
+
covered_hidden = {
|
| 57 |
+
b["group"] for b in custom_resources
|
| 58 |
+
if m in b.get("methods", {})
|
| 59 |
+
}
|
| 60 |
+
missing = []
|
| 61 |
+
for r, coeff_dict in cost_sliders_norm.get(m, {}).items():
|
| 62 |
+
if (m, r) in secondary_resources:
|
| 63 |
+
continue
|
| 64 |
+
if coeff_dict.get("median", 0.0) <= 0:
|
| 65 |
+
continue
|
| 66 |
+
if r in hidden_resource_names:
|
| 67 |
+
if r not in covered_hidden:
|
| 68 |
+
missing.append(r)
|
| 69 |
+
else:
|
| 70 |
+
if resource_caps.get(r, 0.0) <= 0:
|
| 71 |
+
missing.append(r)
|
| 72 |
+
return missing
|
| 73 |
+
|
| 74 |
+
|
| 75 |
def get_uncovered_hidden_resources(m, cost_sliders_norm):
|
| 76 |
"""Return (required, optional) hidden resources for method m that leave it unconstrained.
|
| 77 |
|
|
|
|
| 88 |
Returns ([], []) when nothing is uncovered.
|
| 89 |
"""
|
| 90 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 91 |
+
# A hidden resource r (e.g. "Other mineral") is covered when any custom batch
|
| 92 |
+
# whose group == r is assigned to this method — the batch name differs from
|
| 93 |
+
# the group name (e.g. "Limestone" covers "Other mineral").
|
| 94 |
+
covered_groups = {
|
| 95 |
+
b["group"] for b in custom_resources
|
| 96 |
+
if m in b.get("methods", {})
|
| 97 |
+
}
|
| 98 |
|
| 99 |
required, optional = [], []
|
| 100 |
for r, coeff_dict in cost_sliders_norm.get(m, {}).items():
|
|
|
|
| 102 |
continue
|
| 103 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 104 |
continue
|
| 105 |
+
if r in covered_groups:
|
| 106 |
continue
|
| 107 |
if (m, r) in secondary_resources:
|
| 108 |
optional.append(r)
|
|
|
|
| 120 |
"""
|
| 121 |
resource_caps = st.session_state.get("resource_caps", {})
|
| 122 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 123 |
+
# Map hidden resource group → list of batch names covering it for method m.
|
| 124 |
+
covered_hidden: dict[str, list[str]] = {}
|
| 125 |
+
for b in custom_resources:
|
| 126 |
+
if m in b.get("methods", {}):
|
| 127 |
+
covered_hidden.setdefault(b["group"], []).append(b["name"])
|
| 128 |
|
| 129 |
all_res = cost_sliders_norm.get(m, {})
|
| 130 |
required_rows = []
|
|
|
|
| 134 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 135 |
continue
|
| 136 |
if r in hidden_resource_names:
|
| 137 |
+
batch_names = covered_hidden.get(r)
|
| 138 |
+
available = bool(batch_names)
|
| 139 |
+
# Show actual batch name(s) instead of the generic "Other X" group
|
| 140 |
+
display_r = ", ".join(batch_names) if batch_names else r
|
| 141 |
source = "Add as new resource in Resource inputs"
|
| 142 |
else:
|
| 143 |
available = resource_caps.get(r, 0.0) > 0
|
| 144 |
+
display_r = r
|
| 145 |
source = "Set quantity in Resource inputs"
|
| 146 |
|
| 147 |
icon = "✅" if available else "❌"
|
| 148 |
+
row = (icon, display_r, "" if available else source)
|
| 149 |
|
| 150 |
if (m, r) in secondary_resources:
|
| 151 |
optional_rows.append(row)
|
|
|
|
| 169 |
if hint:
|
| 170 |
label += f' <span style="color:#888;font-style:italic;font-weight:400">: {hint}</span>'
|
| 171 |
st.markdown(label, unsafe_allow_html=True)
|
| 172 |
+
st.caption("Optional resources can be added in the **Resource coefficients** tab (tab 3).")
|
| 173 |
|
| 174 |
|
| 175 |
+
def render_method(m, cols, i, cost_sliders_norm):
|
| 176 |
"""Render the constraint widget group (toggle + cap slider/input) for one method.
|
| 177 |
|
| 178 |
Writes the result into st.session_state.constraints[m].
|
|
|
|
| 196 |
toggle_key = f"toggle_{m}"
|
| 197 |
cap_type_key = f"cap_type_{m}"
|
| 198 |
|
| 199 |
+
# Block the method if any required resource is missing (standard at 0 or
|
| 200 |
+
# hidden resource not covered by a custom batch). Run this before the
|
| 201 |
+
# regular st.toggle so we never touch toggle_{m} for blocked methods —
|
| 202 |
+
# the pre-render sync already set it and Streamlit would raise
|
| 203 |
+
# "cannot be modified after widget is instantiated" otherwise.
|
| 204 |
+
missing_required = get_all_missing_required(m, cost_sliders_norm)
|
| 205 |
+
if missing_required:
|
|
|
|
| 206 |
st.session_state.constraints[m]["active"] = False
|
| 207 |
st.session_state.pop(toggle_key, None)
|
| 208 |
st.toggle(m, value=False, key=toggle_key, disabled=True, help=tooltip)
|
|
|
|
| 231 |
if constraint.get("_cap_type_invalid") or constraint.get("cap_type") not in ("percent", "absolute"):
|
| 232 |
st.warning(
|
| 233 |
f"Invalid **cap_type** value (`{constraint.get('cap_type')}`) loaded from file. "
|
| 234 |
+
"Select a constraint type below to fix it.",
|
| 235 |
+
icon=":material/warning:"
|
| 236 |
)
|
| 237 |
constraint["cap_type"] = "percent"
|
| 238 |
constraint["cap_value"] = 100
|
|
|
|
| 246 |
# directly from the constraint — no stale session-state value possible.
|
| 247 |
st.session_state.setdefault(cap_type_key, constraint["cap_type"])
|
| 248 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
# Capture the cap_type that was active in the previous render so we can
|
| 250 |
# detect a mode switch (absolute → percent) below.
|
| 251 |
old_cap_type = constraint.get("cap_type", "percent")
|
|
|
|
| 274 |
|
| 275 |
if cap_type == "absolute" and old_cap_type == "percent":
|
| 276 |
constraint["cap_value"] = 0.0
|
| 277 |
+
st.session_state.pop(f"manual_{m}", None)
|
| 278 |
st.session_state.pop(f"slider_{m}", None)
|
| 279 |
|
| 280 |
if cap_type == "percent":
|
|
|
|
| 298 |
|
| 299 |
else:
|
| 300 |
|
| 301 |
+
if f"manual_{m}" not in st.session_state and constraint.get("cap_value"):
|
| 302 |
+
st.session_state[f"manual_{m}"] = float(constraint["cap_value"])
|
| 303 |
|
| 304 |
value = st.number_input(
|
| 305 |
f"Max cap ({CO2_UNIT}/yr)",
|
| 306 |
min_value=0.0,
|
| 307 |
+
value=None,
|
| 308 |
format="%.2f",
|
| 309 |
key=f"manual_{m}",
|
| 310 |
placeholder="0.00",
|
|
|
|
| 366 |
|
| 367 |
|
| 368 |
|
| 369 |
+
def _sync_constraint_widgets(cost_sliders_norm):
|
| 370 |
+
"""Drive all constraint widget keys from the constraint dict before any widget is drawn.
|
| 371 |
+
|
| 372 |
+
Two modes:
|
| 373 |
+
- force mode: after a file/scenario load (constraints_just_loaded flag) or for
|
| 374 |
+
freshly activated methods (just_activated_methods). All keys are set
|
| 375 |
+
unconditionally from the constraint.
|
| 376 |
+
- normal mode: setdefault only; never overrides a value the user already set.
|
| 377 |
|
| 378 |
Args:
|
| 379 |
+
cost_sliders_norm (dict): {method: {resource: {min, median, max}}} with unit suffixes stripped.
|
| 380 |
|
| 381 |
Returns:
|
| 382 |
None
|
| 383 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 384 |
just_loaded = st.session_state.pop("constraints_just_loaded", False)
|
| 385 |
just_activated = st.session_state.pop("just_activated_methods", set())
|
| 386 |
+
auto_disabled = st.session_state.get("auto_disabled_methods", set())
|
| 387 |
|
| 388 |
for m, c in st.session_state.constraints.items():
|
| 389 |
+
if get_all_missing_required(m, cost_sliders_norm):
|
|
|
|
|
|
|
|
|
|
| 390 |
st.session_state.constraints[m]["active"] = False
|
| 391 |
st.session_state.pop(f"toggle_{m}", None)
|
| 392 |
st.session_state.pop(f"cap_type_{m}", None)
|
|
|
|
| 394 |
st.session_state.pop(f"manual_{m}", None)
|
| 395 |
continue
|
| 396 |
|
| 397 |
+
if m in auto_disabled:
|
| 398 |
+
st.session_state.constraints[m]["active"] = True
|
| 399 |
+
auto_disabled.discard(m)
|
| 400 |
+
just_activated.add(m)
|
| 401 |
+
|
| 402 |
force = just_loaded or (m in just_activated)
|
| 403 |
if c["active"]:
|
| 404 |
cap_type = c.get("cap_type", "percent")
|
|
|
|
| 409 |
st.session_state[f"slider_{m}"] = int(c.get("cap_value", 100))
|
| 410 |
st.session_state.pop(f"manual_{m}", None)
|
| 411 |
else:
|
| 412 |
+
cap_val = c.get("cap_value", 0.0)
|
| 413 |
+
if cap_val:
|
| 414 |
+
st.session_state[f"manual_{m}"] = float(cap_val)
|
| 415 |
+
else:
|
| 416 |
+
st.session_state.pop(f"manual_{m}", None)
|
| 417 |
st.session_state.pop(f"slider_{m}", None)
|
| 418 |
else:
|
| 419 |
st.session_state.setdefault(f"toggle_{m}", True)
|
|
|
|
| 422 |
st.session_state.setdefault(f"slider_{m}", int(c.get("cap_value", 100)))
|
| 423 |
st.session_state.pop(f"manual_{m}", None)
|
| 424 |
else:
|
| 425 |
+
cap_val = c.get("cap_value", 0.0)
|
| 426 |
+
if cap_val and f"manual_{m}" not in st.session_state:
|
| 427 |
+
st.session_state[f"manual_{m}"] = float(cap_val)
|
| 428 |
st.session_state.pop(f"slider_{m}", None)
|
| 429 |
else:
|
| 430 |
if force:
|
|
|
|
| 435 |
st.session_state.pop(f"slider_{m}", None)
|
| 436 |
st.session_state.pop(f"manual_{m}", None)
|
| 437 |
|
| 438 |
+
st.session_state["auto_disabled_methods"] = auto_disabled
|
| 439 |
+
|
| 440 |
+
|
| 441 |
+
def render_constraints_summary():
|
| 442 |
+
"""Render the summary dataframe of active methods and their caps.
|
| 443 |
+
|
| 444 |
+
Returns:
|
| 445 |
+
None
|
| 446 |
+
"""
|
| 447 |
+
st.markdown("""
|
| 448 |
+
<p class="crra-sub-heading">Summary of Active methods and caps</p>""",
|
| 449 |
+
unsafe_allow_html=True,
|
| 450 |
+
)
|
| 451 |
+
df = build_summary_df()
|
| 452 |
+
if df is not None:
|
| 453 |
+
df_display = df.reset_index().rename(columns={"index": "CDR Method"})
|
| 454 |
+
df_display = df_display[["CDR Method", "Cap value"]]
|
| 455 |
+
num_rows = len(df_display)
|
| 456 |
+
st.dataframe(
|
| 457 |
+
df_display,
|
| 458 |
+
width='stretch',
|
| 459 |
+
hide_index=True,
|
| 460 |
+
height=(num_rows * 36 + 36),
|
| 461 |
+
column_config={
|
| 462 |
+
"CDR Method": st.column_config.TextColumn("CDR Method", width="large"),
|
| 463 |
+
"Cap value": st.column_config.TextColumn("Cap value"),
|
| 464 |
+
},
|
| 465 |
+
row_height=36,
|
| 466 |
+
)
|
| 467 |
+
else:
|
| 468 |
+
st.warning("No active methods selected.", icon=":material/warning:")
|
| 469 |
+
|
| 470 |
+
|
| 471 |
+
def render_tab(cost_sliders):
|
| 472 |
+
"""Render the Method Constraints tab.
|
| 473 |
+
|
| 474 |
+
Args:
|
| 475 |
+
cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}.
|
| 476 |
+
|
| 477 |
+
Returns:
|
| 478 |
+
None
|
| 479 |
+
"""
|
| 480 |
+
cost_sliders_norm = {
|
| 481 |
+
m: {strip_unit_suffix(name): conf for name, conf in resources.items()}
|
| 482 |
+
for m, resources in cost_sliders.items()
|
| 483 |
+
}
|
| 484 |
+
_sync_constraint_widgets(cost_sliders_norm)
|
| 485 |
+
|
| 486 |
st.markdown('<h1 class="crra-heading">Method constraints</h1>', unsafe_allow_html=True)
|
| 487 |
show_bar()
|
| 488 |
|
|
|
|
| 507 |
unsafe_allow_html=True,
|
| 508 |
)
|
| 509 |
|
| 510 |
+
st.markdown('<p class="crra-sub-heading">Controls</p>', unsafe_allow_html=True)
|
| 511 |
+
|
| 512 |
+
if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
|
| 513 |
+
reset_all_constraints()
|
| 514 |
+
|
| 515 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 516 |
|
| 517 |
# Build the full list of display names (variants replace "Other" methods with batches)
|
|
|
|
| 522 |
for display_m, _ in all_display
|
| 523 |
)
|
| 524 |
# Activatable: variants are always activatable; standard methods only if they
|
| 525 |
+
# A method is activatable when all its required resources are available.
|
|
|
|
| 526 |
activatable = []
|
| 527 |
for display_m, orig_m in all_display:
|
| 528 |
+
if not get_all_missing_required(orig_m, cost_sliders_norm):
|
| 529 |
activatable.append(display_m)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 530 |
|
| 531 |
if any_active:
|
| 532 |
if st.button("Deactivate all", icon=":material/remove_done:", key="btn-deactivate-all"):
|
|
|
|
| 554 |
if variants:
|
| 555 |
for batch in variants:
|
| 556 |
vname = make_variant_name(m, batch["name"])
|
| 557 |
+
render_method(vname, cols, col_idx, cost_sliders_norm)
|
| 558 |
col_idx += 1
|
| 559 |
else:
|
| 560 |
+
render_method(m, cols, col_idx, cost_sliders_norm)
|
| 561 |
col_idx += 1
|
| 562 |
|
| 563 |
+
render_constraints_summary()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|