import pandas as pd import streamlit as st from config.config import (method_groups, method_order, resource_groups, resource_order, secondary_resources, resource_unit_map, CO2_UNIT, hidden_resource_names) from utils.ui_helpers import format_efficiency_summary, show_bar from utils.data_utils import strip_unit_suffix, get_resource_to_group def normalize_costs(costs_dict: dict) -> dict: """Strip unit suffixes from resource keys for legacy scenario compatibility.""" return {strip_unit_suffix(r): v for r, v in costs_dict.items()} def get_slider_params(conf: dict) -> tuple: """Derive slider step size and display format from a coefficient config dict. Format is driven by min (display precision); step is driven by range (usability) and floored at 1e-06 (Streamlit's hard lower limit). Args: conf (dict): slider config with keys "min", "median", "max". Returns: tuple[float, str]: (step, fmt) where step is the slider increment and fmt is a printf-style format string for display. """ min_coeff = conf["min"] coeff_range = conf["max"] - min_coeff # Format: enough decimals to show the minimum value without scientific notation if min_coeff < 1e-10: fmt = "%.11f" elif min_coeff < 1e-08: fmt = "%.9f" elif min_coeff < 1e-05: fmt = "%.6f" elif min_coeff < 0.01: fmt = "%.4f" elif min_coeff < 1: fmt = "%.3f" elif min_coeff < 100: fmt = "%.2f" else: fmt = "%.0f" # Step: ~1/1000 of the range, floored at 1e-06 (Streamlit minimum) if coeff_range < 0.001: step = 1e-06 elif coeff_range < 0.1: step = 0.0001 elif coeff_range < 1: step = 0.001 elif coeff_range < 10: step = 0.01 elif coeff_range < 100: step = 0.1 else: step = 1.0 return step, fmt def is_method_enabled(method, resource, enabled_methods): """Check whether a method is currently enabled for a given resource. Args: method (str): CDR method name. resource (str): resource name. enabled_methods (dict): {resource: {method: bool}} from session state. Returns: bool: True if the method is enabled (defaults to True if not explicitly set). """ return enabled_methods.get(resource, {}).get(method, True) def is_custom_secondary(method_name, custom_r, custom_resource_to_sub_group): """Return True if the reference resource for custom_r is secondary for method_name. Args: method_name (str): CDR method name. custom_r (str): custom batch resource name. custom_resource_to_sub_group (dict): {custom_r: reference_resource}. Returns: bool """ sub_group = custom_resource_to_sub_group.get(custom_r) return sub_group is not None and (method_name, sub_group) in secondary_resources def make_sync_s_to_m(slider_key, manual_key): """on_change for slider: mirror its value into the number_input.""" def sync(): st.session_state[manual_key] = st.session_state[slider_key] return sync def make_sync_m_to_s(slider_key, manual_key, min_val, max_val): """on_change for number_input: clamp to [min, max] and sync both widgets.""" def sync(): val = st.session_state[manual_key] if val is None: return clamped = max(min_val, min(max_val, val)) st.session_state[manual_key] = clamped st.session_state[slider_key] = clamped return sync def build_custom_slider_index(cost_sliders_norm, custom_resources): """Build the per-method slider configs and group mappings for custom resource batches. Also initialises session-state cost entries for custom resources (primary at median, secondary at 0) and removes stale entries for methods no longer linked to a given batch. Args: cost_sliders_norm (dict): {method: {resource: {min, median, max}}} with unit suffixes stripped. custom_resources (list): list of custom batch dicts from session state. Returns: tuple[dict, set, dict, dict]: custom_sliders_by_method – {method: {resource: {min, median, max}}}. custom_resource_names – set of all custom batch names. custom_resource_to_sub_group – {custom_r: reference_resource}. custom_resource_to_group – {custom_r: physical_group}. """ custom_sliders_by_method = {} custom_resource_names = set() for batch in custom_resources: r_name = batch["name"] custom_resource_names.add(r_name) for m, coefs in batch["methods"].items(): custom_sliders_by_method.setdefault(m, {})[r_name] = coefs ref = batch["group"] for m, resources in cost_sliders_norm.items(): if (m, ref) in secondary_resources and r_name not in custom_sliders_by_method.get(m, {}): ref_coefs = resources.get(ref) if ref_coefs: custom_sliders_by_method.setdefault(m, {})[r_name] = ref_coefs custom_resource_to_sub_group = {batch["name"]: batch["group"] for batch in custom_resources} resource_to_group = get_resource_to_group() custom_resource_to_group = { r: resource_to_group[s_g] for r, s_g in custom_resource_to_sub_group.items() if s_g in resource_to_group } # Remove stale cost entries for methods no longer linked to a batch valid_methods_for_custom = {} for m, resources in custom_sliders_by_method.items(): for r in resources: valid_methods_for_custom.setdefault(r, set()).add(m) for r_name, valid in valid_methods_for_custom.items(): for m in list(st.session_state.get("costs", {}).keys()): if r_name in st.session_state.costs.get(m, {}) and m not in valid: del st.session_state.costs[m][r_name] # Pre-initialise cost entries for custom resources for m, resources in custom_sliders_by_method.items(): st.session_state.costs.setdefault(m, {}) for r, conf in resources.items(): if r not in st.session_state.costs[m]: st.session_state.costs[m][r] = ( 0.0 if is_custom_secondary(m, r, custom_resource_to_sub_group) else conf["median"] ) return custom_sliders_by_method, custom_resource_names, custom_resource_to_sub_group, custom_resource_to_group def render_standard_resource_row(method, r, conf, display_label, enabled_methods, has_custom_batch=False): """Render the slider row for a single standard resource coefficient. Handles the secondary-resource toggle, fixed-value display, and the slider + manual number_input pair. Writes the result to st.session_state.costs[method][r]. Args: method (str): CDR method name. r (str): resource name. conf (dict): {min, median, max} for this (method, resource) pair. display_label (str): human-readable resource label with unit. enabled_methods (dict): {resource: {method: bool}} from session state. has_custom_batch (bool): True if a custom batch derived from r exists. Returns: None """ if not is_method_enabled(method, r, enabled_methods): st.info(f"INFO: Method disabled for **{r}** (see Resource Inputs tab)") return is_secondary = (method, r) in secondary_resources step, fmt = get_slider_params(conf) toggle_key = f"toggle_{method}_{r}".replace(" ", "_") slider_key = f"slider_{method}_{r}".replace(" ", "_") manual_key = f"manual_{method}_{r}".replace(" ", "_") current_val = normalize_costs(st.session_state.costs.get(method, {})).get(r, 0.0) if is_secondary: if toggle_key not in st.session_state: st.session_state[toggle_key] = current_val != 0.0 if not st.checkbox(f"Use {display_label}", key=toggle_key): st.session_state.costs[method][r] = 0.0 st.markdown("**DISABLED**") return elif current_val == 0.0: st.session_state.costs[method][r] = conf["median"] current_val = conf["median"] if conf["min"] == conf["max"]: st.markdown( f'

