CarlyneB's picture
[UX] British spelling: rename optimization → optimisation throughout code (conflict resolve)
7a80c0b
Raw
History Blame Contribute Delete
8.69 kB
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()