import hashlib import os import sys import subprocess import pathlib import streamlit as st from streamlit_option_menu import option_menu from tabs import (tab0_welcome, tab1_inputs, tab2_constraints, tab3_coefficients, tab4_optimization, tab5_sensitivity, tab6_scenarios) from utils.data_utils import load_slider_data, strip_unit_suffix from config.config import method_order, secondary_resources, hidden_resource_names # ── Auto-update: regenerate JSON files if inputs_aggregated.csv has changed ─── # Uses a hash file to detect changes — works both locally and on Hugging Face. # Must run before importing config/tabs, which read the generated JSON files. csv = "data/inputs_aggregated.csv" hash_file = "data/.csv_hash" generated = [ "data/cost_sliders.json", "data/method_groups.json", "data/secondary_resources.json", ] def md5(path): hasher = hashlib.md5() with open(path, "rb") as f: hasher.update(f.read()) return hasher.hexdigest() if os.path.exists(csv): current_hash = md5(csv) stored_hash = open(hash_file).read().strip() if os.path.exists(hash_file) else "" missing = any(not os.path.exists(f) for f in generated) if current_hash != stored_hash or missing: print("inputs_aggregated.csv has changed — running data/update.py ...") subprocess.run([sys.executable, "data/update.py"], check=True) with open(hash_file, "w") as file: file.write(current_hash) print("Data files updated successfully.") # Force all app modules to reload on next rerun so they read fresh JSONs. # config.config is imported at module level and won't re-execute unless we # evict it from sys.modules before st.rerun() re-runs the script. for _key in list(sys.modules.keys()): if any(_key.startswith(p) for p in ("config", "tabs", "utils")): del sys.modules[_key] st.cache_data.clear() st.rerun() st.set_page_config("Carbon Portfolio Optimiser", layout="wide") def load_css(file_path): with open(file_path) as f: st.markdown(f"", unsafe_allow_html=True) css_path = pathlib.Path("assets/styles.css") load_css(css_path) st.markdown('

CRRA Toolkit: CDR Portfolio Optimisation

', unsafe_allow_html=True) with st.sidebar: selected = option_menu( "Navigation", options=[ "Welcome", "Resource inputs", "Method constraints", "Resource coefficients", "Portfolio generation", "Sensitivity analysis", "Scenario comparison", ], icons=["house", "upload", "gear", "sliders", "graph-up-arrow", "card-checklist", "search"], default_index=0, orientation="vertical", key="crra-menu", styles={ "container":{ "border-radius":"0px", }, "menu-title":{ "font-family": "Segoe UI, sans-serif", "font-weight":"bold", "color": "#ff5f4d", "text-transform": "uppercase", "font-style": "italic", }, "nav-link": { "font-family": "Segoe UI, sans-serif", "margin": "4px 0", "--hover-color": "#F5F5F5", "text-transform": "uppercase", "font-weight":"600", "font-style": "italic", }, "nav-link-selected": { "background-color": "#eae9d8", "color": "#298231", "font-weight": "900", }, } ) cost_sliders = load_slider_data() if "constraints" not in st.session_state: st.session_state.constraints = {} if "auto_disabled_methods" not in st.session_state: st.session_state.auto_disabled_methods = set() for m in method_order: if m not in st.session_state.constraints: primary = [ strip_unit_suffix(r) for r in cost_sliders.get(m, {}) if (m, strip_unit_suffix(r)) not in secondary_resources ] all_hidden = bool(primary) and all(r in hidden_resource_names for r in primary) st.session_state.constraints[m] = { "active": not all_hidden, "cap_type": "percent", "cap_value": 100, } if all_hidden: st.session_state.auto_disabled_methods.add(m) _expected_methods = set(cost_sliders.keys()) _current_methods = set(st.session_state.get("costs", {}).keys()) if "costs" not in st.session_state or st.session_state.costs == {} or _expected_methods != _current_methods: st.session_state.costs = { m: { strip_unit_suffix(r): (0.0 if (m, strip_unit_suffix(r)) in secondary_resources else cost_sliders[m][r]["median"]) for r in cost_sliders[m] } for m in cost_sliders } # When the user navigates TO "Method constraints" from any other tab, force a # full widget re-sync so toggle_M / slider_M keys always reflect the constraint # state. Normal-mode setdefault can't recover keys that were externally set to # False/0; force mode overwrites them unconditionally from constraints (which # remain correct — the summary table at the bottom of tab2 always confirms # this). The flag is consumed (popped) by tab2's own pre-render sync. _prev_tab = st.session_state.get("_active_tab") if selected == "Method constraints" and _prev_tab != "Method constraints": st.session_state["constraints_just_loaded"] = True st.session_state["_active_tab"] = selected if selected == "Welcome": tab0_welcome.render_tab() elif selected == "Resource inputs": tab1_inputs.render_tab(cost_sliders) elif selected == "Method constraints": tab2_constraints.render_tab(cost_sliders) elif selected == "Resource coefficients": tab3_coefficients.render_tab(cost_sliders) elif selected == "Portfolio generation": tab4_optimization.render_tab() elif selected == "Sensitivity analysis": tab5_sensitivity.render_tab() elif selected == "Scenario comparison": tab6_scenarios.render_tab()