Spaces:
Sleeping
Sleeping
File size: 6,221 Bytes
fa58ff0 e5f540d 0fbf139 e5f540d be0befa fa58ff0 4885a3b fa58ff0 4885a3b fa58ff0 4885a3b fa58ff0 4885a3b fa58ff0 4885a3b fa58ff0 01c98b2 fa58ff0 be0befa fa58ff0 be0befa fa58ff0 be0befa 01c98b2 fa58ff0 be0befa fa58ff0 be0befa 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 | 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 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 Optimizer", 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</span>: CDR Portfolio Optimization</h1>', 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() |