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

[REFACTOR] tab1: relocate utility functions, refactor dialog key management and group selector

Browse files
Files changed (1) hide show
  1. tabs/tab1_inputs.py +195 -313
tabs/tab1_inputs.py CHANGED
@@ -4,46 +4,10 @@ import streamlit as st
4
  from config.config import (resource_groups, resource_order, resource_tooltips,
5
  resource_unit_map, method_groups, CO2_UNIT, secondary_resources,
6
  hidden_resource_names)
7
- from utils.data_utils import render_resource_input, strip_unit_suffix, get_resource_to_group, get_resource_to_unit, get_method_group
8
- from utils.ui_helpers import show_bar
9
- from io import BytesIO
10
-
11
-
12
- def standard_resource_names():
13
- """Return the set of all standard resource names defined in resource_groups config.
14
-
15
- Returns:
16
- set[str]: flat set of every resource name across all resource groups.
17
- """
18
- return {r for rlist in resource_groups.values() for r in rlist}
19
-
20
-
21
- def init_custom_resources():
22
- """Initialise custom resource session state keys and coerce amounts/coefficients to float.
23
-
24
- Ensures custom_resources, enabled_methods, and auto_disabled_methods exist in
25
- session state, then sanitises every batch so numeric fields are float (not str).
26
-
27
- Returns:
28
- None
29
- """
30
- if "custom_resources" not in st.session_state:
31
- st.session_state.custom_resources = []
32
- if "enabled_methods" not in st.session_state:
33
- st.session_state.enabled_methods = {}
34
- if "auto_disabled_methods" not in st.session_state:
35
- st.session_state.auto_disabled_methods = set()
36
- for batch in st.session_state.custom_resources:
37
- try:
38
- batch["amount"] = float(batch["amount"])
39
- except (ValueError, TypeError):
40
- batch["amount"] = 0.0
41
- for m, coefs in batch.get("methods", {}).items():
42
- for key in ("min", "median", "max"):
43
- try:
44
- coefs[key] = float(coefs[key])
45
- except (ValueError, TypeError):
46
- coefs[key] = 0.0
47
 
48
 
49
  @st.dialog("Add a new resource", width="large")
@@ -62,13 +26,10 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
62
  Returns:
63
  None
