Spaces:
Sleeping
Sleeping
| 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_optimisation, tab5_sensitivity, | |
| tab6_scenarios) | |
| from utils.data_utils import load_slider_data, strip_unit_suffix | |
| from utils.state_utils import init_custom_resources | |
| from config.config import method_order, resource_order, secondary_resources | |
| # ── Auto-update: regenerate JSON files if inputs.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.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.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"<style>{f.read()}</style>", unsafe_allow_html=True) | |
| css_path = pathlib.Path("assets/styles.css") | |
| load_css(css_path) | |
| st.markdown('<h1 class="custom-title">CRRA <span class="crra-hl-toolkit">Toolkit</span>: CDR Portfolio Optimisation</h1>', unsafe_allow_html=True) | |
| _NAV_OPTIONS = [ | |
| "Welcome", | |
| "Resource inputs", | |
| "Method constraints", | |
| "Resource coefficients", | |
| "Portfolio generation", | |
| "Sensitivity analysis", | |
| "Scenario comparison", | |
| ] | |
| _nav_target = st.session_state.pop("_navigate_to", None) | |
| if _nav_target and _nav_target in _NAV_OPTIONS: | |
| st.session_state["_menu_default_idx"] = _NAV_OPTIONS.index(_nav_target) | |
| st.session_state["_menu_ver"] = st.session_state.get("_menu_ver", 0) + 1 | |
| st.session_state["_scroll_ver"] = st.session_state["_menu_ver"] | |
| _menu_default_idx = st.session_state.get("_menu_default_idx", 0) | |
| _menu_ver = st.session_state.get("_menu_ver", 0) | |
| with st.sidebar: | |
| selected = option_menu( | |
| "Navigation", | |
| options=_NAV_OPTIONS, | |
| icons=["house", "upload", "gear", "sliders", "graph-up-arrow", "card-checklist", "search"], | |
| default_index=_menu_default_idx, | |
| orientation="vertical", | |
| key=f"crra-menu-{_menu_ver}", | |
| 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", | |
| }, | |
| } | |
| ) | |
| # Track sidebar clicks so the next "Next" button starts from the right index | |
| if selected in _NAV_OPTIONS: | |
| st.session_state["_menu_default_idx"] = _NAV_OPTIONS.index(selected) | |
| cost_sliders = load_slider_data() | |
| # Initialise resource_caps / custom_resources / enabled_methods / auto_disabled_methods | |
| # here (not just in tab1) — the app's default landing tab is "Welcome", so a | |
| # user can reach any other tab (e.g. "Portfolio generation") without ever | |
| # rendering tab1 first. Several tabs access these via direct attribute | |
| # access (st.session_state.resource_caps, not .get()), which crashes with | |
| # an uninitialised-key AttributeError if tab1 hasn't run yet. | |
| if "resource_caps" not in st.session_state: | |
| st.session_state.resource_caps = {} | |
| for r in resource_order: | |
| if r not in st.session_state.resource_caps: | |
| st.session_state.resource_caps[r] = 0.0 | |
| init_custom_resources() | |
| # Same reasoning for "scenarios" — tab4's "Save scenario" button writes to it | |
| # directly, but it was previously only ever initialised inside tab6's render. | |
| if "scenarios" not in st.session_state: | |
| st.session_state.scenarios = {} | |
| 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: | |
| # All methods start disabled; tab2 pre-render sync auto-activates each | |
| # method once all its required resources are available (standard > 0 and | |
| # hidden resources covered by a custom batch). | |
| st.session_state.constraints[m] = { | |
| "active": False, | |
| "cap_type": "percent", | |
| "cap_value": 100, | |
| } | |
| 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 | |
| } | |
| # Auto-activate/deactivate methods based on real resource availability on | |
| # EVERY render, not only when Tab 2 is the active tab — otherwise a user who | |
| # goes straight from Tab 1 to Tab 3/4/5 without visiting Tab 2 first would | |
| # find every method stuck "inactive" and the optimisation would fail with | |
| # "No active methods" despite correctly configured resources. | |
| _cost_sliders_norm = { | |
| m: {strip_unit_suffix(name): conf for name, conf in resources.items()} | |
| for m, resources in cost_sliders.items() | |
| } | |
| tab2_constraints.refresh_active_methods(_cost_sliders_norm) | |
| # 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 | |
| _scroll_ver = st.session_state.pop("_scroll_ver", None) | |
| if _scroll_ver is not None: | |
| st.html(f"""<script> | |
| (function scroll{_scroll_ver}() {{ | |
| var doScroll = function() {{ | |
| try {{ window.parent.scrollTo(0, 0); }} catch(e) {{}} | |
| window.scrollTo(0, 0); | |
| document.documentElement.scrollTop = 0; | |
| document.body.scrollTop = 0; | |
| }}; | |
| setTimeout(doScroll, 80); | |
| }})(); | |
| </script>""") | |
| 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_optimisation.render_tab() | |
| elif selected == "Sensitivity analysis": | |
| tab5_sensitivity.render_tab() | |
| elif selected == "Scenario comparison": | |
| tab6_scenarios.render_tab() |