{display_label} = {conf["median"]} (fixed)

', unsafe_allow_html=True, ) st.session_state.costs[method][r] = conf["median"] if st.session_state.get("resource_caps", {}).get(r, 0) <= 0: if has_custom_batch: st.warning( f"No available quantity set for **{r}** in Resource Inputs tab.", icon=":material/warning:", ) else: st.warning( f"No available quantity set for **{r}** in Resource Inputs tab: " f"**{method}** won't be used in the optimization.", icon=":material/warning:", ) return if slider_key not in st.session_state: st.session_state[slider_key] = current_val if manual_key not in st.session_state: st.session_state[manual_key] = st.session_state[slider_key] if st.session_state.get("resource_caps", {}).get(r, 0) <= 0: if has_custom_batch: st.warning( f"No available quantity set for **{r}** in Resource Inputs tab.", icon=":material/warning:", ) else: st.warning( f"No available quantity set for **{r}** in Resource Inputs tab: " f"**{method}** won't be used in the optimization.", icon=":material/warning:", ) col1, col2 = st.columns([3, 1]) with col1: slider_val = st.slider( label=display_label, min_value=conf["min"], max_value=conf["max"], step=step, format=fmt, key=slider_key, on_change=make_sync_s_to_m(slider_key, manual_key) ) with col2: st.number_input( label="label", label_visibility="collapsed", key=manual_key, help="Manual override (optional, any value)", format=fmt, value=None, placeholder=str(round(slider_val, 6)), on_change=make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]), ) manual_value = st.session_state.get(manual_key) st.session_state.costs[method][r] = slider_val if manual_value is None else manual_value def render_custom_resource_row(method, r, conf, enabled_methods, custom_resource_to_sub_group, custom_resources): """Render the slider row for a single custom resource batch coefficient. Args: method (str): CDR method name. r (str): custom resource batch name. conf (dict): {min, median, max} for this (method, resource) pair. enabled_methods (dict): {resource: {method: bool}} from session state. custom_resource_to_sub_group (dict): {custom_r: reference_resource}. custom_resources (list): list of custom batch dicts from session state. Returns: None """ if not is_method_enabled(method, r, enabled_methods): st.info("INFO: Method disabled for this resource (see Resource Inputs tab)") return is_sec = is_custom_secondary(method, r, custom_resource_to_sub_group) step, fmt = get_slider_params(conf) toggle_key = f"toggle_{method}_{r}".replace(" ", "_") slider_key = f"slider_{method}_{r}".replace(" ", "_") manual_key = f"manual_{method}_{r}".replace(" ", "_") current_val = st.session_state.costs.get(method, {}).get(r, 0.0 if is_sec else conf["median"]) if is_sec: if toggle_key not in st.session_state: st.session_state[toggle_key] = current_val != 0.0 if not st.checkbox(f"Use {r}", key=toggle_key): st.session_state.costs.setdefault(method, {})[r] = 0.0 st.markdown("**DISABLED**") return elif current_val == 0.0: st.session_state.costs.setdefault(method, {})[r] = conf["median"] current_val = conf["median"] if conf["min"] == conf["max"]: st.markdown( f'