64
  """
65
- if edit_index is not None and not st.session_state.get(f"_dlg_initialized_{edit_index}"):
66
- for key in list(st.session_state.keys()):
67
- if key.startswith(("_dlg_existing_enable_",
68
- "_dlg_existing_min_", "_dlg_existing_med_", "_dlg_existing_max_",
69
- "_dlg_new_chk_", "_dlg_new_min_", "_dlg_new_med_", "_dlg_new_max_")):
70
- del st.session_state[key]
71
- st.session_state[f"_dlg_initialized_{edit_index}"] = True
72
 
73
  standard_names = standard_resource_names()
74
  existing_names = {
@@ -82,7 +43,6 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
82
  # If opened from a specific resource-group expander ("+ Add resource" inline
83
  # button), restrict the reference-resource choice to that group's resources
84
  # only — the user picked the group by clicking that button.
85
- group_options = preset_resources if preset_resources else resource_order
86
  if preset_resources:
87
  physical_group = resource_to_group.get(preset_resources[0], "")
88
  st.markdown(
@@ -99,19 +59,48 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
99
  value=existing.get("name", ""),
100
  placeholder="e.g. Degraded forest lands – North",
101
  help="It can't match the name of an existing resource.",
102
- key="label-input-name"
103
- )
104
- with col_group:
105
- default_group_idx = group_options.index(existing["group"]) if existing.get("group") in group_options else 0
106
- batch_group = st.selectbox(
107
- "Resource group *",
108
- options=group_options,
109
- index=default_group_idx,
110
- help="Determines the physical unit of this resource.",
111
- disabled=bool(preset_resources) and len(group_options) == 1,
112
- key="label-input-group"
113
  )
114
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  group = resource_to_group.get(batch_group)
116
  unit = resource_unit_map.get(group, "?")
117
 
@@ -125,7 +114,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
125
  format="%.2f",
126
  value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None,
127
  help=f"Total quantity available for 1 year, in {unit}.",
128
- key="label-input-amount",
129
  placeholder="0.0"
130
  )
131
 
@@ -167,12 +156,10 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
167
 
168
  already_selected = m in existing.get("methods", {})
169
 
170
- if f"_dlg_existing_enable_{m}" not in st.session_state:
171
- st.session_state[f"_dlg_existing_enable_{m}"] = already_selected
172
-
173
  enabled = st.checkbox(
174
  f"{m}",
175
- key=f"_dlg_existing_enable_{m}",
 
176
  )
177
 
178
  if not enabled:
@@ -180,31 +167,31 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
180
 
181
  existing_coeff = existing.get("methods", {}).get(m, coeff)
182
 
183
- st.warning("Modifying these values only affects this resource batch: the base method coefficients are unchanged.")
184
  col_min, col_med, col_max = st.columns(3)
185
  with col_min:
186
  e_min = st.number_input(
187
  f"Min ({unit}/{CO2_UNIT})",
188
  min_value=0.0,
189
  format="%.10g",
190
- value=float(existing_coeff["min"]),
191
- key=f"_dlg_existing_min_{m}",
192
  )
193
  with col_med:
194
  e_med = st.number_input(
195
  f"Median ({unit}/{CO2_UNIT})",
196
  min_value=0.0,
197
  format="%.10g",
198
- value=float(existing_coeff["median"]),
199
- key=f"_dlg_existing_med_{m}",
200
  )
201
  with col_max:
202
  e_max = st.number_input(
203
  f"Max ({unit}/{CO2_UNIT})",
204
  min_value=0.0,
205
  format="%.10g",
206
- value=float(existing_coeff["max"]),
207
- key=f"_dlg_existing_max_{m}",
208
  )
209
  method_configs[m] = {
210
  "min": e_min,
@@ -237,7 +224,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
237
  for m in available_methods:
238
  enabled = st.checkbox(
239
  f"**{m}**",
240
- key=f"_dlg_new_chk_{m}",
241
  )
242
  if enabled:
243
  col_min, col_med, col_max = st.columns(3)
@@ -247,7 +234,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
247
  min_value=0.0,
248
  value=None,
249
  format="%.10g",
250
- key=f"_dlg_new_min_{m}",
251
  placeholder="0.0000",
252
  )
253
  with col_med:
@@ -256,16 +243,16 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
256
  min_value=0.0,
257
  value=None,
258
  format="%.10g",
259
- key=f"_dlg_new_med_{m}",
260
  placeholder="0.0000",
261
  )
262
  with col_max:
263
  v_max = st.number_input(
264
- f"Ma ({unit}/{CO2_UNIT})",
265
  min_value=0.0,
266
  value=None,
267
  format="%.10g",
268
- key=f"_dlg_new_max_{m}",
269
  placeholder="0.0000",
270
  )
271
  method_configs[m] = {
@@ -277,7 +264,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
277
  col_save, col_cancel = st.columns([2, 2])
278
 
279
  with col_cancel:
280
- if st.button("Cancel", width='stretch', key="btn-reset-cancel"):
281
  st.rerun()
282
 
283
  def validate():
@@ -303,7 +290,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
303
  return errors
304
 
305
  with col_save:
306
- if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key="btn-new-add"):
307
  errors = validate()
308
  if errors:
309
  with error_placeholder.container():
@@ -329,77 +316,6 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
329
 
330
 
331
 
332
- def to_excel_bytes(batches: list) -> bytes:
333
- """Serialise all custom resource batches to an Excel file (one row per batch × method).
334
-
335
- Args:
336
- batches (list): list of custom batch dicts with keys name, group, amount, unit, methods.
337
-
338
- Returns:
339
- bytes: raw bytes of the .xlsx workbook.
340
- """
341
- rows = []
342
- for batch in batches:
343
- for m, coefs in batch["methods"].items():
344
- rows.append({
345
- "name": batch["name"],
346
- "group": batch["group"],
347
- "amount": float(batch["amount"]),
348
- "unit": batch["unit"],
349
- "method": m,
350
- "min": float(coefs["min"]),
351
- "median": float(coefs["median"]),
352
- "max": float(coefs["max"]),
353
- })
354
- excel_buffer = BytesIO()
355
- with pd.ExcelWriter(excel_buffer, engine="openpyxl") as writer:
356
- pd.DataFrame(rows).to_excel(writer, index=False, sheet_name="custom_resources")
357
- return excel_buffer.getvalue()
358
-
359
-
360
- def parse_custom_excel(file) -> list:
361
- """Parse a custom resource Excel file into a list of batch dicts.
362
-
363
- Expects columns: name, group, amount, unit, method, min, median, max.
364
-
365
- Args:
366
- file: file-like object pointing to an .xlsx workbook.
367
-
368
- Returns:
369
- list[dict]: one dict per unique batch name with keys
370
- name, group, amount, unit, methods.
371
-
372
- Raises:
373
- ValueError: if required columns are missing.
374
- """
375
- df = pd.read_excel(file, sheet_name=0)
376
- required = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
377
- if not required.issubset(df.columns):
378
- raise ValueError(f"Missing columns: {required - set(df.columns)}")
379
- batches = []
380
- for batch_name in df["name"].unique():
381
- df_b = df[df["name"] == batch_name]
382
- methods = {
383
- row["method"]: {
384
- "min": float(row["min"]),
385
- "median": float(row["median"]),
386
- "max": float(row["max"]),
387
- }
388
- for _, row in df_b.iterrows()
389
- }
390
- group_name = str(df_b["group"].iloc[0])
391
- resource_to_group = get_resource_to_group()
392
- batches.append({
393
- "name": str(batch_name),
394
- "group": group_name,
395
- "amount": float(df_b["amount"].iloc[0]),
396
- "unit": resource_unit_map.get(resource_to_group.get(group_name), "?"),
397
- "methods": methods,
398
- })
399
- return batches
400
-
401
-
402
-
403
  @st.dialog("Load custom resource batches", width="large")
404
  def dialog_load_custom_batch():
405
  """Dialog for importing custom resource batches from an Excel file.
