Spaces:
Sleeping
Sleeping
[FEAT] Objective 3 – Additional data: amount of CO₂ (MtCO₂) removed if one unit of each resource is added
Browse files- optimization/optimization.py +36 -7
- tabs/tab1_inputs.py +4 -4
- tabs/tab2_constraints.py +15 -2
- tabs/tab3_coefficients.py +39 -9
- tabs/tab4_optimization.py +108 -15
- tabs/tab6_scenarios.py +1 -1
optimization/optimization.py
CHANGED
|
@@ -117,7 +117,9 @@ def run_optimization(
|
|
| 117 |
resources_no_batches = [r for r in standard_resources if r not in ref_to_batches]
|
| 118 |
|
| 119 |
# ── Build LP constraint matrix ────────────────────────────────────────────
|
| 120 |
-
|
|
|
|
|
|
|
| 121 |
|
| 122 |
# (A) Standard resources with NO custom batch substitutes — unchanged rows
|
| 123 |
for r in resources_no_batches:
|
|
@@ -127,6 +129,7 @@ def run_optimization(
|
|
| 127 |
if np.any(row[:M] != 0):
|
| 128 |
A_rows.append(row)
|
| 129 |
b_rows.append(resource_caps[r])
|
|
|
|
| 130 |
|
| 131 |
# (B) Standard resources that DO have custom batch substitutes
|
| 132 |
# Constraint: Σ_m c_s[m] * (y_m - Σ_n z_{m,n}) ≤ Q_s
|
|
@@ -141,6 +144,7 @@ def run_optimization(
|
|
| 141 |
if np.any(row != 0):
|
| 142 |
A_rows.append(row)
|
| 143 |
b_rows.append(resource_caps[r])
|
|
|
|
| 144 |
|
| 145 |
# (C) Custom batch capacity constraints: Σ_m c_n[m] * z_{m,n} ≤ Q_n
|
| 146 |
for bname, info in custom_batch_info.items():
|
|
@@ -156,6 +160,7 @@ def run_optimization(
|
|
| 156 |
if np.any(row != 0):
|
| 157 |
A_rows.append(row)
|
| 158 |
b_rows.append(resource_caps.get(bname, 0.0))
|
|
|
|
| 159 |
|
| 160 |
# (D1) Coupling lower bound: Σ_{n: ref(n)=r} z_{m,n} ≤ y_m
|
| 161 |
# Keeps standard resource usage non-negative.
|
|
@@ -181,6 +186,7 @@ def run_optimization(
|
|
| 181 |
row[col] = 1.0
|
| 182 |
A_rows.append(row)
|
| 183 |
b_rows.append(0.0)
|
|
|
|
| 184 |
|
| 185 |
# D2 (only when c_s = 0): y_m ≤ Σ z → y_m - Σ z ≤ 0
|
| 186 |
if c_s_m == 0:
|
|
@@ -190,6 +196,7 @@ def run_optimization(
|
|
| 190 |
row2[col] = -1.0
|
| 191 |
A_rows.append(row2)
|
| 192 |
b_rows.append(0.0)
|
|
|
|
| 193 |
|
| 194 |
# (E) Method cap constraints (on y_m)
|
| 195 |
for j, m in enumerate(methods):
|
|
@@ -201,6 +208,7 @@ def run_optimization(
|
|
| 201 |
row[j] = 1.0
|
| 202 |
A_rows.append(row)
|
| 203 |
b_rows.append(cap_value)
|
|
|
|
| 204 |
else:
|
| 205 |
cap_ratio = cap_value / 100.0
|
| 206 |
row = np.zeros(N)
|
|
@@ -208,6 +216,7 @@ def run_optimization(
|
|
| 208 |
row[j] += 1.0
|
| 209 |
A_rows.append(row)
|
| 210 |
b_rows.append(0.0)
|
|
|
|
| 211 |
|
| 212 |
A_ub = np.array(A_rows) if A_rows else np.empty((0, N))
|
| 213 |
b_ub = np.array(b_rows)
|
|
@@ -276,14 +285,34 @@ def run_optimization(
|
|
| 276 |
**{bn: info["amount"] for bn, info in custom_batch_info.items()}}
|
| 277 |
resource_remaining = {r: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
|
| 278 |
for r in all_caps}
|
| 279 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 280 |
|
| 281 |
return True, {
|
| 282 |
-
"total_removed":
|
| 283 |
-
"method_usage":
|
| 284 |
-
"resource_usage":
|
| 285 |
-
"resource_used_total":resource_used_total,
|
| 286 |
-
"resource_remaining":
|
|
|
|
| 287 |
}
|
| 288 |
|
| 289 |
except Exception as e:
|
|
|
|
| 117 |
resources_no_batches = [r for r in standard_resources if r not in ref_to_batches]
|
| 118 |
|
| 119 |
# ── Build LP constraint matrix ────────────────────────────────────────────
|
| 120 |
+
# constraint_labels tracks (type, identifier) for each row added to A_rows,
|
| 121 |
+
# enabling shadow-price extraction after the solve.
|
| 122 |
+
A_rows, b_rows, constraint_labels = [], [], []
|
| 123 |
|
| 124 |
# (A) Standard resources with NO custom batch substitutes — unchanged rows
|
| 125 |
for r in resources_no_batches:
|
|
|
|
| 129 |
if np.any(row[:M] != 0):
|
| 130 |
A_rows.append(row)
|
| 131 |
b_rows.append(resource_caps[r])
|
| 132 |
+
constraint_labels.append(("resource", r))
|
| 133 |
|
| 134 |
# (B) Standard resources that DO have custom batch substitutes
|
| 135 |
# Constraint: Σ_m c_s[m] * (y_m - Σ_n z_{m,n}) ≤ Q_s
|
|
|
|
| 144 |
if np.any(row != 0):
|
| 145 |
A_rows.append(row)
|
| 146 |
b_rows.append(resource_caps[r])
|
| 147 |
+
constraint_labels.append(("resource", r))
|
| 148 |
|
| 149 |
# (C) Custom batch capacity constraints: Σ_m c_n[m] * z_{m,n} ≤ Q_n
|
| 150 |
for bname, info in custom_batch_info.items():
|
|
|
|
| 160 |
if np.any(row != 0):
|
| 161 |
A_rows.append(row)
|
| 162 |
b_rows.append(resource_caps.get(bname, 0.0))
|
| 163 |
+
constraint_labels.append(("custom_batch", bname))
|
| 164 |
|
| 165 |
# (D1) Coupling lower bound: Σ_{n: ref(n)=r} z_{m,n} ≤ y_m
|
| 166 |
# Keeps standard resource usage non-negative.
|
|
|
|
| 186 |
row[col] = 1.0
|
| 187 |
A_rows.append(row)
|
| 188 |
b_rows.append(0.0)
|
| 189 |
+
constraint_labels.append(("coupling_D1", (m, ref)))
|
| 190 |
|
| 191 |
# D2 (only when c_s = 0): y_m ≤ Σ z → y_m - Σ z ≤ 0
|
| 192 |
if c_s_m == 0:
|
|
|
|
| 196 |
row2[col] = -1.0
|
| 197 |
A_rows.append(row2)
|
| 198 |
b_rows.append(0.0)
|
| 199 |
+
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 200 |
|
| 201 |
# (E) Method cap constraints (on y_m)
|
| 202 |
for j, m in enumerate(methods):
|
|
|
|
| 208 |
row[j] = 1.0
|
| 209 |
A_rows.append(row)
|
| 210 |
b_rows.append(cap_value)
|
| 211 |
+
constraint_labels.append(("method_cap", m))
|
| 212 |
else:
|
| 213 |
cap_ratio = cap_value / 100.0
|
| 214 |
row = np.zeros(N)
|
|
|
|
| 216 |
row[j] += 1.0
|
| 217 |
A_rows.append(row)
|
| 218 |
b_rows.append(0.0)
|
| 219 |
+
constraint_labels.append(("method_cap", m))
|
| 220 |
|
| 221 |
A_ub = np.array(A_rows) if A_rows else np.empty((0, N))
|
| 222 |
b_ub = np.array(b_rows)
|
|
|
|
| 285 |
**{bn: info["amount"] for bn, info in custom_batch_info.items()}}
|
| 286 |
resource_remaining = {r: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
|
| 287 |
for r in all_caps}
|
| 288 |
+
|
| 289 |
+
# ── Actual CDR gain per +1 unit (perturbation analysis) ──────────────
|
| 290 |
+
# For every resource, re-solve the LP with b_ub[i] += 1 (same constraint
|
| 291 |
+
# matrix, only the RHS changes) and measure the real gain. This correctly
|
| 292 |
+
# accounts for all simultaneously binding constraints.
|
| 293 |
+
resource_actual_gain = {}
|
| 294 |
+
for i, (ctype, cid) in enumerate(constraint_labels):
|
| 295 |
+
if ctype not in ("resource", "custom_batch"):
|
| 296 |
+
continue
|
| 297 |
+
b_perturbed = b_ub.copy()
|
| 298 |
+
b_perturbed[i] += 1.0
|
| 299 |
+
try:
|
| 300 |
+
perturbed = linprog(
|
| 301 |
+
c_obj, A_ub=A_ub, b_ub=b_perturbed, bounds=bounds, method="highs"
|
| 302 |
+
)
|
| 303 |
+
if perturbed.success:
|
| 304 |
+
new_total = float(perturbed.x[:M].sum())
|
| 305 |
+
resource_actual_gain[cid] = max(new_total - total_removed, 0.0)
|
| 306 |
+
except Exception:
|
| 307 |
+
pass
|
| 308 |
|
| 309 |
return True, {
|
| 310 |
+
"total_removed": total_removed,
|
| 311 |
+
"method_usage": method_usage,
|
| 312 |
+
"resource_usage": resource_usage,
|
| 313 |
+
"resource_used_total": resource_used_total,
|
| 314 |
+
"resource_remaining": resource_remaining,
|
| 315 |
+
"resource_actual_gain": resource_actual_gain,
|
| 316 |
}
|
| 317 |
|
| 318 |
except Exception as e:
|
tabs/tab1_inputs.py
CHANGED
|
@@ -2,9 +2,9 @@ import pandas as pd
|
|
| 2 |
import streamlit as st
|
| 3 |
|
| 4 |
from config.config import (resource_groups, resource_order, resource_tooltips,
|
| 5 |
-
resource_unit_map, method_groups,
|
| 6 |
from utils.data_utils import render_resource_input, strip_unit_suffix
|
| 7 |
-
from utils.ui_helpers import format_resource_summary,
|
| 8 |
from io import BytesIO
|
| 9 |
|
| 10 |
# ─── Custom resource helpers ──────────────────────────────────────────────────
|
|
@@ -588,14 +588,14 @@ def render_tab(cost_sliders):
|
|
| 588 |
unit = resource_unit_map.get(group, "")
|
| 589 |
with st.expander(f"**{group}** ({unit})", expanded=True):
|
| 590 |
|
| 591 |
-
cols = st.columns(
|
| 592 |
|
| 593 |
for i, r in enumerate(rlist):
|
| 594 |
|
| 595 |
tooltip = resource_tooltips.get(r, None)
|
| 596 |
name_clean = r.split(" (")[0]
|
| 597 |
|
| 598 |
-
with cols[i %
|
| 599 |
|
| 600 |
render_resource_input(name_clean, r, tooltip=tooltip)
|
| 601 |
|
|
|
|
| 2 |
import streamlit as st
|
| 3 |
|
| 4 |
from config.config import (resource_groups, resource_order, resource_tooltips,
|
| 5 |
+
resource_unit_map, method_groups, CO2_UNIT, secondary_resources)
|
| 6 |
from utils.data_utils import render_resource_input, strip_unit_suffix
|
| 7 |
+
from utils.ui_helpers import format_resource_summary, show_bar
|
| 8 |
from io import BytesIO
|
| 9 |
|
| 10 |
# ─── Custom resource helpers ──────────────────────────────────────────────────
|
|
|
|
| 588 |
unit = resource_unit_map.get(group, "")
|
| 589 |
with st.expander(f"**{group}** ({unit})", expanded=True):
|
| 590 |
|
| 591 |
+
cols = st.columns(4)
|
| 592 |
|
| 593 |
for i, r in enumerate(rlist):
|
| 594 |
|
| 595 |
tooltip = resource_tooltips.get(r, None)
|
| 596 |
name_clean = r.split(" (")[0]
|
| 597 |
|
| 598 |
+
with cols[i % 4]:
|
| 599 |
|
| 600 |
render_resource_input(name_clean, r, tooltip=tooltip)
|
| 601 |
|
tabs/tab2_constraints.py
CHANGED
|
@@ -4,7 +4,7 @@ import pandas as pd
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips
|
| 7 |
-
from utils.ui_helpers import
|
| 8 |
|
| 9 |
|
| 10 |
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -60,6 +60,7 @@ def render_method(m, cols, i):
|
|
| 60 |
format_func=lambda x: "% of total potential"
|
| 61 |
if x == "percent"
|
| 62 |
else f"Absolute ({CO2_UNIT}/yr)",
|
|
|
|
| 63 |
)
|
| 64 |
|
| 65 |
st.session_state.constraints[m]["cap_type"] = cap_type
|
|
@@ -167,6 +168,18 @@ def render_tab():
|
|
| 167 |
)
|
| 168 |
df = build_summary_df()
|
| 169 |
if df is not None:
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
else:
|
| 172 |
st.warning("No active methods selected.")
|
|
|
|
| 4 |
import streamlit as st
|
| 5 |
|
| 6 |
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips
|
| 7 |
+
from utils.ui_helpers import show_bar
|
| 8 |
|
| 9 |
|
| 10 |
# ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
| 60 |
format_func=lambda x: "% of total potential"
|
| 61 |
if x == "percent"
|
| 62 |
else f"Absolute ({CO2_UNIT}/yr)",
|
| 63 |
+
label_visibility="collapsed",
|
| 64 |
)
|
| 65 |
|
| 66 |
st.session_state.constraints[m]["cap_type"] = cap_type
|
|
|
|
| 168 |
)
|
| 169 |
df = build_summary_df()
|
| 170 |
if df is not None:
|
| 171 |
+
df_display = df.reset_index().rename(columns={"index": "CDR Method"})
|
| 172 |
+
df_display = df_display[["CDR Method", "Cap value"]]
|
| 173 |
+
num_rows = len(df_display)
|
| 174 |
+
st.dataframe(
|
| 175 |
+
df_display,
|
| 176 |
+
use_container_width=True,
|
| 177 |
+
hide_index=True,
|
| 178 |
+
height=(num_rows * 36 + 10),
|
| 179 |
+
column_config={
|
| 180 |
+
"CDR Method": st.column_config.TextColumn("CDR Method", width="large"),
|
| 181 |
+
"Cap value": st.column_config.TextColumn("Cap value"),
|
| 182 |
+
},
|
| 183 |
+
)
|
| 184 |
else:
|
| 185 |
st.warning("No active methods selected.")
|
tabs/tab3_coefficients.py
CHANGED
|
@@ -25,8 +25,22 @@ def _get_slider_params(conf: dict) -> tuple:
|
|
| 25 |
def is_method_enabled(method, resource, enabled_methods):
|
| 26 |
return enabled_methods.get(resource, {}).get(method, True)
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
# Intentionally a no-op: disabling a method for a resource must NOT permanently
|
| 31 |
# zero the stored coefficient in session state, because re-enabling would then
|
| 32 |
# see an existing key at 0 and the pre-init would skip restoration.
|
|
@@ -73,7 +87,7 @@ def render_tab(cost_sliders):
|
|
| 73 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 74 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 75 |
|
| 76 |
-
_enforce_disabled_coefficients(
|
| 77 |
|
| 78 |
# Normalize cost_sliders once: strip unit suffixes from resource keys
|
| 79 |
# cost_sliders_norm[method][resource_name] = {min, median, max}
|
|
@@ -231,6 +245,11 @@ def render_tab(cost_sliders):
|
|
| 231 |
|
| 232 |
if slider_key not in st.session_state:
|
| 233 |
st.session_state[slider_key] = current_val
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
|
| 235 |
col1, col2 = st.columns([3, 1])
|
| 236 |
with col1:
|
|
@@ -241,18 +260,20 @@ def render_tab(cost_sliders):
|
|
| 241 |
step=step,
|
| 242 |
format=fmt,
|
| 243 |
key=slider_key,
|
|
|
|
| 244 |
)
|
| 245 |
with col2:
|
| 246 |
manual_val = st.number_input(
|
| 247 |
label="label",
|
| 248 |
label_visibility="collapsed",
|
| 249 |
-
value=slider_val,
|
| 250 |
key=manual_key,
|
| 251 |
help="Manual override (optional, any value)",
|
| 252 |
format=fmt,
|
|
|
|
| 253 |
)
|
| 254 |
|
| 255 |
-
st.session_state.costs[method][r] =
|
|
|
|
| 256 |
|
| 257 |
# ── New (custom) resources linked to this method ────────────────
|
| 258 |
|
|
@@ -286,6 +307,12 @@ def render_tab(cost_sliders):
|
|
| 286 |
|
| 287 |
if slider_key not in st.session_state:
|
| 288 |
st.session_state[slider_key] = current_val
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 289 |
|
| 290 |
col1, col2 = st.columns([3, 1])
|
| 291 |
with col1:
|
|
@@ -296,19 +323,20 @@ def render_tab(cost_sliders):
|
|
| 296 |
step=step,
|
| 297 |
format=fmt,
|
| 298 |
key=slider_key,
|
|
|
|
| 299 |
)
|
| 300 |
with col2:
|
| 301 |
manual_val = st.number_input(
|
| 302 |
label="label",
|
| 303 |
label_visibility="collapsed",
|
| 304 |
-
value=slider_val,
|
| 305 |
key=manual_key,
|
| 306 |
help="Manual override (optional, any value)",
|
| 307 |
format=fmt,
|
|
|
|
| 308 |
)
|
| 309 |
|
| 310 |
st.session_state.costs.setdefault(method, {})
|
| 311 |
-
st.session_state.costs[method][r] =
|
| 312 |
|
| 313 |
|
| 314 |
# ── Reset buttons ────────────────────────────────────────────────────
|
|
@@ -362,12 +390,14 @@ def render_tab(cost_sliders):
|
|
| 362 |
df_current = df_current.rename(columns=resource_display_full)
|
| 363 |
|
| 364 |
styled = format_efficiency_summary(df_current, df_default)
|
|
|
|
| 365 |
st.dataframe(
|
| 366 |
data=styled,
|
|
|
|
| 367 |
use_container_width=True,
|
| 368 |
column_config={
|
| 369 |
"_index": st.column_config.TextColumn(
|
| 370 |
"Method",
|
| 371 |
-
|
| 372 |
-
)
|
| 373 |
})
|
|
|
|
| 25 |
def is_method_enabled(method, resource, enabled_methods):
|
| 26 |
return enabled_methods.get(resource, {}).get(method, True)
|
| 27 |
|
| 28 |
+
def _make_sync_s_to_m(slider_key, manual_key):
|
| 29 |
+
"""on_change for slider: mirror its value into the number_input."""
|
| 30 |
+
def _sync():
|
| 31 |
+
st.session_state[manual_key] = st.session_state[slider_key]
|
| 32 |
+
return _sync
|
| 33 |
+
|
| 34 |
+
def _make_sync_m_to_s(slider_key, manual_key, min_val, max_val):
|
| 35 |
+
"""on_change for number_input: clamp to [min, max] and sync both widgets."""
|
| 36 |
+
def _sync():
|
| 37 |
+
clamped = max(min_val, min(max_val, st.session_state[manual_key]))
|
| 38 |
+
st.session_state[manual_key] = clamped
|
| 39 |
+
st.session_state[slider_key] = clamped
|
| 40 |
+
return _sync
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def _enforce_disabled_coefficients():
|
| 44 |
# Intentionally a no-op: disabling a method for a resource must NOT permanently
|
| 45 |
# zero the stored coefficient in session state, because re-enabling would then
|
| 46 |
# see an existing key at 0 and the pre-init would skip restoration.
|
|
|
|
| 87 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 88 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 89 |
|
| 90 |
+
_enforce_disabled_coefficients()
|
| 91 |
|
| 92 |
# Normalize cost_sliders once: strip unit suffixes from resource keys
|
| 93 |
# cost_sliders_norm[method][resource_name] = {min, median, max}
|
|
|
|
| 245 |
|
| 246 |
if slider_key not in st.session_state:
|
| 247 |
st.session_state[slider_key] = current_val
|
| 248 |
+
if manual_key not in st.session_state:
|
| 249 |
+
st.session_state[manual_key] = st.session_state[slider_key]
|
| 250 |
+
|
| 251 |
+
if st.session_state.get("resource_caps", {}).get(r, 0) <= 0:
|
| 252 |
+
st.info(f"No available quantity set for **{r}** in Resource Inputs tab: **{method}** won't be used in the optimization.", icon=":material/info:")
|
| 253 |
|
| 254 |
col1, col2 = st.columns([3, 1])
|
| 255 |
with col1:
|
|
|
|
| 260 |
step=step,
|
| 261 |
format=fmt,
|
| 262 |
key=slider_key,
|
| 263 |
+
on_change=_make_sync_s_to_m(slider_key, manual_key),
|
| 264 |
)
|
| 265 |
with col2:
|
| 266 |
manual_val = st.number_input(
|
| 267 |
label="label",
|
| 268 |
label_visibility="collapsed",
|
|
|
|
| 269 |
key=manual_key,
|
| 270 |
help="Manual override (optional, any value)",
|
| 271 |
format=fmt,
|
| 272 |
+
on_change=_make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]),
|
| 273 |
)
|
| 274 |
|
| 275 |
+
st.session_state.costs[method][r] = st.session_state.get(manual_key, slider_val)
|
| 276 |
+
|
| 277 |
|
| 278 |
# ── New (custom) resources linked to this method ────────────────
|
| 279 |
|
|
|
|
| 307 |
|
| 308 |
if slider_key not in st.session_state:
|
| 309 |
st.session_state[slider_key] = current_val
|
| 310 |
+
if manual_key not in st.session_state:
|
| 311 |
+
st.session_state[manual_key] = st.session_state[slider_key]
|
| 312 |
+
|
| 313 |
+
batch_amount = next((float(b.get("amount", 0)) for b in custom_resources if b["name"] == r), 0.0)
|
| 314 |
+
if batch_amount <= 0:
|
| 315 |
+
st.info(f"No available quantity set for **{r}** in Resource Inputs — this method won't be used in the optimization.", icon=":material/info:")
|
| 316 |
|
| 317 |
col1, col2 = st.columns([3, 1])
|
| 318 |
with col1:
|
|
|
|
| 323 |
step=step,
|
| 324 |
format=fmt,
|
| 325 |
key=slider_key,
|
| 326 |
+
on_change=_make_sync_s_to_m(slider_key, manual_key),
|
| 327 |
)
|
| 328 |
with col2:
|
| 329 |
manual_val = st.number_input(
|
| 330 |
label="label",
|
| 331 |
label_visibility="collapsed",
|
|
|
|
| 332 |
key=manual_key,
|
| 333 |
help="Manual override (optional, any value)",
|
| 334 |
format=fmt,
|
| 335 |
+
on_change=_make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]),
|
| 336 |
)
|
| 337 |
|
| 338 |
st.session_state.costs.setdefault(method, {})
|
| 339 |
+
st.session_state.costs[method][r] = st.session_state.get(manual_key, slider_val)
|
| 340 |
|
| 341 |
|
| 342 |
# ── Reset buttons ────────────────────────────────────────────────────
|
|
|
|
| 390 |
df_current = df_current.rename(columns=resource_display_full)
|
| 391 |
|
| 392 |
styled = format_efficiency_summary(df_current, df_default)
|
| 393 |
+
num_rows = len(df_current)
|
| 394 |
st.dataframe(
|
| 395 |
data=styled,
|
| 396 |
+
height=(num_rows*36 + 15),
|
| 397 |
use_container_width=True,
|
| 398 |
column_config={
|
| 399 |
"_index": st.column_config.TextColumn(
|
| 400 |
"Method",
|
| 401 |
+
width="large"
|
| 402 |
+
),
|
| 403 |
})
|
tabs/tab4_optimization.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
| 1 |
from collections import defaultdict
|
|
|
|
| 2 |
import io
|
| 3 |
import json
|
| 4 |
import re
|
|
@@ -13,7 +14,7 @@ import plotly.express as px
|
|
| 13 |
|
| 14 |
from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, method_order, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
|
| 15 |
from optimization.optimization import run_optimization
|
| 16 |
-
from utils.ui_helpers import apply_chart_style,
|
| 17 |
from utils.data_utils import is_valid_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix
|
| 18 |
|
| 19 |
|
|
@@ -395,6 +396,7 @@ def run_and_store_result():
|
|
| 395 |
"resource_usage": result["resource_usage"],
|
| 396 |
"resource_used_total": result["resource_used_total"],
|
| 397 |
"resource_remaining": result["resource_remaining"],
|
|
|
|
| 398 |
"resource_caps": st.session_state.resource_caps.copy(),
|
| 399 |
"method_constraints": st.session_state.constraints,
|
| 400 |
"method_costs": st.session_state.costs,
|
|
@@ -664,6 +666,40 @@ def show_resource_balance(latest):
|
|
| 664 |
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 665 |
register_chart_data("Resource balance (used vs remaining)", df)
|
| 666 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
# ─── Save / export / reset ───────────────────────────────────────────────────────────
|
| 668 |
|
| 669 |
def save_scenario(latest):
|
|
@@ -736,9 +772,20 @@ def export_scenario():
|
|
| 736 |
zf.writestr("method_costs.csv",
|
| 737 |
pd.DataFrame.from_dict(scenario["method_costs"], orient="index").to_csv())
|
| 738 |
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
|
| 743 |
custom_batches = scenario.get("custom_resources", [])
|
| 744 |
if custom_batches:
|
|
@@ -768,22 +815,67 @@ def export_scenario():
|
|
| 768 |
|
| 769 |
resource_used_total = scenario.get("resource_used_total", {})
|
| 770 |
resource_remaining = scenario.get("resource_remaining", {})
|
|
|
|
|
|
|
|
|
|
|
|
|
| 771 |
if resource_used_total or resource_remaining:
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 777 |
|
| 778 |
resource_usage = scenario.get("resource_usage", {})
|
| 779 |
if resource_usage:
|
| 780 |
-
usage_rows = [
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
|
|
|
|
|
|
|
|
|
| 785 |
zf.writestr("resource_usage_detail.csv", pd.DataFrame(usage_rows).to_csv(index=False))
|
| 786 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 787 |
with col_csv:
|
| 788 |
st.download_button(
|
| 789 |
"Export CSV (zip)", data=buf.getvalue(),
|
|
@@ -1032,7 +1124,7 @@ def render_tab():
|
|
| 1032 |
<p class="crra-sub-sub-heading">From inputs</p>""",
|
| 1033 |
unsafe_allow_html=True,
|
| 1034 |
)
|
| 1035 |
-
run_clicked = st.button("
|
| 1036 |
panel_run_from_tabs()
|
| 1037 |
panel_load_from_excel()
|
| 1038 |
|
|
@@ -1092,6 +1184,7 @@ def render_tab():
|
|
| 1092 |
resource_usage=latest.get("resource_usage"))
|
| 1093 |
|
| 1094 |
show_resource_balance(latest)
|
|
|
|
| 1095 |
df_usage = build_resource_usage_df(latest["resource_usage"])
|
| 1096 |
|
| 1097 |
st.markdown("### Detailed resource usage by method")
|
|
|
|
| 1 |
from collections import defaultdict
|
| 2 |
+
import datetime
|
| 3 |
import io
|
| 4 |
import json
|
| 5 |
import re
|
|
|
|
| 14 |
|
| 15 |
from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, method_order, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
|
| 16 |
from optimization.optimization import run_optimization
|
| 17 |
+
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 18 |
from utils.data_utils import is_valid_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix
|
| 19 |
|
| 20 |
|
|
|
|
| 396 |
"resource_usage": result["resource_usage"],
|
| 397 |
"resource_used_total": result["resource_used_total"],
|
| 398 |
"resource_remaining": result["resource_remaining"],
|
| 399 |
+
"resource_actual_gain": result.get("resource_actual_gain", {}),
|
| 400 |
"resource_caps": st.session_state.resource_caps.copy(),
|
| 401 |
"method_constraints": st.session_state.constraints,
|
| 402 |
"method_costs": st.session_state.costs,
|
|
|
|
| 666 |
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 667 |
register_chart_data("Resource balance (used vs remaining)", df)
|
| 668 |
|
| 669 |
+
def show_bottleneck_analysis(latest):
|
| 670 |
+
st.markdown("### Resource sensitivity")
|
| 671 |
+
st.markdown(
|
| 672 |
+
'<p class="caption">'
|
| 673 |
+
'For every resource: actual CDR gain if you add exactly +1 unit, recomputed with all other constraints in place.'
|
| 674 |
+
'</p>',
|
| 675 |
+
unsafe_allow_html=True,
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
+
resource_to_group = {
|
| 679 |
+
r: group for group, resources in resource_groups.items() for r in resources
|
| 680 |
+
}
|
| 681 |
+
custom_batch_lookup = {b["name"]: b for b in latest.get("custom_resources", [])}
|
| 682 |
+
|
| 683 |
+
actual_gains = latest.get("resource_actual_gain", {})
|
| 684 |
+
if actual_gains:
|
| 685 |
+
sp_rows = []
|
| 686 |
+
for r, gain in sorted(actual_gains.items(), key=lambda x: -x[1]):
|
| 687 |
+
if r in custom_batch_lookup:
|
| 688 |
+
unit = resource_unit_map.get(resource_to_group.get(custom_batch_lookup[r]["group"]), "?")
|
| 689 |
+
else:
|
| 690 |
+
unit = resource_unit_map.get(resource_to_group.get(r), "?")
|
| 691 |
+
sp_rows.append({
|
| 692 |
+
"Resource": r,
|
| 693 |
+
"Unit": unit,
|
| 694 |
+
f"CDR gain for +1 unit ({CO2_UNIT})": round(gain, 4),
|
| 695 |
+
})
|
| 696 |
+
df_sp = pd.DataFrame(sp_rows)
|
| 697 |
+
with st.expander("📊 View / download resource sensitivity", expanded=True):
|
| 698 |
+
st.dataframe(df_sp, use_container_width=True, hide_index=True)
|
| 699 |
+
register_chart_data("Resource sensitivity (CDR gain per +1 unit)", df_sp)
|
| 700 |
+
else:
|
| 701 |
+
st.info("No resources to analyse.")
|
| 702 |
+
|
| 703 |
# ─── Save / export / reset ───────────────────────────────────────────────────────────
|
| 704 |
|
| 705 |
def save_scenario(latest):
|
|
|
|
| 772 |
zf.writestr("method_costs.csv",
|
| 773 |
pd.DataFrame.from_dict(scenario["method_costs"], orient="index").to_csv())
|
| 774 |
|
| 775 |
+
alloc_df = pd.DataFrame.from_dict(scenario["allocations"], orient="index",
|
| 776 |
+
columns=[f"{CO2_UNIT} removed"])
|
| 777 |
+
alloc_df[f"{CO2_UNIT} removed"] = pd.to_numeric(alloc_df[f"{CO2_UNIT} removed"], errors="coerce")
|
| 778 |
+
alloc_total = alloc_df[f"{CO2_UNIT} removed"].sum()
|
| 779 |
+
if alloc_total > 0:
|
| 780 |
+
alloc_df["Total share (%)"] = (alloc_df[f"{CO2_UNIT} removed"] / alloc_total * 100).round(2)
|
| 781 |
+
_method_constraints = scenario.get("method_constraints", {})
|
| 782 |
+
alloc_df["Cap constraint"] = pd.Series({
|
| 783 |
+
m: (f"{c['cap_value']:,.2f} {CO2_UNIT}/yr" if c.get("cap_type") == "absolute"
|
| 784 |
+
else f"{int(c.get('cap_value', 100))}% of potential")
|
| 785 |
+
for m, c in _method_constraints.items()
|
| 786 |
+
})
|
| 787 |
+
alloc_df = alloc_df.sort_values(f"{CO2_UNIT} removed", ascending=False)
|
| 788 |
+
zf.writestr("allocations.csv", alloc_df.to_csv())
|
| 789 |
|
| 790 |
custom_batches = scenario.get("custom_resources", [])
|
| 791 |
if custom_batches:
|
|
|
|
| 815 |
|
| 816 |
resource_used_total = scenario.get("resource_used_total", {})
|
| 817 |
resource_remaining = scenario.get("resource_remaining", {})
|
| 818 |
+
_all_caps = dict(scenario.get("resource_caps", {}))
|
| 819 |
+
_custom_batches_map = {b["name"]: b for b in scenario.get("custom_resources", [])}
|
| 820 |
+
for _bn, _bi in _custom_batches_map.items():
|
| 821 |
+
_all_caps.setdefault(_bn, float(_bi["amount"]))
|
| 822 |
if resource_used_total or resource_remaining:
|
| 823 |
+
balance_rows = []
|
| 824 |
+
for r, cap in _all_caps.items():
|
| 825 |
+
if cap <= 0:
|
| 826 |
+
continue
|
| 827 |
+
if r in _custom_batches_map:
|
| 828 |
+
_unit = resource_unit_map.get(resource_to_group.get(_custom_batches_map[r]["group"]), "?")
|
| 829 |
+
else:
|
| 830 |
+
_unit = resource_unit_map.get(resource_to_group.get(r), "?")
|
| 831 |
+
used = resource_used_total.get(r, 0.0)
|
| 832 |
+
balance_rows.append({
|
| 833 |
+
"Resource": r,
|
| 834 |
+
"Unit": _unit,
|
| 835 |
+
"Available amount": cap,
|
| 836 |
+
"Quantity used": used,
|
| 837 |
+
"Quantity remaining": resource_remaining.get(r, cap),
|
| 838 |
+
"% used": round(used / cap * 100, 1) if cap > 0 else 0,
|
| 839 |
+
})
|
| 840 |
+
zf.writestr("resource_balance.csv", pd.DataFrame(balance_rows).to_csv(index=False))
|
| 841 |
|
| 842 |
resource_usage = scenario.get("resource_usage", {})
|
| 843 |
if resource_usage:
|
| 844 |
+
usage_rows = []
|
| 845 |
+
for r, methods in resource_usage.items():
|
| 846 |
+
if r in _custom_batches_map:
|
| 847 |
+
_u = resource_unit_map.get(resource_to_group.get(_custom_batches_map[r]["group"]), "?")
|
| 848 |
+
else:
|
| 849 |
+
_u = resource_unit_map.get(resource_to_group.get(r), "?")
|
| 850 |
+
for m, qty in methods.items():
|
| 851 |
+
usage_rows.append({"resource": r, "unit": _u, "method": m, "used": qty})
|
| 852 |
zf.writestr("resource_usage_detail.csv", pd.DataFrame(usage_rows).to_csv(index=False))
|
| 853 |
|
| 854 |
+
_actual_gains = scenario.get("resource_actual_gain", {})
|
| 855 |
+
if _actual_gains:
|
| 856 |
+
sens_rows = []
|
| 857 |
+
for r, gain in sorted(_actual_gains.items(), key=lambda x: -x[1]):
|
| 858 |
+
if r in _custom_batches_map:
|
| 859 |
+
_u = resource_unit_map.get(resource_to_group.get(_custom_batches_map[r]["group"]), "?")
|
| 860 |
+
else:
|
| 861 |
+
_u = resource_unit_map.get(resource_to_group.get(r), "?")
|
| 862 |
+
sens_rows.append({
|
| 863 |
+
"resource": r,
|
| 864 |
+
"unit": _u,
|
| 865 |
+
f"CDR_gain_for_+1_unit_{CO2_UNIT}": round(gain, 4),
|
| 866 |
+
})
|
| 867 |
+
zf.writestr("resource_sensitivity.csv", pd.DataFrame(sens_rows).to_csv(index=False))
|
| 868 |
+
|
| 869 |
+
_summary = scenario.get("summary", {})
|
| 870 |
+
summary_rows = [
|
| 871 |
+
{"metric": "Scenario name", "value": name, "unit": ""},
|
| 872 |
+
{"metric": "Export timestamp", "value": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "unit": ""},
|
| 873 |
+
{"metric": f"Total {CO2_UNIT} removed", "value": _summary.get("total_co2_removed", ""), "unit": CO2_UNIT},
|
| 874 |
+
{"metric": "Total cost (resource units sum)", "value": _summary.get("total_cost", ""), "unit": "mixed"},
|
| 875 |
+
{"metric": "Methods used", "value": len(scenario.get("allocations", {})), "unit": "count"},
|
| 876 |
+
]
|
| 877 |
+
zf.writestr("summary.csv", pd.DataFrame(summary_rows).to_csv(index=False))
|
| 878 |
+
|
| 879 |
with col_csv:
|
| 880 |
st.download_button(
|
| 881 |
"Export CSV (zip)", data=buf.getvalue(),
|
|
|
|
| 1124 |
<p class="crra-sub-sub-heading">From inputs</p>""",
|
| 1125 |
unsafe_allow_html=True,
|
| 1126 |
)
|
| 1127 |
+
run_clicked = st.button("Run Optimization", icon=":material/play_arrow:", key="btn-run-optimization", type="primary")
|
| 1128 |
panel_run_from_tabs()
|
| 1129 |
panel_load_from_excel()
|
| 1130 |
|
|
|
|
| 1184 |
resource_usage=latest.get("resource_usage"))
|
| 1185 |
|
| 1186 |
show_resource_balance(latest)
|
| 1187 |
+
show_bottleneck_analysis(latest)
|
| 1188 |
df_usage = build_resource_usage_df(latest["resource_usage"])
|
| 1189 |
|
| 1190 |
st.markdown("### Detailed resource usage by method")
|
tabs/tab6_scenarios.py
CHANGED
|
@@ -142,7 +142,7 @@ def render_tab():
|
|
| 142 |
panel_load_scenario()
|
| 143 |
|
| 144 |
if not st.session_state.scenarios:
|
| 145 |
-
st.info("No scenarios saved yet. Run an optimization in
|
| 146 |
return
|
| 147 |
|
| 148 |
# Select & manage scenarios
|
|
|
|
| 142 |
panel_load_scenario()
|
| 143 |
|
| 144 |
if not st.session_state.scenarios:
|
| 145 |
+
st.info("No scenarios saved yet. Run an optimization in **Portfolio Optimization** tab or load a file above.")
|
| 146 |
return
|
| 147 |
|
| 148 |
# Select & manage scenarios
|