{r} = {conf["median"]} (fixed)

', unsafe_allow_html=True, ) st.session_state.costs.setdefault(method, {})[r] = conf["median"] return if slider_key not in st.session_state: st.session_state[slider_key] = current_val if manual_key not in st.session_state: st.session_state[manual_key] = st.session_state[slider_key] batch_amount = next((float(b.get("amount", 0)) for b in custom_resources if b["name"] == r), 0.0) if batch_amount <= 0: if is_sec: st.info( f"No available quantity set for **{r}** in Resource Inputs.", icon=":material/info:", ) else: st.info( f"No available quantity set for **{r}** in Resource Inputs — " "this method won't be used in the optimization.", icon=":material/info:", ) col1, col2 = st.columns([3, 1]) with col1: slider_val = st.slider( label=r, min_value=conf["min"], max_value=conf["max"], step=step, format=fmt, key=slider_key, on_change=make_sync_s_to_m(slider_key, manual_key), ) with col2: st.number_input( label="label", label_visibility="collapsed", key=manual_key, help="Manual override (optional, any value)", format=fmt, value=None, placeholder=str(round(slider_val, 6)), on_change=make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]), ) st.session_state.costs.setdefault(method, {}) manual_value = st.session_state.get(manual_key) st.session_state.costs[method][r] = slider_val if manual_value is None else manual_value def panel_method_editor( method, cost_sliders_norm, custom_sliders_by_method, resource_display, enabled_methods, custom_resources, custom_resource_to_group, custom_resource_to_sub_group, ): """Render the coefficient-editing expander for one CDR method. Shows one slider row per standard resource, then one per custom resource batch, grouped by resource group. Includes a per-method reset button. Args: method (str): CDR method name. cost_sliders_norm (dict): {method: {resource: {min, median, max}}}. custom_sliders_by_method (dict): {method: {resource: {min, median, max}}}. resource_display (dict): {resource: label_with_unit}. enabled_methods (dict): {resource: {method: bool}} from session state. custom_resources (list): list of custom batch dicts from session state. custom_resource_to_group (dict): {custom_r: physical_group}. custom_resource_to_sub_group (dict): {custom_r: reference_resource}. Returns: None """ with st.expander(f"Manage resources for **{method}**", expanded=True): method_sliders = cost_sliders_norm.get(method, {}) custom_for_method = custom_sliders_by_method.get(method, {}) primary_resources = [r for r in method_sliders if (method, r) not in secondary_resources] if primary_resources and all(r in hidden_resource_names for r in primary_resources) and not custom_for_method: required = [r for r in primary_resources if r in hidden_resource_names] optional = [r for r in method_sliders if r in hidden_resource_names and (method, r) in secondary_resources] req_list = "\n".join(f"- **{r}** *(required)*" for r in required) opt_list = "\n".join(f"- **{r}** *(optional)*" for r in optional) if optional else "" body = f"To use this method, add a new resource in the **Resource inputs** tab:\n\n{req_list}" if opt_list: body += f"\n{opt_list}" st.info(body, icon=":material/info:") return for group_name, rlist in resource_groups.items(): used = [r for r in rlist if r in method_sliders and r not in hidden_resource_names] custom_in_group = [ r for r in custom_for_method if custom_resource_to_group.get(r) == group_name ] hidden_secondary_in_group = [ r for r in rlist if r in method_sliders and r in hidden_resource_names and (method, r) in secondary_resources and not any(custom_resource_to_sub_group.get(cr) == r for cr in custom_for_method) ] if not used and not custom_in_group and not hidden_secondary_in_group: continue st.markdown(f'

{group_name}

', unsafe_allow_html=True) resources_with_batches = set(custom_resource_to_sub_group.values()) for r in used: conf = method_sliders[r] render_standard_resource_row( method, r, conf, resource_display.get(r, r), enabled_methods, has_custom_batch=(r in resources_with_batches), ) for r in custom_in_group: render_custom_resource_row( method, r, custom_for_method[r], enabled_methods, custom_resource_to_sub_group, custom_resources ) for r in hidden_secondary_in_group: st.info( f"**{r}** can be used as an optional secondary resource for this method. " f"Add a custom resource of type **{r}** in the **Resource inputs** tab to activate it.", icon=":material/add_circle:", ) if st.button("Reset selected method", key="btn-reset-method", icon=":material/reset_settings:"): for r, conf in cost_sliders_norm.get(method, {}).items(): is_secondary = (method, r) in secondary_resources st.session_state.costs[method][r] = 0.0 if is_secondary else conf["median"] for prefix in ["slider", "manual", "toggle"]: st.session_state.pop(f"{prefix}_{method}_{r}".replace(" ", "_"), None) for r, conf in custom_sliders_by_method.get(method, {}).items(): st.session_state.costs.setdefault(method, {})[r] = conf["median"] for prefix in ["slider", "manual"]: st.session_state.pop(f"{prefix}_{method}_{r}".replace(" ", "_"), None) st.success(f"{method} reset to default values.") st.rerun() def panel_summary_table( cost_sliders_norm, custom_sliders_by_method, enabled_methods, custom_resource_names, custom_resource_to_sub_group, ): """Render the full coefficient summary table with cells highlighted when modified. Args: cost_sliders_norm (dict): {method: {resource: {min, median, max}}}. custom_sliders_by_method (dict): {method: {resource: {min, median, max}}}. enabled_methods (dict): {resource: {method: bool}} from session state. custom_resource_names (set[str]): names of all custom resource batches. custom_resource_to_sub_group (dict): {custom_r: reference_resource}. Returns: None """ st.markdown( '

Summary of resource usage coefficients

', unsafe_allow_html=True, ) resource_display = { r: f"{r} ({resource_unit_map[cat]}/{CO2_UNIT})" for cat, resources in resource_groups.items() for r in resources } all_resource_columns = [r for r in resource_order if r not in hidden_resource_names] + sorted(custom_resource_names) df_default = pd.DataFrame(index=method_order, columns=all_resource_columns) df_current = pd.DataFrame(index=method_order, columns=all_resource_columns) for m in method_order: current_costs = normalize_costs(st.session_state.costs.get(m, {})) sliders = dict(cost_sliders_norm.get(m, {})) sliders.update(custom_sliders_by_method.get(m, {})) for r in all_resource_columns: is_sec = (m, r) in secondary_resources or is_custom_secondary(m, r, custom_resource_to_sub_group) disabled = not is_method_enabled(m, r, enabled_methods) current_val = 0.0 if disabled else current_costs.get(r, 0.0) default_val = 0.0 if disabled else sliders.get(r, {}).get("median", 0.0) if is_sec and current_val == 0.0: default_val = 0.0 df_default.at[m, r] = default_val df_current.at[m, r] = current_val df_default = df_default.astype(float).fillna(0.0) df_current = df_current.astype(float).fillna(0.0) resource_display_full = dict(resource_display) resource_display_full.update({r: r for r in custom_resource_names}) df_default = df_default.rename(columns=resource_display_full) df_current = df_current.rename(columns=resource_display_full) styled = format_efficiency_summary(df_current, df_default) st.dataframe( data=styled, height=(len(df_current) * 36 + 36), width='stretch', column_config={"_index": st.column_config.TextColumn("Method", width="large")}, row_height=36 ) def render_tab(cost_sliders): """Render the Resource Efficiency Coefficients tab. Args: cost_sliders (dict): full cost slider config {method: {resource: {min, median, max}}}. Returns: None """ st.markdown('

Resource efficiency coefficients

', unsafe_allow_html=True) show_bar() st.markdown('

OBJECTIVE

', unsafe_allow_html=True) st.markdown( '

' "This section lets you adjust the resource usage coefficients for each CDR method " "to best fit your country conditions.
" "

  • Primary resources are always active and initialized with default values.
  • " "
  • Secondary resources are initialized at zero and toggled off by default.
  • " "
  • Toggling on a secondary resource restores its default value.
  • " "

    ", unsafe_allow_html=True, ) with st.container(key="warning_resources"): st.info( "Red cells in the table highlight coefficients modified from their **default median values**", icon=":material/info:", ) enabled_methods = st.session_state.get("enabled_methods", {}) custom_resources = st.session_state.get("custom_resources", []) cost_sliders_norm = {m: normalize_costs(resources) for m, resources in cost_sliders.items()} resource_display = { r: f"{r} ({resource_unit_map[cat]}/{CO2_UNIT})" for cat, resources in resource_groups.items() for r in resources } ( custom_sliders_by_method, custom_resource_names, custom_resource_to_sub_group, custom_resource_to_group, ) = build_custom_slider_index(cost_sliders_norm, custom_resources) constraints = st.session_state.get("constraints", {}) active_methods = {m for m, c in constraints.items() if c.get("active", True)} method_choices = [ f"{g} | {m}" for g, methods in method_groups.items() for m in methods if not constraints or m in active_methods ] if not method_choices: st.warning("No active methods. Enable at least one method in the **Method Constraints** tab.") return method_lookup = {label: label.split(" | ", 1)[1] for label in method_choices} selected_label = st.selectbox("Select a method:", method_choices, key="crra-select") if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"): for m, sliders in cost_sliders_norm.items(): for r, conf in sliders.items(): st.session_state.costs[m][r] = 0.0 if (m, r) in secondary_resources else conf["median"] for prefix in ["slider", "manual", "toggle"]: st.session_state.pop(f"{prefix}_{m}_{r}".replace(" ", "_"), None) for m, sliders in custom_sliders_by_method.items(): for r, conf in sliders.items(): st.session_state.costs.setdefault(m, {})[r] = ( 0.0 if is_custom_secondary(m, r, custom_resource_to_sub_group) else conf["median"] ) for prefix in ["slider", "manual", "toggle"]: st.session_state.pop(f"{prefix}_{m}_{r}".replace(" ", "_"), None) st.success("All methods reset to default values.") st.rerun() panel_method_editor( method=method_lookup[selected_label], cost_sliders_norm=cost_sliders_norm, custom_sliders_by_method=custom_sliders_by_method, resource_display=resource_display, enabled_methods=enabled_methods, custom_resources=custom_resources, custom_resource_to_group=custom_resource_to_group, custom_resource_to_sub_group=custom_resource_to_sub_group, ) panel_summary_table( cost_sliders_norm=cost_sliders_norm, custom_sliders_by_method=custom_sliders_by_method, enabled_methods=enabled_methods, custom_resource_names=custom_resource_names, custom_resource_to_sub_group=custom_resource_to_sub_group, )