CarlyneB commited on
Commit
c6db565
·
1 Parent(s): 93fe9ab

[FEAT/FIX] tab2: unify method activatability check, fix hidden resource coverage by batch group, move default_constraint to data_utils

Browse files
Files changed (1) hide show
  1. 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
- covered = {b["name"] for b in custom_resources if m in b.get("methods", {})}
 
 
 
 
 
 
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 covered:
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
- covered = {b["name"] for b in custom_resources if m in b.get("methods", {})}
 
 
 
 
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
- available = r in covered
 
 
 
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, r, "" if available else source)
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, method_to_primary_resources, cost_sliders_norm):
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
- # Check for missing hidden resources. This runs before the regular
164
- # st.toggle so we never touch toggle_{m} at all for blocked methods —
165
- # the pre-render sync already set that key and Streamlit would raise
166
- # "cannot be modified after widget is instantiated" if we touched it again.
167
- # Instead we force the constraint to inactive in the data layer and show
168
- # a static label + info box without any widget that uses toggle_{m}.
169
- uncovered_required, _ = get_uncovered_hidden_resources(m, cost_sliders_norm)
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[f"manual_{m}"] = 0.0
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"]) if constraint["cap_value"] else 0.0
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 render_tab(cost_sliders):
343
- """Render the Method Constraints tab.
 
 
 
 
 
 
344
 
345
  Args:
346
- cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}.
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
- # Methods with uncovered hidden resources are always forced inactive —
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
- st.session_state[f"manual_{m}"] = float(c.get("cap_value", 0.0))
 
 
 
 
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
- st.session_state.setdefault(f"manual_{m}", float(c.get("cap_value", 0.0)))
 
 
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
- # have at least one visible (non-hidden) primary resource AND no uncovered
449
- # hidden resource that would block them.
450
  activatable = []
451
  for display_m, orig_m in all_display:
452
- if display_m != orig_m: # it's a variant → batch provides the resource
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, method_to_primary_resources, cost_sliders_norm)
486
  col_idx += 1
487
  else:
488
- render_method(m, cols, col_idx, method_to_primary_resources, cost_sliders_norm)
489
  col_idx += 1
490
 
491
- st.divider()
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()