@@ -531,9 +447,12 @@ def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
531
  st.session_state.custom_resources.pop(global_idx)
532
  st.session_state.pop("latest_result", None)
533
  st.rerun()
534
- if st.button("Edit", key=f"edit_batch_{global_idx}",
535
  type="secondary", icon=":material/edit:"):
536
- dialog_add_custom_resource(cost_sliders, edit_index=global_idx)
 
 
 
537
 
538
 
539
 
@@ -565,6 +484,128 @@ def panel_custom_resources_controls(cost_sliders):
565
 
566
 
567
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  def render_tab(cost_sliders):
569
  """Render the Resource Inputs tab.
570
 
@@ -704,52 +745,7 @@ def render_tab(cost_sliders):
704
 
705
  render_resource_input(name_clean, r, tooltip=tooltip)
706
 
707
- related_methods = {}
708
- related_by_group = {}
709
-
710
- for cdr_method, resource in cost_sliders.items():
711
- for name, coeff in resource.items():
712
- if r == strip_unit_suffix(name):
713
- related_methods[cdr_method] = coeff
714
-
715
- for m in related_methods:
716
- related_by_group.setdefault(get_method_group(m), []).append(m)
717
-
718
- related_method_names = list(related_methods.keys())
719
- secondary_for_r = [m for m in related_method_names if (m, r) in secondary_resources]
720
- primary_for_r = [m for m in related_method_names if (m, r) not in secondary_resources]
721
-
722
- if r not in st.session_state.enabled_methods:
723
- st.session_state.enabled_methods[r] = {}
724
- for m in related_method_names:
725
- st.session_state.enabled_methods[r].setdefault(m, True)
726
- popover_key_prefix = f"pop_{r}"
727
-
728
- with st.popover(
729
- f":material/tune: Related CDR Methods",
730
- width='stretch',
731
- ):
732
- for method_group, methods in related_by_group.items():
733
- primary_in_group = [m for m in methods if m in primary_for_r]
734
- if not primary_in_group:
735
- continue
736
- st.markdown(f"*{method_group}*")
737
- for m in primary_in_group:
738
- widget_key = f"{popover_key_prefix}_{m}"
739
- if widget_key not in st.session_state:
740
- st.session_state[widget_key] = st.session_state.enabled_methods[r][m]
741
-
742
- checked = st.checkbox(m, key=widget_key)
743
- st.session_state.enabled_methods[r][m] = checked
744
-
745
- if secondary_for_r:
746
- st.caption(
747
- "The following methods use this resource as secondary and may be assigned later in the **Resource Coefficients** tab:\n\n"
748
- + "\n".join(f"- {m}" for m in secondary_for_r)
749
- )
750
-
751
- for m in secondary_for_r:
752
- st.session_state.enabled_methods[r][m] = True
753
 
754
  # Custom batches for this group
755
  group_unit = resource_unit_map.get(group, "")
@@ -768,122 +764,8 @@ def render_tab(cost_sliders):
768
  ):
769
  dialog_add_custom_resource(cost_sliders, preset_resources=rlist)
770
 
771
- # ── AUTO-DISABLE LOGIC ────────────────────────────────────────────────────
772
- # A CDR method cannot run in the optimisation if it has no usable resources.
773
- # This block detects that situation automatically and deactivates the method
774
- # in the Method Constraints tab — so the optimisation never fails silently.
775
- #
776
- # The logic runs in three steps every time this tab renders:
777
- # 1. Count how many active primary resources each method still has.
778
- # 2. Re-enable any method that was auto-disabled and now has resources again
779
- # (e.g. the user re-entered a value). Methods the user manually disabled
780
- # are stored separately and are NOT touched here.
781
- # 3. Disable any method whose resource count just dropped to zero, and show
782
- # a notification so the user knows what changed.
783
- # ─────────────────────────────────────────────────────────────────────────
784
-
785
- # Step 1 – count active primary resources per method (secondary resources are
786
- # optional extras; they do not determine whether a method can run).
787
- method_active_resource_count = {}
788
- for r, methods in st.session_state.enabled_methods.items():
789
- for m, ok in methods.items():
790
- if (m, r) in secondary_resources:
791
- continue
792
- method_active_resource_count.setdefault(m, 0)
793
- if ok:
794
- method_active_resource_count[m] += 1
795
-
796
- # Custom batches also count as active resources for the methods they are
797
- # assigned to, even when the standard reference resource is toggled off.
798
- for batch in st.session_state.get("custom_resources", []):
799
- if float(batch.get("amount") or 0) <= 0:
800
- continue
801
- for m_name in batch.get("methods", {}).keys():
802
- method_active_resource_count.setdefault(m_name, 0)
803
- method_active_resource_count[m_name] += 1
804
-
805
- methods_with_no_active_resource = [
806
- m for m in method_to_primary_resources
807
- if method_active_resource_count.get(m, 0) == 0
808
- ]
809
-
810
- # Step 2 – re-enable methods that the tool previously auto-disabled but that
811
- # now have at least one active resource (user restored a value or added a batch).
812
- for m in list(st.session_state.auto_disabled_methods):
813
- if method_active_resource_count.get(m, 0) > 0:
814
- if m in st.session_state.constraints:
815
- st.session_state.constraints[m]["active"] = True
816
- st.session_state.constraints[m]["cap_type"] = "percent"
817
- st.session_state.constraints[m]["cap_value"] = 100
818
- st.session_state[f"toggle_{m}"] = True
819
- st.session_state[f"slider_{m}"] = 100
820
- st.session_state.auto_disabled_methods.discard(m)
821
- st.session_state.setdefault("just_activated_methods", set()).add(m)
822
-
823
- # Step 3 – disable methods that now have zero active primary resources.
824
- # We record which methods the tool disabled (auto_disabled_methods) so that
825
- # step 2 can safely re-enable them later without touching user-chosen settings.
826
- newly_disabled = []
827
- for m in methods_with_no_active_resource:
828
- # Always track in auto_disabled_methods so Step 2 can re-enable later,
829
- # even if the method was already inactive (e.g. initialised as False in app.py).
830
- st.session_state.auto_disabled_methods.add(m)
831
- if st.session_state.constraints.get(m, {}).get("active", True):
832
- st.session_state.constraints.setdefault(m, {"active": True, "cap_type": "percent", "cap_value": 100})
833
- st.session_state.constraints[m]["active"] = False
834
- newly_disabled.append(m)
835
- toggle_key = f"toggle_{m}"
836
- if toggle_key in st.session_state:
837
- st.session_state[toggle_key] = False
838
 
839
  panel_custom_resources_controls(cost_sliders)
840
 
841
- st.markdown('<p class="crra-sub-heading">Summary of available resources</p>', unsafe_allow_html=True)
842
-
843
- resource_to_unit = get_resource_to_unit()
844
-
845
- df_caps = pd.DataFrame.from_dict(
846
- {r: st.session_state.resource_caps[r] for r in resource_order
847
- if st.session_state.resource_caps.get(r, 0.0) > 0},
848
- orient="index", columns=["Available Amount"],
849
- ).reindex(resource_order).dropna()
850
- df_caps["Available Amount"] = [
851
- f"{value:,.2f} {resource_to_unit.get(resource, '?')}"
852
- for resource, value in df_caps["Available Amount"].items()
853
- ]
854
-
855
- custom_rows = []
856
- for batch in st.session_state.get("custom_resources", []):
857
- group = resource_to_group.get(batch["group"])
858
- unit = resource_unit_map.get(group, "?")
859
- custom_rows.append({
860
- "Resource": batch["name"],
861
- "Available Amount": f"{float(batch['amount']):,.2f} {unit}",
862
- })
863
-
864
- if not df_caps.empty or custom_rows:
865
- dfs = []
866
- if not df_caps.empty:
867
- df_std = df_caps.copy()
868
- df_std["Type"] = "Standard"
869
- dfs.append(df_std)
870
- if custom_rows:
871
- df_custom = pd.DataFrame(custom_rows).set_index("Resource")
872
- df_custom["Type"] = "New"
873
- dfs.append(df_custom)
874
- df_all = pd.concat(dfs)[["Available Amount", "Type"]].reset_index().rename(columns={"index": "Resource"})
875
- num_rows = len(df_all)
876
- st.dataframe(
877
- df_all,
878
- width='stretch',
879
- hide_index=True,
880
- height=(num_rows * 36 + 36),
881
- column_config={
882
- "Resource": st.column_config.TextColumn("Resource", width="large"),
883
- "Available Amount": st.column_config.TextColumn("Available Amount"),
884
- "Type": st.column_config.TextColumn("Type"),
885
- },
886
- row_height=36
887
- )
888
- else:
889
- st.error("No resources defined yet. Set availability above to populate the summary table.")
 
4
  from config.config import (resource_groups, resource_order, resource_tooltips,
5
  resource_unit_map, method_groups, CO2_UNIT, secondary_resources,
6
  hidden_resource_names)
7
+ from utils.data_utils import (strip_unit_suffix, get_resource_to_group, get_resource_to_unit,
8
+ get_method_group, standard_resource_names, to_excel_bytes, parse_custom_excel)
9
+ from utils.state_utils import apply_auto_disable_logic, init_custom_resources
10
+ from utils.ui_helpers import show_bar, render_resource_input
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
 
13
  @st.dialog("Add a new resource", width="large")
 
26
  Returns:
27
  None
28
  """
