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