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()