29
+ # Use edit_index as a key suffix so each edit session gets fresh widgets.
30
+ # Streamlit ignores value= when a key already exists in session_state, so
31
+ # suffixing with eix guarantees unique keys per resource and avoids stale data.
32
+ eix = edit_index if edit_index is not None else "new"
 
 
 
33
 
34
  standard_names = standard_resource_names()
35
  existing_names = {
 
43
  # If opened from a specific resource-group expander ("+ Add resource" inline
44
  # button), restrict the reference-resource choice to that group's resources
45
  # only — the user picked the group by clicking that button.
 
46
  if preset_resources:
47
  physical_group = resource_to_group.get(preset_resources[0], "")
48
  st.markdown(
 
59
  value=existing.get("name", ""),
60
  placeholder="e.g. Degraded forest lands – North",
61
  help="It can't match the name of an existing resource.",
62
+ key=f"label-input-name-{eix}"
 
 
 
 
 
 
 
 
 
 
63
  )
64
 
65
+ if preset_resources:
66
+ # Already scoped to a specific resource list (inline "Add resource" button)
67
+ preset_idx = preset_resources.index(existing["group"]) if existing.get("group") in preset_resources else 0
68
+ with col_group:
69
+ batch_group = st.selectbox(
70
+ "Resource group *",
71
+ options=preset_resources,
72
+ index=preset_idx,
73
+ help="Determines the physical unit of this resource.",
74
+ disabled=len(preset_resources) == 1,
75
+ key=f"label-input-group-{eix}"
76
+ )
77
+ else:
78
+ # Two-level selection: physical group → specific resource
79
+ phys_group_options = list(resource_groups.keys())
80
+ existing_phys = resource_to_group.get(existing.get("group", ""), phys_group_options[0])
81
+ default_phys_idx = phys_group_options.index(existing_phys) if existing_phys in phys_group_options else 0
82
+ with col_group:
83
+ selected_phys = st.selectbox(
84
+ "Resource group *",
85
+ options=phys_group_options,
86
+ index=default_phys_idx,
87
+ help="Determines the physical category of this resource.",
88
+ key=f"label-input-phys-group-{eix}"
89
+ )
90
+ sub_resources = resource_groups[selected_phys]
91
+ if len(sub_resources) > 1:
92
+ existing_sub = existing.get("group", "")
93
+ default_sub_idx = sub_resources.index(existing_sub) if existing_sub in sub_resources else 0
94
+ batch_group = st.selectbox(
95
+ "Resource sub-type *",
96
+ options=sub_resources,
97
+ index=default_sub_idx,
98
+ help="Specific resource type within this group.",
99
+ key=f"label-input-subgroup-{eix}"
100
+ )
101
+ else:
102
+ batch_group = sub_resources[0]
103
+
104
  group = resource_to_group.get(batch_group)
105
  unit = resource_unit_map.get(group, "?")
106
 
 
114
  format="%.2f",
115
  value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None,
116
  help=f"Total quantity available for 1 year, in {unit}.",
117
+ key=f"label-input-amount-{eix}",
118
  placeholder="0.0"
119
  )
