import pandas as pd import streamlit as st from config.config import (resource_groups, resource_order, resource_tooltips, resource_unit_map, method_groups, CO2_UNIT, secondary_resources, hidden_resource_names, section_intros) from utils.data_utils import (strip_unit_suffix, get_resource_to_group, get_resource_to_unit, get_method_group, standard_resource_names, to_excel_bytes, parse_custom_excel, resource_caps_to_excel_bytes) from utils.state_utils import init_custom_resources from utils.ui_helpers import show_bar, render_resource_input from tabs.tab2_constraints import refresh_active_methods @st.dialog("Add a new resource", width="large") def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, preset_resources: list | None = None): """Dialog for adding or editing a custom resource batch. Opens a modal form where the user picks a name, reference resource group, available quantity, and per-method coefficients (min/median/max). Args: cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}. edit_index (int | None): index into custom_resources to edit; None for a new batch. preset_resources (list | None): restrict the group selector to this resource list (used when launched from a group expander button). Returns: None """ # Use edit_index as a key suffix so each edit session gets fresh widgets. # Streamlit ignores value= when a key already exists in session_state, so # suffixing with eix guarantees unique keys per resource and avoids stale data. eix = edit_index if edit_index is not None else "new" standard_names = standard_resource_names() existing_names = { b["name"] for i, b in enumerate(st.session_state.custom_resources) if i != edit_index } existing = st.session_state.custom_resources[edit_index] if edit_index is not None else {} resource_to_group = get_resource_to_group() # If opened from a specific resource-group expander ("+ Add resource" inline # button), restrict the reference-resource choice to that group's resources # only — the user picked the group by clicking that button. if preset_resources: physical_group = resource_to_group.get(preset_resources[0], "") st.markdown( f'
Adding a new resource batch in the {physical_group} group.
', unsafe_allow_html=True, ) st.markdown('1 — Name & group
', unsafe_allow_html=True) col_name, col_group = st.columns(2) with col_name: batch_name = st.text_input( "Resource name *", value=existing.get("name", ""), placeholder="e.g. Degraded forest lands – North", help="It can't match the name of an existing resource.", key=f"label-input-name-{eix}" ) if preset_resources: # Already scoped to a specific resource list (inline "Add resource" button) preset_idx = preset_resources.index(existing["group"]) if existing.get("group") in preset_resources else 0 with col_group: batch_group = st.selectbox( "Resource group *", options=preset_resources, index=preset_idx, help="Determines the physical unit of this resource.", disabled=len(preset_resources) == 1, key=f"label-input-group-{eix}" ) else: # Two-level selection: physical group → specific resource phys_group_options = list(resource_groups.keys()) existing_phys = resource_to_group.get(existing.get("group", ""), phys_group_options[0]) default_phys_idx = phys_group_options.index(existing_phys) if existing_phys in phys_group_options else 0 with col_group: selected_phys = st.selectbox( "Resource group *", options=phys_group_options, index=default_phys_idx, help="Determines the physical category of this resource.", key=f"label-input-phys-group-{eix}" ) sub_resources = resource_groups[selected_phys] if len(sub_resources) > 1: existing_sub = existing.get("group", "") default_sub_idx = sub_resources.index(existing_sub) if existing_sub in sub_resources else 0 batch_group = st.selectbox( "Resource sub-type *", options=sub_resources, index=default_sub_idx, help="Specific resource type within this group.", key=f"label-input-subgroup-{eix}" ) else: batch_group = sub_resources[0] group = resource_to_group.get(batch_group) unit = resource_unit_map.get(group, "?") st.markdown(f'Unit for this group: {unit}/yr
', unsafe_allow_html=True) st.markdown('2 — Available quantity
', unsafe_allow_html=True) batch_amount = st.number_input( label=f"Available amount ({unit}/yr)", min_value=0.0, format="%.10g", value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None, help=f"Total quantity available for 1 year, in {unit}.", key=f"label-input-amount-{eix}", placeholder="0.0" ) st.markdown('3 — Existing CDR methods for this resource
', unsafe_allow_html=True) method_configs = {} all_related_methods = {} for cdr_method, resource in cost_sliders.items(): for name, coeff in resource.items(): if batch_group == strip_unit_suffix(name): all_related_methods[cdr_method] = { "min": coeff["min"], "median": coeff["median"], "max": coeff["max"], } # Split into primary (selectable) and secondary (auto-linked in Tab 3) primary_related = { m: coeff for m, coeff in all_related_methods.items() if (m, batch_group) not in secondary_resources } secondary_related = { m: coeff for m, coeff in all_related_methods.items() if (m, batch_group) in secondary_resources } primary_by_group = {} for m, coeff in primary_related.items(): method_group = get_method_group(m) primary_by_group.setdefault(method_group, {})[m] = coeff if not primary_by_group: st.markdown('No primary CDR methods use this resource directly.
', unsafe_allow_html=True) for method_group, methods in primary_by_group.items(): with st.expander(f"**{method_group}**", expanded=False): for m, coeff in methods.items(): already_selected = m in existing.get("methods", {}) enabled = st.checkbox( f"{m}", value=already_selected, key=f"_dlg_existing_enable_{eix}_{m}", ) if not enabled: continue existing_coeff = existing.get("methods", {}).get(m, coeff) st.warning("Modifying these values only affects this resource batch: the base method coefficients are unchanged.", icon=":material/warning:") col_min, col_med, col_max = st.columns(3) with col_min: e_min = st.number_input( f"Min ({unit}/{CO2_UNIT})", min_value=0.0, format="%.10g", value=max(float(0.0), float(existing_coeff["min"])), key=f"_dlg_existing_min_{eix}_{m}", ) with col_med: e_med = st.number_input( f"Median ({unit}/{CO2_UNIT})", min_value=0.0, format="%.10g", value=max(float(0.0), float(existing_coeff["median"])), key=f"_dlg_existing_med_{eix}_{m}", ) with col_max: e_max = st.number_input( f"Max ({unit}/{CO2_UNIT})", min_value=0.0, format="%.10g", value=max(float(0.0), float(existing_coeff["max"])), key=f"_dlg_existing_max_{eix}_{m}", ) method_configs[m] = { "min": e_min, "median": e_med, "max": e_max, } if secondary_related: names = "\n".join(f"- **{m}**" for m in secondary_related) st.info( f"""The following methods use this resource as **secondary** and may be assigned later in the **Resource Coefficients** tab:\n{names} """, icon=":material/info:", ) st.markdown('4 — Add additional CDR methods
', unsafe_allow_html=True) st.markdown(f'Select additional methods and enter their coefficients in {unit}/{CO2_UNIT}.
', unsafe_allow_html=True) already_used = set(all_related_methods.keys()) for group_name, methods in method_groups.items(): available_methods = [ m for m in methods if m not in already_used ] if not available_methods: continue with st.expander(f"**{group_name}**", expanded=False): for m in available_methods: already_selected = m in existing.get("methods", {}) enabled = st.checkbox( f"**{m}**", value=already_selected, key=f"_dlg_new_chk_{eix}_{m}", ) if enabled: existing_coeff = existing.get("methods", {}).get(m, {}) col_min, col_med, col_max = st.columns(3) with col_min: v_min = st.number_input( f"Min ({unit}/{CO2_UNIT})", min_value=0.0, value=float(existing_coeff["min"]) if existing_coeff.get("min") is not None else None, format="%.10g", key=f"_dlg_new_min_{eix}_{m}", placeholder="0.0000", ) with col_med: v_med = st.number_input( f"Median ({unit}/{CO2_UNIT})", min_value=0.0, value=float(existing_coeff["median"]) if existing_coeff.get("median") is not None else None, format="%.10g", key=f"_dlg_new_med_{eix}_{m}", placeholder="0.0000", ) with col_max: v_max = st.number_input( f"Max ({unit}/{CO2_UNIT})", min_value=0.0, value=float(existing_coeff["max"]) if existing_coeff.get("max") is not None else None, format="%.10g", key=f"_dlg_new_max_{eix}_{m}", placeholder="0.0000", ) method_configs[m] = { "min": v_min, "median": v_med, "max": v_max, } error_placeholder = st.empty() col_save, col_cancel = st.columns([2, 2]) with col_cancel: if st.button("Cancel", width='stretch', key=f"btn-reset-cancel-{eix}"): st.rerun() def validate(): errors = [] if not batch_name.strip(): errors.append("Resource name is required.") elif batch_name.strip().lower() in {n.lower() for n in standard_names}: errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.") elif batch_name.strip().lower() in {n.lower() for n in existing_names}: errors.append(f"**{batch_name}** already exists. Choose a different name.") if not batch_amount or batch_amount <= 0: errors.append("Available amount must be greater than 0.") if not method_configs and not secondary_related: errors.append("Select at least one CDR method.") for m, c in method_configs.items(): if any(v is None for v in [c["min"], c["median"], c["max"]]): errors.append(f"**{m}**: all coefficient fields (min, median, max) are required.") continue if c["min"] > c["max"]: errors.append(f"**{m}**: min ({c['min']}) cannot be greater than max ({c['max']}).") if not (c["min"] <= c["median"] <= c["max"]): errors.append(f"**{m}**: median must be between min and max.") return errors with col_save: if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key=f"btn-new-add-{eix}"): errors = validate() if errors: with error_placeholder.container(): for e in errors: st.error(e, icon=":material/error:") else: batch = { "name": batch_name.strip(), "group": batch_group, "amount": float(batch_amount), # always store as float "unit": resource_unit_map.get(resource_to_group.get(batch_group), "?"), "methods": method_configs, } if edit_index is not None: # Clean up stale costs[method][old_name] entries left behind by # this edit — otherwise tab4's missing-resource check wrongly # deactivates that method on the next optimisation run. Two # triggers: (a) a method that was previously assigned is no # longer checked, or (b) the batch itself was renamed, which # orphans the old name even for methods still checked (the new # name gets a fresh entry injected at run time, but the old # name's entry is never removed on its own). old_name = existing.get("name", "") new_name = batch_name.strip() renamed = bool(old_name) and old_name != new_name for method_name in existing.get("methods", {}): if renamed or method_name not in method_configs: st.session_state.costs.get(method_name, {}).pop(old_name, None) st.session_state.custom_resources[edit_index] = batch st.session_state.pop(f"custom_amount_{edit_index}", None) else: new_idx = len(st.session_state.custom_resources) st.session_state.custom_resources.append(batch) st.session_state.pop(f"custom_amount_{new_idx}", None) # Push the new median into costs for any (method, coefficient) # pair that actually changed in this edit — tab3's own init only # sets a value the first time a key appears (`if r not in # costs[m]`), so without this a coefficient edit made here would # never overwrite the stale value already there. Only touch # pairs whose median actually changed (or that are brand new) so # a deliberate Tab 3 slider adjustment isn't silently reset just # because the user edited something unrelated (e.g. the amount). old_methods = existing.get("methods", {}) for method_name, coeff in method_configs.items(): old_coeff = old_methods.get(method_name, {}) if old_coeff.get("median") != coeff["median"]: st.session_state.costs.setdefault(method_name, {})[batch["name"]] = coeff["median"] # If min/max changed, tab3's current scenario value (used to # seed its slider/manual widgets, keyed only by method+resource # name so it persists across this edit) may now fall outside # the new range — st.slider raises if its stored value is out # of [min_value, max_value]. Clamp it into the new bounds # (rather than resetting to the median) so the user's chosen # operating point is kept as close as possible, then clear the # widget keys so tab3 re-initialises from this clamped value. if old_coeff.get("min") != coeff["min"] or old_coeff.get("max") != coeff["max"]: current = st.session_state.costs.get(method_name, {}).get(batch["name"]) if current is not None: clamped = max(coeff["min"], min(coeff["max"], current)) st.session_state.costs.setdefault(method_name, {})[batch["name"]] = clamped key_suffix = f"{method_name}_{batch['name']}".replace(" ", "_") for prefix in ("slider_", "manual_", "toggle_"): st.session_state.pop(f"{prefix}{key_suffix}", None) st.session_state.pop("latest_result", None) st.rerun() @st.dialog("Load custom resource batches", width="large") def dialog_load_custom_batch(): """Dialog for importing custom resource batches from an Excel file. Reads an .xlsx file produced by to_excel_bytes, previews each batch's status (new / overwrite / conflict), and merges them into session state on confirmation. Returns: None """ st.markdown('Import an Excel file previously exported from the Custom resource section.
', unsafe_allow_html=True) uploaded = st.file_uploader("Upload Excel file", type=["xlsx"], key="inputs_loader_custom") if uploaded is None: return try: new_batches = parse_custom_excel(uploaded) except Exception as e: st.error(f"Error reading file: {e}") return standard_names = standard_resource_names() existing_names = {b["name"] for b in st.session_state.custom_resources} previews = [] for batch in new_batches: if batch["name"] in standard_names: status = "❌ Conflicts with standard resource — will be skipped" elif batch["name"] in existing_names: status = "⚠️ Already exists — will be overwritten" else: status = "✅ New" previews.append({ "Resource name": batch["name"], "Group": batch["group"], "Amount": f"{batch["amount"]} {resource_unit_map.get(batch["group"], "")}", "Methods": len(batch["methods"]), "Status": status, }) st.dataframe(pd.DataFrame(previews), width='stretch', hide_index=True) if st.button("↩️ Import all batches", type="primary", width='stretch'): imported = 0 for batch in new_batches: if batch["name"] in standard_names: continue if batch["name"] in existing_names: idx = next(i for i, b in enumerate(st.session_state.custom_resources) if b["name"] == batch["name"]) # Purge costs[method][batch_name] for methods dropped by the # re-imported version — harmless today (build_lp_indices gates # on the batch's own primary_methods) but avoids stale data # lingering in session state until the next Tab 3 visit. old_methods = set(st.session_state.custom_resources[idx].get("methods", {})) for method_name in old_methods - set(batch.get("methods", {})): st.session_state.costs.get(method_name, {}).pop(batch["name"], None) st.session_state.custom_resources[idx] = batch else: st.session_state.custom_resources.append(batch) imported += 1 st.session_state.pop("latest_result", None) st.success(f"Imported {imported} batch(es).") st.rerun() @st.dialog("Load standard resource values", width="large") def dialog_load_standard_resources(): """Dialog for importing standard resource availabilities from an Excel file. Reads an .xlsx file with "Resource" and "Available Amount" columns (the same format as the downloadable template / current-values export) and overwrites st.session_state.resource_caps on confirmation. Returns: None """ st.markdown( 'Upload an Excel file: either the blank template or a previous export of your current values.
', unsafe_allow_html=True, ) uploaded = st.file_uploader("Upload Excel file", type=["xlsx"], key="resource_loader_dialog") if uploaded is None: return try: df = pd.read_excel(uploaded) required_cols = {"Resource", "Available Amount"} if not required_cols.issubset(df.columns): raise ValueError(f"Missing columns: {required_cols - set(df.columns)}") except Exception as e: st.error(f"Error reading file: {e}") return preview_rows = [ {"Resource": row["Resource"], "Available Amount": row["Available Amount"]} for _, row in df.iterrows() if row["Resource"] in st.session_state.resource_caps ] st.dataframe(pd.DataFrame(preview_rows), width='stretch', hide_index=True) if st.button("Import values", type="primary", width='stretch', icon=":material/check:"): for r in st.session_state.resource_caps: st.session_state.resource_caps[r] = 0.0 st.session_state[f"cap_{r}"] = 0.0 for _, row in df.iterrows(): r = row["Resource"] if r not in st.session_state.resource_caps: continue val = row["Available Amount"] try: val = 0.0 if pd.isna(val) else float(val) except (ValueError, TypeError): val = 0.0 st.session_state.resource_caps[r] = val st.session_state[f"cap_{r}"] = val st.session_state["resource_last_loaded_file"] = uploaded.name for key in ["excel_loaded_file", "scenario_name_suggestion", "scenario_loaded_file", "input_file_name_suggestion", "latest_result"]: st.session_state.pop(key, None) st.success("Resource values imported successfully.", icon=":material/check:") st.rerun() def render_custom_batch_inline(global_idx, batch, cost_sliders, unit): """Render a single custom batch inside its resource-group expander.""" amount_val = float(batch["amount"]) batch["unit"] = unit with st.container(border=True): key_amount = f"custom_amount_{global_idx}" if key_amount not in st.session_state: st.session_state[key_amount] = amount_val if amount_val else None col_amount, _, _, _ = st.columns(4) with col_amount: new_amount = st.number_input( f"{batch['name']}", min_value=0.0, format="%.10g", key=key_amount, placeholder="0.0", ) new_amount_float = float(new_amount) if new_amount is not None else 0.0 if new_amount_float != amount_val: st.session_state.custom_resources[global_idx]["amount"] = new_amount_float st.session_state.pop("latest_result", None) col_min = f"Min ({unit}/MtCO₂)" col_med = f"Median ({unit}/MtCO₂)" col_max = f"Max ({unit}/MtCO₂)" rows = [ { "Method": m, col_min: float(c["min"]), col_med: float(c["median"]), col_max: float(c["max"]), } for m, c in batch["methods"].items() ] if rows: st.dataframe( pd.DataFrame(rows), width='stretch', hide_index=True, key=f"batch_editor_{global_idx}", column_config={ "Method": st.column_config.TextColumn(disabled=True), col_min: st.column_config.NumberColumn(format="%.10g", min_value=0.0), col_med: st.column_config.NumberColumn(format="%.10g", min_value=0.0), col_max: st.column_config.NumberColumn(format="%.10g", min_value=0.0), }, ) else: secondary_methods = [ m for m in cost_sliders if (m, batch["group"]) in secondary_resources ] if secondary_methods: methods_list = ", ".join(f"**{m}**" for m in secondary_methods) st.caption( f"*No CDR methods selected.*\n\n" f"This resource is available as an optional input for: {methods_list}.\n\n" f"Go to the **Resource Coefficients** tab to enable this resource for any of the methods above." ) else: st.caption("No CDR methods selected.") with st.container(key=f"container-button-{global_idx}"): if st.button("Delete", key=f"del_batch_{global_idx}", type="primary", icon=":material/delete:"): deleted = st.session_state.custom_resources.pop(global_idx) # Clean up any coefficient this batch left in st.session_state.costs # for the methods it was assigned to. Without this, a stale # costs[method][batch_name] entry survives the deletion, and tab4's # "missing resource" check (which only looks at custom_resources, # not costs) wrongly deactivates that method on the next run. for method_name in deleted.get("methods", {}): st.session_state.costs.get(method_name, {}).pop(deleted["name"], None) st.session_state.pop("latest_result", None) st.rerun() if st.button("Edit", key=f"edit_batch_{global_idx}", type="secondary", icon=":material/edit:"): _b = st.session_state.custom_resources[global_idx] _phys = get_resource_to_group().get(_b.get("group", "")) _preset = resource_groups.get(_phys) if _phys else None dialog_add_custom_resource(cost_sliders, edit_index=global_idx, preset_resources=_preset) def panel_custom_resources_controls(cost_sliders): """Buttons to add / load / download custom resource batches (no batch display here).""" init_custom_resources() st.markdown('➕ New resource
', unsafe_allow_html=True) st.markdown( 'Add sub-batches for any resource group and manually assign them to specific CDR methods.
', unsafe_allow_html=True, ) batches = st.session_state.custom_resources with st.container(key="container-btn"): if st.button("New resource", type="tertiary", icon=":material/add:", key="btn-new-resource"): dialog_add_custom_resource(cost_sliders) if st.button("Load from Excel", type="tertiary", icon=":material/arrow_upload_progress:", key="btn-load"): dialog_load_custom_batch() if batches: st.download_button( "Download new resources", data=to_excel_bytes(batches), file_name="new_resources.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", icon=":material/download:", key="download_button_save_custom", ) def render_related_methods_popover(r, cost_sliders, secondary_resources): """Render the 'Related CDR Methods' popover for a single resource. Builds the method-to-group mapping, initialises enabled_methods in session state, and renders a popover with per-method checkboxes (primary only) and a caption listing secondary methods. Args: r (str): canonical resource name. cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}. secondary_resources (set[tuple[str, str]]): set of (method, resource) pairs where the resource is optional for that method. Returns: None """ related_methods = {} related_by_group = {} for cdr_method, resource in cost_sliders.items(): for name, coeff in resource.items(): if r == strip_unit_suffix(name): related_methods[cdr_method] = coeff for m in related_methods: related_by_group.setdefault(get_method_group(m), []).append(m) related_method_names = list(related_methods.keys()) secondary_for_r = [m for m in related_method_names if (m, r) in secondary_resources] primary_for_r = [m for m in related_method_names if (m, r) not in secondary_resources] if r not in st.session_state.enabled_methods: st.session_state.enabled_methods[r] = {} for m in related_method_names: st.session_state.enabled_methods[r].setdefault(m, True) popover_key_prefix = f"pop_{r}" with st.popover(":material/tune: Related CDR Methods", width='stretch'): for method_group, methods in related_by_group.items(): primary_in_group = [m for m in methods if m in primary_for_r] if not primary_in_group: continue st.markdown(f"*{method_group}*") for m in primary_in_group: widget_key = f"{popover_key_prefix}_{m}" if widget_key not in st.session_state: st.session_state[widget_key] = st.session_state.enabled_methods[r][m] checked = st.checkbox(m, key=widget_key) st.session_state.enabled_methods[r][m] = checked if secondary_for_r: st.caption( "The following methods use this resource as secondary and may be assigned later in the **Resource Coefficients** tab:\n\n" + "\n".join(f"- {m}" for m in secondary_for_r) ) for m in secondary_for_r: st.session_state.enabled_methods[r][m] = True def render_resource_summary(): """Render the 'Summary of available resources' section. Builds a combined dataframe of standard and custom resource availability and displays it as a Streamlit dataframe. Returns: None """ st.markdown('Summary of available resources
', unsafe_allow_html=True) resource_to_unit = get_resource_to_unit() resource_to_group = get_resource_to_group() df_caps = pd.DataFrame.from_dict( {r: st.session_state.resource_caps[r] for r in resource_order if st.session_state.resource_caps.get(r, 0.0) > 0}, orient="index", columns=["Available Amount"], ).reindex(resource_order).dropna() df_caps["Available Amount"] = [ f"{value:,.2f} {resource_to_unit.get(resource, '?')}" for resource, value in df_caps["Available Amount"].items() ] custom_rows = [] for batch in st.session_state.get("custom_resources", []): group = resource_to_group.get(batch["group"]) unit = resource_unit_map.get(group, "?") custom_rows.append({ "Resource": batch["name"], "Available Amount": f"{float(batch['amount']):,.2f} {unit}", }) if not df_caps.empty or custom_rows: dfs = [] if not df_caps.empty: df_std = df_caps.copy() df_std["Type"] = "Standard" dfs.append(df_std) if custom_rows: df_custom = pd.DataFrame(custom_rows).set_index("Resource") df_custom["Type"] = "New" dfs.append(df_custom) df_all = pd.concat(dfs)[["Available Amount", "Type"]].reset_index().rename(columns={"index": "Resource"}) num_rows = len(df_all) st.dataframe( df_all, width='stretch', hide_index=True, height=(num_rows * 36 + 36), column_config={ "Resource": st.column_config.TextColumn("Resource", width="large"), "Available Amount": st.column_config.TextColumn("Available Amount"), "Type": st.column_config.TextColumn("Type"), }, row_height=36, ) else: st.warning("No resources defined yet. Set availability above to populate the summary table.", icon=":material/warning:") def render_tab(cost_sliders): """Render the Resource Inputs tab. Shows per-resource number inputs grouped by resource group, custom batch controls, an upload/download template workflow, and a summary table. Also auto-deactivates CDR methods whose primary resources are all disabled. Args: cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}. Returns: None """ # resource_caps is already fully populated (every resource_order key set) # by app.py's shared init, which runs before any tab renders. init_custom_resources() st.markdown('OBJECTIVE
', unsafe_allow_html=True) st.markdown( f'{section_intros.get("tab1_inputs", "")}
', unsafe_allow_html=True, ) with st.container(key="warning_resources"): st.warning( "Resources with zero availability will block portfolio feasibility if required by active methods.", icon=":material/warning:", ) st.markdown('Controls
', unsafe_allow_html=True) if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"): for r in st.session_state.resource_caps: st.session_state.resource_caps[r] = 0.0 # Purge costs[method][batch_name] entries left by custom resources before # clearing them below — otherwise tab4's missing-resource check wrongly # deactivates any method that relied on a batch no longer in existence. for batch in st.session_state.custom_resources: for method_name in batch.get("methods", {}): st.session_state.costs.get(method_name, {}).pop(batch["name"], None) st.session_state.custom_resources = [] st.session_state.enabled_methods = {} st.success("All resource values reset to 0.") st.rerun() st.markdown('Import resource values
', unsafe_allow_html=True) st.markdown( "You can use the template below to fill in your resource availability values "
"for a given year and import them directly into the tool."
"
"
"You can also re-import a file previously downloaded from this section.
Set available resources for one year
', unsafe_allow_html=True) resource_to_group = get_resource_to_group() for group, rlist in resource_groups.items(): unit = resource_unit_map.get(group, "") with st.expander(f"**{group}** ({unit}/yr)", expanded=True): display_rlist = [r for r in rlist if r not in hidden_resource_names] if not display_rlist: st.caption( "No standard resource to display for this group. " "Add a specific resource below to include it in the optimisation." ) cols = st.columns(4) for i, r in enumerate(display_rlist): tooltip = resource_tooltips.get(r, None) name_clean = r.split(" (")[0] with cols[i % 4]: render_resource_input(name_clean, r, tooltip=tooltip) render_related_methods_popover(r, cost_sliders, secondary_resources) # Custom batches for this group group_unit = resource_unit_map.get(group, "") group_batches = [ (idx, b) for idx, b in enumerate(st.session_state.get("custom_resources", [])) if resource_to_group.get(b["group"]) == group ] for global_idx, batch in group_batches: render_custom_batch_inline(global_idx, batch, cost_sliders, group_unit) _, col_button = st.columns([3,1]) with col_button: if st.button( "Add resource", key=f"btn-add-resource-{group}", type="secondary", icon=":material/add:", width='stretch' ): dialog_add_custom_resource(cost_sliders, preset_resources=rlist) # Refresh method activation now that this render's resource inputs and # "Related CDR Methods" toggles are final — reuses the same real-quantity # check as app.py's shared init (which runs too early to see toggles the # user just changed on this exact render) and Tab 2's own pre-render sync. cost_sliders_norm = { m: {strip_unit_suffix(name): conf for name, conf in resources.items()} for m, resources in cost_sliders.items() } refresh_active_methods(cost_sliders_norm) # Rendered after all resource inputs above (which write into # resource_caps) so the export always reflects this render's latest # values — placing it earlier would download the previous rerun's values, # since Streamlit executes top-to-bottom and the inputs update the dict # further down in the script. if any(v > 0 for v in st.session_state.resource_caps.values()): st.download_button( label="Download current resources", data=resource_caps_to_excel_bytes(st.session_state.resource_caps), file_name="current_resources.xlsx", mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", icon=":material/download:", key="download_button_current_resources", ) panel_custom_resources_controls(cost_sliders) render_resource_summary() if st.button("Method constraints →", key="btn-nav-next-inputs", type="primary", help="Configure which CDR methods are active and set their constraints."): st.session_state["_navigate_to"] = "Method constraints" st.rerun()