Spaces:
Sleeping
Sleeping
File size: 8,692 Bytes
fa58ff0 e5f540d 7a80c0b e5f540d 6b18587 fa58ff0 60dd41c fa58ff0 60dd41c 4885a3b fa58ff0 4885a3b fa58ff0 4885a3b fa58ff0 4885a3b 60dd41c fa58ff0 4885a3b fa58ff0 01c98b2 fa58ff0 90b7366 fa58ff0 90b7366 fa58ff0 20f0fcc fa58ff0 20f0fcc fa58ff0 20f0fcc fa58ff0 20f0fcc fa58ff0 20f0fcc fa58ff0 be0befa 6b18587 fa58ff0 be0befa fa58ff0 393404f be0befa 393404f be0befa 393404f 01c98b2 fa58ff0 6b18587 be0befa 20f0fcc fa58ff0 be0befa fa58ff0 7a80c0b fa58ff0 ba2dcff | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | 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() |