120
 
 
156
 
157
  already_selected = m in existing.get("methods", {})
158
 
 
 
 
159
  enabled = st.checkbox(
160
  f"{m}",
161
+ value=already_selected,
162
+ key=f"_dlg_existing_enable_{eix}_{m}",
163
  )
164
 
165
  if not enabled:
 
167
 
168
  existing_coeff = existing.get("methods", {}).get(m, coeff)
169
 
170
+ st.warning("Modifying these values only affects this resource batch: the base method coefficients are unchanged.", icon=":material/warning:")
171
  col_min, col_med, col_max = st.columns(3)
172
  with col_min:
173
  e_min = st.number_input(
174
  f"Min ({unit}/{CO2_UNIT})",
175
  min_value=0.0,
176
  format="%.10g",
177
+ value=max(float(0.0), float(existing_coeff["min"])),
178
+ key=f"_dlg_existing_min_{eix}_{m}",
179
  )
180
  with col_med:
181
  e_med = st.number_input(
182
  f"Median ({unit}/{CO2_UNIT})",
183
  min_value=0.0,
184
  format="%.10g",
185
+ value=max(float(0.0), float(existing_coeff["median"])),
186
+ key=f"_dlg_existing_med_{eix}_{m}",
187
  )
188
  with col_max:
189
  e_max = st.number_input(
190
  f"Max ({unit}/{CO2_UNIT})",
191
  min_value=0.0,
192
  format="%.10g",
193
+ value=max(float(0.0), float(existing_coeff["max"])),
194
+ key=f"_dlg_existing_max_{eix}_{m}",
195
  )
196
  method_configs[m] = {
197
  "min": e_min,
 
224
  for m in available_methods:
225
  enabled = st.checkbox(
226
  f"**{m}**",
227
+ key=f"_dlg_new_chk_{eix}_{m}",
228
  )
229
  if enabled:
230
  col_min, col_med, col_max = st.columns(3)
 
234
  min_value=0.0,
235
  value=None,
236
  format="%.10g",
237
+ key=f"_dlg_new_min_{eix}_{m}",
238
  placeholder="0.0000",
239
  )
240
  with col_med:
 
243
  min_value=0.0,
244
  value=None,
245
  format="%.10g",
246
+ key=f"_dlg_new_med_{eix}_{m}",
247
  placeholder="0.0000",
248
  )
249
  with col_max:
250
  v_max = st.number_input(
251
+ f"Max ({unit}/{CO2_UNIT})",
252
  min_value=0.0,
253
  value=None,
254
  format="%.10g",
255
+ key=f"_dlg_new_max_{eix}_{m}",
256
  placeholder="0.0000",
257
  )
258
  method_configs[m] = {
 
264
  col_save, col_cancel = st.columns([2, 2])
265
 
266
  with col_cancel:
267
+ if st.button("Cancel", width='stretch', key=f"btn-reset-cancel-{eix}"):
268
  st.rerun()
269
 
270
  def validate():
 
290
  return errors
291
 
292
  with col_save:
293
+ if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key=f"btn-new-add-{eix}"):
294
  errors = validate()
295
  if errors:
296
  with error_placeholder.container():
 
316
 
317
 
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  @st.dialog("Load custom resource batches", width="large")
320
  def dialog_load_custom_batch():
321
  """Dialog for importing custom resource batches from an Excel file.
 
447
  st.session_state.custom_resources.pop(global_idx)
448
  st.session_state.pop("latest_result", None)
449
  st.rerun()
450
+ if st.button("Edit", key=f"edit_batch_{global_idx}",
451
  type="secondary", icon=":material/edit:"):
452
+ _b = st.session_state.custom_resources[global_idx]
453
+ _phys = get_resource_to_group().get(_b.get("group", ""))
454
+ _preset = resource_groups.get(_phys) if _phys else None
455
+ dialog_add_custom_resource(cost_sliders, edit_index=global_idx, preset_resources=_preset)
456
 
457
 
458
 
 
484
 
485
 
486
 
487
+ def render_related_methods_popover(r, cost_sliders, secondary_resources):
488
+ """Render the 'Related CDR Methods' popover for a single resource.
489
+
490
+ Builds the method-to-group mapping, initialises enabled_methods in session
491
+ state, and renders a popover with per-method checkboxes (primary only) and
492
+ a caption listing secondary methods.
493
+
494
+ Args:
495
+ r (str): canonical resource name.
496
+ cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}.
497
+ secondary_resources (set[tuple[str, str]]): set of (method, resource) pairs
498
+ where the resource is optional for that method.
499
+
500
+ Returns:
501
+ None
502
+ """
503
+ related_methods = {}
504
+ related_by_group = {}
505
+
506
+ for cdr_method, resource in cost_sliders.items():
507
+ for name, coeff in resource.items():
508
+ if r == strip_unit_suffix(name):
509
+ related_methods[cdr_method] = coeff
510
+
511
+ for m in related_methods:
512
+ related_by_group.setdefault(get_method_group(m), []).append(m)
513
+
514
+ related_method_names = list(related_methods.keys())
515
+ secondary_for_r = [m for m in related_method_names if (m, r) in secondary_resources]
516
+ primary_for_r = [m for m in related_method_names if (m, r) not in secondary_resources]
517
+
518
+ if r not in st.session_state.enabled_methods:
519
+ st.session_state.enabled_methods[r] = {}
520
+ for m in related_method_names:
521
+ st.session_state.enabled_methods[r].setdefault(m, True)
522
+
523
+ popover_key_prefix = f"pop_{r}"
524
+
525
+ with st.popover(":material/tune: Related CDR Methods", width='stretch'):
526
+ for method_group, methods in related_by_group.items():
527
+ primary_in_group = [m for m in methods if m in primary_for_r]
528
+ if not primary_in_group:
529
+ continue
530
+ st.markdown(f"*{method_group}*")
531
+ for m in primary_in_group:
532
+ widget_key = f"{popover_key_prefix}_{m}"
533
+ if widget_key not in st.session_state:
534
+ st.session_state[widget_key] = st.session_state.enabled_methods[r][m]
535
+ checked = st.checkbox(m, key=widget_key)
536
+ st.session_state.enabled_methods[r][m] = checked
537
+
538
+ if secondary_for_r:
539
+ st.caption(
540
+ "The following methods use this resource as secondary and may be assigned later in the **Resource Coefficients** tab:\n\n"
541
+ + "\n".join(f"- {m}" for m in secondary_for_r)
542
+ )
543
+
544
+ for m in secondary_for_r:
545
+ st.session_state.enabled_methods[r][m] = True
546
+
547
+
548
+ def render_resource_summary():
549
+ """Render the 'Summary of available resources' section.
550
+
551
+ Builds a combined dataframe of standard and custom resource availability
552
+ and displays it as a Streamlit dataframe.
553
+
554
+ Returns:
555
+ None
556
+ """
557
+ st.markdown('<p class="crra-sub-heading">Summary of available resources</p>', unsafe_allow_html=True)
558
+
559
+ resource_to_unit = get_resource_to_unit()
560
+ resource_to_group = get_resource_to_group()
561
+
562
+ df_caps = pd.DataFrame.from_dict(
563
+ {r: st.session_state.resource_caps[r] for r in resource_order
564
+ if st.session_state.resource_caps.get(r, 0.0) > 0},
565
+ orient="index", columns=["Available Amount"],
566
+ ).reindex(resource_order).dropna()
567
+ df_caps["Available Amount"] = [
568
+ f"{value:,.2f} {resource_to_unit.get(resource, '?')}"
569
+ for resource, value in df_caps["Available Amount"].items()
570
+ ]
571
+
572
+ custom_rows = []
573
+ for batch in st.session_state.get("custom_resources", []):
574
+ group = resource_to_group.get(batch["group"])
575
+ unit = resource_unit_map.get(group, "?")
576
+ custom_rows.append({
577
+ "Resource": batch["name"],
578
+ "Available Amount": f"{float(batch['amount']):,.2f} {unit}",
579
+ })
580
+
581
+ if not df_caps.empty or custom_rows:
582
+ dfs = []
583
+ if not df_caps.empty:
584
+ df_std = df_caps.copy()
585
+ df_std["Type"] = "Standard"
586
+ dfs.append(df_std)
587
+ if custom_rows:
588
+ df_custom = pd.DataFrame(custom_rows).set_index("Resource")
589
+ df_custom["Type"] = "New"
590
+ dfs.append(df_custom)
591
+ df_all = pd.concat(dfs)[["Available Amount", "Type"]].reset_index().rename(columns={"index": "Resource"})
592
+ num_rows = len(df_all)
593
+ st.dataframe(
594
+ df_all,
595
+ width='stretch',
596
+ hide_index=True,
597
+ height=(num_rows * 36 + 36),
598
+ column_config={
599
+ "Resource": st.column_config.TextColumn("Resource", width="large"),
600
+ "Available Amount": st.column_config.TextColumn("Available Amount"),
601
+ "Type": st.column_config.TextColumn("Type"),
602
+ },
603
+ row_height=36,
604
+ )
605
+ else:
606
+ st.warning("No resources defined yet. Set availability above to populate the summary table.", icon=":material/warning:")
607
+
608
+
609
  def render_tab(cost_sliders):
610
  """Render the Resource Inputs tab.
611
 
 
745
 
746
  render_resource_input(name_clean, r, tooltip=tooltip)
747
 
748
+ render_related_methods_popover(r, cost_sliders, secondary_resources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
749
 
750
  # Custom batches for this group
751
  group_unit = resource_unit_map.get(group, "")
 
764
  ):
765
  dialog_add_custom_resource(cost_sliders, preset_resources=rlist)
766
 
767
+ apply_auto_disable_logic(method_to_primary_resources, secondary_resources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
768
 
769
  panel_custom_resources_controls(cost_sliders)
770
 
771
+ render_resource_summary()