Spaces:
Sleeping
Sleeping
[UX] British spelling: rename optimization → optimisation throughout code (conflict resolve)
Browse files- app.py +2 -2
- assets/styles.css +5 -5
- data/update.py +1 -1
- {optimization → optimisation}/__init__.py +0 -0
- optimization/optimization.py → optimisation/optimisation.py +5 -5
- optimization/__pycache__/__init__.cpython-311.pyc +0 -0
- optimization/__pycache__/optimization.cpython-311.pyc +0 -0
- tabs/tab1_inputs.py +1 -1
- tabs/tab2_constraints.py +2 -2
- tabs/{tab4_optimization.py → tab4_optimisation.py} +19 -19
- tabs/tab5_sensitivity.py +6 -6
- tabs/tab6_scenarios.py +1 -1
- utils/data_utils.py +6 -6
app.py
CHANGED
|
@@ -6,7 +6,7 @@ import pathlib
|
|
| 6 |
import streamlit as st
|
| 7 |
from streamlit_option_menu import option_menu
|
| 8 |
from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
|
| 9 |
-
tab3_coefficients,
|
| 10 |
tab6_scenarios)
|
| 11 |
from utils.data_utils import load_slider_data, strip_unit_suffix
|
| 12 |
from utils.state_utils import init_custom_resources
|
|
@@ -207,7 +207,7 @@ elif selected == "Method constraints":
|
|
| 207 |
elif selected == "Resource coefficients":
|
| 208 |
tab3_coefficients.render_tab(cost_sliders)
|
| 209 |
elif selected == "Portfolio generation":
|
| 210 |
-
|
| 211 |
elif selected == "Sensitivity analysis":
|
| 212 |
tab5_sensitivity.render_tab()
|
| 213 |
elif selected == "Scenario comparison":
|
|
|
|
| 6 |
import streamlit as st
|
| 7 |
from streamlit_option_menu import option_menu
|
| 8 |
from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
|
| 9 |
+
tab3_coefficients, tab4_optimisation, tab5_sensitivity,
|
| 10 |
tab6_scenarios)
|
| 11 |
from utils.data_utils import load_slider_data, strip_unit_suffix
|
| 12 |
from utils.state_utils import init_custom_resources
|
|
|
|
| 207 |
elif selected == "Resource coefficients":
|
| 208 |
tab3_coefficients.render_tab(cost_sliders)
|
| 209 |
elif selected == "Portfolio generation":
|
| 210 |
+
tab4_optimisation.render_tab()
|
| 211 |
elif selected == "Sensitivity analysis":
|
| 212 |
tab5_sensitivity.render_tab()
|
| 213 |
elif selected == "Scenario comparison":
|
assets/styles.css
CHANGED
|
@@ -404,8 +404,8 @@ h1.custom-title {
|
|
| 404 |
font-style: italic;
|
| 405 |
}
|
| 406 |
|
| 407 |
-
/* Run
|
| 408 |
-
.st-key-btn-run-
|
| 409 |
border: none !important;
|
| 410 |
border-radius: 0px;
|
| 411 |
background: var(--green-dark) !important;
|
|
@@ -416,17 +416,17 @@ h1.custom-title {
|
|
| 416 |
transition: background 0.25s ease, transform 0.15s ease;
|
| 417 |
box-shadow: 4px 4px 0px var(--green);
|
| 418 |
}
|
| 419 |
-
.st-key-btn-run-
|
| 420 |
background: #01740f !important;
|
| 421 |
color: var(--off-white) !important;
|
| 422 |
transform: translate(-2px, -2px);
|
| 423 |
box-shadow: 6px 6px 0px var(--green);
|
| 424 |
}
|
| 425 |
-
.st-key-btn-run-
|
| 426 |
transform: translate(0px, 0px);
|
| 427 |
box-shadow: 2px 2px 0px var(--green);
|
| 428 |
}
|
| 429 |
-
.st-key-btn-run-
|
| 430 |
font-weight: 700 !important;
|
| 431 |
font-size: clamp(15px, 1vw, 20px) !important;
|
| 432 |
letter-spacing: 0.12em !important;
|
|
|
|
| 404 |
font-style: italic;
|
| 405 |
}
|
| 406 |
|
| 407 |
+
/* Run optimisation — CTA principal */
|
| 408 |
+
.st-key-btn-run-optimisation button {
|
| 409 |
border: none !important;
|
| 410 |
border-radius: 0px;
|
| 411 |
background: var(--green-dark) !important;
|
|
|
|
| 416 |
transition: background 0.25s ease, transform 0.15s ease;
|
| 417 |
box-shadow: 4px 4px 0px var(--green);
|
| 418 |
}
|
| 419 |
+
.st-key-btn-run-optimisation button:hover {
|
| 420 |
background: #01740f !important;
|
| 421 |
color: var(--off-white) !important;
|
| 422 |
transform: translate(-2px, -2px);
|
| 423 |
box-shadow: 6px 6px 0px var(--green);
|
| 424 |
}
|
| 425 |
+
.st-key-btn-run-optimisation button:active {
|
| 426 |
transform: translate(0px, 0px);
|
| 427 |
box-shadow: 2px 2px 0px var(--green);
|
| 428 |
}
|
| 429 |
+
.st-key-btn-run-optimisation button p {
|
| 430 |
font-weight: 700 !important;
|
| 431 |
font-size: clamp(15px, 1vw, 20px) !important;
|
| 432 |
letter-spacing: 0.12em !important;
|
data/update.py
CHANGED
|
@@ -19,7 +19,7 @@ scripts = [
|
|
| 19 |
]
|
| 20 |
|
| 21 |
print("=" * 60)
|
| 22 |
-
print("CRRA
|
| 23 |
print("=" * 60)
|
| 24 |
|
| 25 |
for label, script in scripts:
|
|
|
|
| 19 |
]
|
| 20 |
|
| 21 |
print("=" * 60)
|
| 22 |
+
print("CRRA Optimisation Tool — data update")
|
| 23 |
print("=" * 60)
|
| 24 |
|
| 25 |
for label, script in scripts:
|
{optimization → optimisation}/__init__.py
RENAMED
|
File without changes
|
optimization/optimization.py → optimisation/optimisation.py
RENAMED
|
@@ -26,7 +26,7 @@ def build_custom_batch_structures(method_costs, resource_caps, custom_resources)
|
|
| 26 |
|
| 27 |
Mutates method_costs (adds fallback median coefficients) and resource_caps
|
| 28 |
(adds batch caps that are not yet present). Both dicts are already copies
|
| 29 |
-
when this function is called from
|
| 30 |
|
| 31 |
Args:
|
| 32 |
method_costs (dict): {method: {resource: coefficient}} — mutated in place.
|
|
@@ -531,7 +531,7 @@ def resolve_pooling_tiebreak(
|
|
| 531 |
"""Re-solve the LP with each method's output pinned, preferring the more
|
| 532 |
efficient resource within each standard/custom pooling pair or family pool.
|
| 533 |
|
| 534 |
-
The main LP (
|
| 535 |
in its objective distinguishes a standard resource from a custom batch
|
| 536 |
that substitutes for it (or from a same-family alternative resource), so
|
| 537 |
when several could satisfy the same demand the solver picks an arbitrary
|
|
@@ -650,7 +650,7 @@ def compute_standalone_potential(method, resource_caps, method_costs, custom_res
|
|
| 650 |
usage pairs once every other method is excluded).
|
| 651 |
enabled_methods (dict | None): {resource: {method: bool}}.
|
| 652 |
resource_to_group (dict | None): {resource: family group name} — see
|
| 653 |
-
build_family_pools. Must match what the outer
|
| 654 |
call uses, so the method's standalone potential already reflects
|
| 655 |
OR-availability across its own same-family resources.
|
| 656 |
|
|
@@ -659,7 +659,7 @@ def compute_standalone_potential(method, resource_caps, method_costs, custom_res
|
|
| 659 |
produce anything on its own).
|
| 660 |
"""
|
| 661 |
_UNCAPPED = 1e15
|
| 662 |
-
success, result =
|
| 663 |
resource_caps=resource_caps,
|
| 664 |
method_constraints={method: {"active": True, "cap_type": "absolute", "cap_value": _UNCAPPED}},
|
| 665 |
method_costs={method: dict(method_costs.get(method, {}))},
|
|
@@ -670,7 +670,7 @@ def compute_standalone_potential(method, resource_caps, method_costs, custom_res
|
|
| 670 |
return result["total_removed"] if success else 0.0
|
| 671 |
|
| 672 |
|
| 673 |
-
def
|
| 674 |
resource_caps,
|
| 675 |
method_constraints,
|
| 676 |
method_costs,
|
|
|
|
| 26 |
|
| 27 |
Mutates method_costs (adds fallback median coefficients) and resource_caps
|
| 28 |
(adds batch caps that are not yet present). Both dicts are already copies
|
| 29 |
+
when this function is called from run_optimisation.
|
| 30 |
|
| 31 |
Args:
|
| 32 |
method_costs (dict): {method: {resource: coefficient}} — mutated in place.
|
|
|
|
| 531 |
"""Re-solve the LP with each method's output pinned, preferring the more
|
| 532 |
efficient resource within each standard/custom pooling pair or family pool.
|
| 533 |
|
| 534 |
+
The main LP (run_optimisation) maximises total CO2 removed only — nothing
|
| 535 |
in its objective distinguishes a standard resource from a custom batch
|
| 536 |
that substitutes for it (or from a same-family alternative resource), so
|
| 537 |
when several could satisfy the same demand the solver picks an arbitrary
|
|
|
|
| 650 |
usage pairs once every other method is excluded).
|
| 651 |
enabled_methods (dict | None): {resource: {method: bool}}.
|
| 652 |
resource_to_group (dict | None): {resource: family group name} — see
|
| 653 |
+
build_family_pools. Must match what the outer run_optimisation
|
| 654 |
call uses, so the method's standalone potential already reflects
|
| 655 |
OR-availability across its own same-family resources.
|
| 656 |
|
|
|
|
| 659 |
produce anything on its own).
|
| 660 |
"""
|
| 661 |
_UNCAPPED = 1e15
|
| 662 |
+
success, result = run_optimisation(
|
| 663 |
resource_caps=resource_caps,
|
| 664 |
method_constraints={method: {"active": True, "cap_type": "absolute", "cap_value": _UNCAPPED}},
|
| 665 |
method_costs={method: dict(method_costs.get(method, {}))},
|
|
|
|
| 670 |
return result["total_removed"] if success else 0.0
|
| 671 |
|
| 672 |
|
| 673 |
+
def run_optimisation(
|
| 674 |
resource_caps,
|
| 675 |
method_constraints,
|
| 676 |
method_costs,
|
optimization/__pycache__/__init__.cpython-311.pyc
DELETED
|
Binary file (204 Bytes)
|
|
|
optimization/__pycache__/optimization.cpython-311.pyc
DELETED
|
Binary file (4.08 kB)
|
|
|
tabs/tab1_inputs.py
CHANGED
|
@@ -312,7 +312,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 312 |
if edit_index is not None:
|
| 313 |
# Clean up stale costs[method][old_name] entries left behind by
|
| 314 |
# this edit — otherwise tab4's missing-resource check wrongly
|
| 315 |
-
# deactivates that method on the next
|
| 316 |
# triggers: (a) a method that was previously assigned is no
|
| 317 |
# longer checked, or (b) the batch itself was renamed, which
|
| 318 |
# orphans the old name even for methods still checked (the new
|
|
|
|
| 312 |
if edit_index is not None:
|
| 313 |
# Clean up stale costs[method][old_name] entries left behind by
|
| 314 |
# this edit — otherwise tab4's missing-resource check wrongly
|
| 315 |
+
# deactivates that method on the next optimisation run. Two
|
| 316 |
# triggers: (a) a method that was previously assigned is no
|
| 317 |
# longer checked, or (b) the batch itself was renamed, which
|
| 318 |
# orphans the old name even for methods still checked (the new
|
tabs/tab2_constraints.py
CHANGED
|
@@ -58,7 +58,7 @@ def get_all_missing_required(m, cost_sliders_norm):
|
|
| 58 |
|
| 59 |
Family pooling: when two or more of the method's resources share the same
|
| 60 |
resource family (e.g. "Arable land" and "Other land" are both "Land" — see
|
| 61 |
-
build_family_pools in
|
| 62 |
than all-required. A resource in such a family is only reported as missing
|
| 63 |
if every member of that family is unavailable; a family member currently
|
| 64 |
enabled as a secondary resource (its slider toggled on in Tab 3) also
|
|
@@ -159,7 +159,7 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 159 |
|
| 160 |
Family pooling: when 2+ of the method's required resources share the same
|
| 161 |
resource family (e.g. "Forest biomass" / "Other biomass" are both
|
| 162 |
-
"Biomass" — see build_family_pools in
|
| 163 |
is actually needed. A family member that isn't set is shown as ✅
|
| 164 |
"optional" (not a blocking ❌) once a sibling already satisfies the
|
| 165 |
family, since adding it is a bonus, not a requirement.
|
|
|
|
| 58 |
|
| 59 |
Family pooling: when two or more of the method's resources share the same
|
| 60 |
resource family (e.g. "Arable land" and "Other land" are both "Land" — see
|
| 61 |
+
build_family_pools in optimisation.py), they are OR-substitutable rather
|
| 62 |
than all-required. A resource in such a family is only reported as missing
|
| 63 |
if every member of that family is unavailable; a family member currently
|
| 64 |
enabled as a secondary resource (its slider toggled on in Tab 3) also
|
|
|
|
| 159 |
|
| 160 |
Family pooling: when 2+ of the method's required resources share the same
|
| 161 |
resource family (e.g. "Forest biomass" / "Other biomass" are both
|
| 162 |
+
"Biomass" — see build_family_pools in optimisation.py), only one of them
|
| 163 |
is actually needed. A family member that isn't set is shown as ✅
|
| 164 |
"optional" (not a blocking ❌) once a sibling already satisfies the
|
| 165 |
family, since adding it is a bonus, not a requirement.
|
tabs/{tab4_optimization.py → tab4_optimisation.py}
RENAMED
|
@@ -9,7 +9,7 @@ import pandas as pd
|
|
| 9 |
import streamlit as st
|
| 10 |
import plotly.express as px
|
| 11 |
from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT, method_order, section_intros
|
| 12 |
-
from
|
| 13 |
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 14 |
from utils.data_utils import (is_valid_scenario, is_complete_scenario, load_inputs_from_excel,
|
| 15 |
neutralize_zero_coefficient_methods, strip_unit_suffix,
|
|
@@ -20,7 +20,7 @@ from utils.state_utils import clear_widget_keys
|
|
| 20 |
from config.config import hidden_resource_names as _hidden
|
| 21 |
|
| 22 |
def panel_run_from_tabs():
|
| 23 |
-
"""Left sub-panel: run
|
| 24 |
st.markdown("**Option 1 - From current tab values**", unsafe_allow_html=True)
|
| 25 |
st.markdown('<p class="caption">Set resource availability and method constraints in the tabs above, then run.</p>', unsafe_allow_html=True)
|
| 26 |
st.divider()
|
|
@@ -88,7 +88,7 @@ def parse_zip_scenario(uploaded) -> dict | None:
|
|
| 88 |
|
| 89 |
The detailed outputs (resource_usage, resource_used_total, resource_remaining)
|
| 90 |
are NOT trusted from allocations.csv alone — they are
|
| 91 |
-
recomputed by re-running the
|
| 92 |
the reloaded scenario is always internally consistent (and lets results pages
|
| 93 |
render without missing keys), instead of relying on whatever was exported.
|
| 94 |
"""
|
|
@@ -200,7 +200,7 @@ def parse_zip_scenario(uploaded) -> dict | None:
|
|
| 200 |
|
| 201 |
# Recompute detailed outputs from the reloaded inputs so the scenario
|
| 202 |
# is internally consistent (rather than trusting allocations.csv alone).
|
| 203 |
-
success, result =
|
| 204 |
resource_caps=resource_caps,
|
| 205 |
method_constraints=constraints,
|
| 206 |
method_costs=costs,
|
|
@@ -255,7 +255,7 @@ def panel_load_from_json_or_csv():
|
|
| 255 |
|
| 256 |
if "scenario_loaded_file" in st.session_state:
|
| 257 |
st.success(
|
| 258 |
-
f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated -
|
| 259 |
icon="✅"
|
| 260 |
)
|
| 261 |
uploaded_json = st.file_uploader(
|
|
@@ -275,7 +275,7 @@ def panel_load_from_json_or_csv():
|
|
| 275 |
st.error("Invalid scenario format.")
|
| 276 |
return
|
| 277 |
if not is_complete_scenario(data):
|
| 278 |
-
success, result =
|
| 279 |
resource_caps=data["resource_caps"],
|
| 280 |
method_constraints=data["method_constraints"],
|
| 281 |
method_costs=data["method_costs"],
|
|
@@ -302,7 +302,7 @@ def panel_load_from_json_or_csv():
|
|
| 302 |
|
| 303 |
filename_stem = uploaded_json.name.rsplit(".", 1)[0]
|
| 304 |
|
| 305 |
-
if st.button("↩️ Reload scenario and run
|
| 306 |
st.session_state.setdefault("scenario_name_suggestion", filename_stem)
|
| 307 |
st.session_state.pop("latest_result", None)
|
| 308 |
st.session_state.pop("excel_loaded_file", None)
|
|
@@ -346,7 +346,7 @@ def run_and_store_result():
|
|
| 346 |
|
| 347 |
Reads resource_caps, constraints, costs, custom_resources, and enabled_methods
|
| 348 |
from session state. Applies enabled_methods overrides to a temporary cost copy,
|
| 349 |
-
neutralises zero-coefficient methods, runs
|
| 350 |
full result dict to st.session_state.latest_result.
|
| 351 |
|
| 352 |
Shows a Streamlit warning/error if inputs are invalid or optimisation fails.
|
|
@@ -503,7 +503,7 @@ def run_and_store_result():
|
|
| 503 |
if group_cap_is_zero and batch_group in applied_costs[method_name]:
|
| 504 |
applied_costs[method_name][batch_group] = 0.0
|
| 505 |
|
| 506 |
-
success, result =
|
| 507 |
resource_caps=resource_caps_for_lp,
|
| 508 |
method_constraints=safe_constraints,
|
| 509 |
method_costs=applied_costs,
|
|
@@ -513,7 +513,7 @@ def run_and_store_result():
|
|
| 513 |
)
|
| 514 |
|
| 515 |
if not success:
|
| 516 |
-
st.error("❌
|
| 517 |
if "message" in result:
|
| 518 |
st.error(result["message"])
|
| 519 |
return
|
|
@@ -529,7 +529,7 @@ def run_and_store_result():
|
|
| 529 |
total_removed = result["total_removed"]
|
| 530 |
|
| 531 |
if total_removed <= 0:
|
| 532 |
-
st.warning(f"❌
|
| 533 |
return
|
| 534 |
|
| 535 |
total_cost = sum(
|
|
@@ -586,7 +586,7 @@ def show_metrics(latest):
|
|
| 586 |
"""Render top-level KPI metrics and an optional custom-resource summary expander.
|
| 587 |
|
| 588 |
Args:
|
| 589 |
-
latest (dict): result dict from
|
| 590 |
custom_resources.
|
| 591 |
|
| 592 |
Returns:
|
|
@@ -1131,7 +1131,7 @@ def export_inputs_only():
|
|
| 1131 |
|
| 1132 |
# Coefficients — a disabled (method, resource) pair is always exported
|
| 1133 |
# as 0, regardless of whether the user ever opened it in tab3. This
|
| 1134 |
-
# keeps the export consistent with what the
|
| 1135 |
ws3 = wb.create_sheet("coefficients")
|
| 1136 |
ws3.append(["method", "resource", "value", "secondary"])
|
| 1137 |
for m, resources in st.session_state.costs.items():
|
|
@@ -1333,7 +1333,7 @@ def render_tab():
|
|
| 1333 |
st.markdown(
|
| 1334 |
f"""
|
| 1335 |
<div class="text_with_border">
|
| 1336 |
-
<p>{section_intros.get("
|
| 1337 |
</div>
|
| 1338 |
""",
|
| 1339 |
unsafe_allow_html=True
|
|
@@ -1358,7 +1358,7 @@ def render_tab():
|
|
| 1358 |
|
| 1359 |
with tab_run:
|
| 1360 |
st.markdown("""
|
| 1361 |
-
<p class="crra-sub-heading">Run
|
| 1362 |
unsafe_allow_html=True,
|
| 1363 |
)
|
| 1364 |
col_inputs, col_scenario = st.columns(2, gap="large")
|
|
@@ -1368,7 +1368,7 @@ def render_tab():
|
|
| 1368 |
<p class="crra-sub-sub-heading">From inputs</p>""",
|
| 1369 |
unsafe_allow_html=True,
|
| 1370 |
)
|
| 1371 |
-
run_clicked = st.button("Run
|
| 1372 |
run_msg_area = st.container()
|
| 1373 |
panel_run_from_tabs()
|
| 1374 |
panel_load_from_excel()
|
|
@@ -1384,7 +1384,7 @@ def render_tab():
|
|
| 1384 |
with run_msg_area:
|
| 1385 |
run_and_store_result()
|
| 1386 |
|
| 1387 |
-
no_results = '<p class="caption">Run the
|
| 1388 |
|
| 1389 |
latest = st.session_state.get("latest_result")
|
| 1390 |
if not latest:
|
|
@@ -1418,7 +1418,7 @@ def render_tab():
|
|
| 1418 |
|
| 1419 |
with tab_results:
|
| 1420 |
show_metrics(latest)
|
| 1421 |
-
st.markdown('<p class="crra-sub-heading">
|
| 1422 |
st.dataframe(df_display, width='stretch')
|
| 1423 |
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
| 1424 |
st.markdown(
|
|
@@ -1455,6 +1455,6 @@ def render_tab():
|
|
| 1455 |
with tab_reset:
|
| 1456 |
reset_scenario("reset")
|
| 1457 |
|
| 1458 |
-
if st.button("Sensitivity analysis →", key="btn-nav-next-
|
| 1459 |
st.session_state["_navigate_to"] = "Sensitivity analysis"
|
| 1460 |
st.rerun()
|
|
|
|
| 9 |
import streamlit as st
|
| 10 |
import plotly.express as px
|
| 11 |
from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT, method_order, section_intros
|
| 12 |
+
from optimisation.optimisation import run_optimisation
|
| 13 |
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 14 |
from utils.data_utils import (is_valid_scenario, is_complete_scenario, load_inputs_from_excel,
|
| 15 |
neutralize_zero_coefficient_methods, strip_unit_suffix,
|
|
|
|
| 20 |
from config.config import hidden_resource_names as _hidden
|
| 21 |
|
| 22 |
def panel_run_from_tabs():
|
| 23 |
+
"""Left sub-panel: run optimisation from manually filled tabs."""
|
| 24 |
st.markdown("**Option 1 - From current tab values**", unsafe_allow_html=True)
|
| 25 |
st.markdown('<p class="caption">Set resource availability and method constraints in the tabs above, then run.</p>', unsafe_allow_html=True)
|
| 26 |
st.divider()
|
|
|
|
| 88 |
|
| 89 |
The detailed outputs (resource_usage, resource_used_total, resource_remaining)
|
| 90 |
are NOT trusted from allocations.csv alone — they are
|
| 91 |
+
recomputed by re-running the optimiser on the reloaded inputs. This guarantees
|
| 92 |
the reloaded scenario is always internally consistent (and lets results pages
|
| 93 |
render without missing keys), instead of relying on whatever was exported.
|
| 94 |
"""
|
|
|
|
| 200 |
|
| 201 |
# Recompute detailed outputs from the reloaded inputs so the scenario
|
| 202 |
# is internally consistent (rather than trusting allocations.csv alone).
|
| 203 |
+
success, result = run_optimisation(
|
| 204 |
resource_caps=resource_caps,
|
| 205 |
method_constraints=constraints,
|
| 206 |
method_costs=costs,
|
|
|
|
| 255 |
|
| 256 |
if "scenario_loaded_file" in st.session_state:
|
| 257 |
st.success(
|
| 258 |
+
f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated - optimisation results displayed.",
|
| 259 |
icon="✅"
|
| 260 |
)
|
| 261 |
uploaded_json = st.file_uploader(
|
|
|
|
| 275 |
st.error("Invalid scenario format.")
|
| 276 |
return
|
| 277 |
if not is_complete_scenario(data):
|
| 278 |
+
success, result = run_optimisation(
|
| 279 |
resource_caps=data["resource_caps"],
|
| 280 |
method_constraints=data["method_constraints"],
|
| 281 |
method_costs=data["method_costs"],
|
|
|
|
| 302 |
|
| 303 |
filename_stem = uploaded_json.name.rsplit(".", 1)[0]
|
| 304 |
|
| 305 |
+
if st.button("↩️ Reload scenario and run optimisation", width='stretch'):
|
| 306 |
st.session_state.setdefault("scenario_name_suggestion", filename_stem)
|
| 307 |
st.session_state.pop("latest_result", None)
|
| 308 |
st.session_state.pop("excel_loaded_file", None)
|
|
|
|
| 346 |
|
| 347 |
Reads resource_caps, constraints, costs, custom_resources, and enabled_methods
|
| 348 |
from session state. Applies enabled_methods overrides to a temporary cost copy,
|
| 349 |
+
neutralises zero-coefficient methods, runs run_optimisation, then writes the
|
| 350 |
full result dict to st.session_state.latest_result.
|
| 351 |
|
| 352 |
Shows a Streamlit warning/error if inputs are invalid or optimisation fails.
|
|
|
|
| 503 |
if group_cap_is_zero and batch_group in applied_costs[method_name]:
|
| 504 |
applied_costs[method_name][batch_group] = 0.0
|
| 505 |
|
| 506 |
+
success, result = run_optimisation(
|
| 507 |
resource_caps=resource_caps_for_lp,
|
| 508 |
method_constraints=safe_constraints,
|
| 509 |
method_costs=applied_costs,
|
|
|
|
| 513 |
)
|
| 514 |
|
| 515 |
if not success:
|
| 516 |
+
st.error("❌ optimisation failed")
|
| 517 |
if "message" in result:
|
| 518 |
st.error(result["message"])
|
| 519 |
return
|
|
|
|
| 529 |
total_removed = result["total_removed"]
|
| 530 |
|
| 531 |
if total_removed <= 0:
|
| 532 |
+
st.warning(f"❌ optimisation removed 0 {CO2_UNIT}. Relax constraints or increase resources.", icon=":material/warning:")
|
| 533 |
return
|
| 534 |
|
| 535 |
total_cost = sum(
|
|
|
|
| 586 |
"""Render top-level KPI metrics and an optional custom-resource summary expander.
|
| 587 |
|
| 588 |
Args:
|
| 589 |
+
latest (dict): result dict from run_optimisation with keys summary, allocations,
|
| 590 |
custom_resources.
|
| 591 |
|
| 592 |
Returns:
|
|
|
|
| 1131 |
|
| 1132 |
# Coefficients — a disabled (method, resource) pair is always exported
|
| 1133 |
# as 0, regardless of whether the user ever opened it in tab3. This
|
| 1134 |
+
# keeps the export consistent with what the optimiser actually uses.
|
| 1135 |
ws3 = wb.create_sheet("coefficients")
|
| 1136 |
ws3.append(["method", "resource", "value", "secondary"])
|
| 1137 |
for m, resources in st.session_state.costs.items():
|
|
|
|
| 1333 |
st.markdown(
|
| 1334 |
f"""
|
| 1335 |
<div class="text_with_border">
|
| 1336 |
+
<p>{section_intros.get("tab4_optimisation", "")}</p>
|
| 1337 |
</div>
|
| 1338 |
""",
|
| 1339 |
unsafe_allow_html=True
|
|
|
|
| 1358 |
|
| 1359 |
with tab_run:
|
| 1360 |
st.markdown("""
|
| 1361 |
+
<p class="crra-sub-heading">Run optimisation</p>""",
|
| 1362 |
unsafe_allow_html=True,
|
| 1363 |
)
|
| 1364 |
col_inputs, col_scenario = st.columns(2, gap="large")
|
|
|
|
| 1368 |
<p class="crra-sub-sub-heading">From inputs</p>""",
|
| 1369 |
unsafe_allow_html=True,
|
| 1370 |
)
|
| 1371 |
+
run_clicked = st.button("Run optimisation", icon=":material/play_arrow:", key="btn-run-optimisation", type="primary")
|
| 1372 |
run_msg_area = st.container()
|
| 1373 |
panel_run_from_tabs()
|
| 1374 |
panel_load_from_excel()
|
|
|
|
| 1384 |
with run_msg_area:
|
| 1385 |
run_and_store_result()
|
| 1386 |
|
| 1387 |
+
no_results = '<p class="caption">Run the optimisation first to see results here.</p>'
|
| 1388 |
|
| 1389 |
latest = st.session_state.get("latest_result")
|
| 1390 |
if not latest:
|
|
|
|
| 1418 |
|
| 1419 |
with tab_results:
|
| 1420 |
show_metrics(latest)
|
| 1421 |
+
st.markdown('<p class="crra-sub-heading">optimisation Summary</p>', unsafe_allow_html=True)
|
| 1422 |
st.dataframe(df_display, width='stretch')
|
| 1423 |
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
| 1424 |
st.markdown(
|
|
|
|
| 1455 |
with tab_reset:
|
| 1456 |
reset_scenario("reset")
|
| 1457 |
|
| 1458 |
+
if st.button("Sensitivity analysis →", key="btn-nav-next-optimisation", type="primary", help="Explore how resource or method changes affect your portfolio."):
|
| 1459 |
st.session_state["_navigate_to"] = "Sensitivity analysis"
|
| 1460 |
st.rerun()
|
tabs/tab5_sensitivity.py
CHANGED
|
@@ -3,7 +3,7 @@ import pandas as pd
|
|
| 3 |
import streamlit as st
|
| 4 |
import plotly.express as px
|
| 5 |
from config.config import CO2_UNIT, hidden_resource_names, method_order, resource_order, resource_unit_map, section_intros
|
| 6 |
-
from utils.data_utils import (get_resource_to_unit, get_resource_to_group,
|
| 7 |
is_other_method, get_other_method_variants, make_variant_name,
|
| 8 |
default_constraint)
|
| 9 |
from utils.ui_helpers import register_chart_data, show_bar
|
|
@@ -18,7 +18,7 @@ def compute_baseline():
|
|
| 18 |
all_resources – standard resources + custom batch names.
|
| 19 |
resource_to_unit – {resource: unit} including custom batches.
|
| 20 |
"""
|
| 21 |
-
baseline_success, baseline_result =
|
| 22 |
resource_caps=st.session_state.resource_caps,
|
| 23 |
method_constraints=st.session_state.constraints,
|
| 24 |
costs=st.session_state.costs,
|
|
@@ -98,7 +98,7 @@ def panel_resource_sensitivity(baseline_removed, all_resources, resource_to_unit
|
|
| 98 |
actual_amount = base_cap * pct / 100
|
| 99 |
perturbed_caps[resource] = actual_amount
|
| 100 |
|
| 101 |
-
success, result =
|
| 102 |
resource_caps=perturbed_caps,
|
| 103 |
method_constraints=st.session_state.constraints,
|
| 104 |
costs=st.session_state.costs,
|
|
@@ -162,7 +162,7 @@ def panel_method_sensitivity(baseline_removed):
|
|
| 162 |
|
| 163 |
# "Other"-tagged methods with custom batches attached are expanded into
|
| 164 |
# independent variants (e.g. "Enhanced weathering - Other mineral: Ganite")
|
| 165 |
-
# by
|
| 166 |
# entries too, alongside the standard method names, so their own cap can
|
| 167 |
# be swept just like any other method. An "Other"-tagged method with NO
|
| 168 |
# batch attached has no variant AND can never produce any result on its
|
|
@@ -226,7 +226,7 @@ def panel_method_sensitivity(baseline_removed):
|
|
| 226 |
perturbed_constraints[method]["cap_value"] = actual_cap
|
| 227 |
cap_label = f"{actual_cap:.2f} {CO2_UNIT}/yr"
|
| 228 |
|
| 229 |
-
success, result =
|
| 230 |
resource_caps=st.session_state.resource_caps,
|
| 231 |
method_constraints=perturbed_constraints,
|
| 232 |
costs=st.session_state.costs,
|
|
@@ -267,7 +267,7 @@ def panel_method_sensitivity(baseline_removed):
|
|
| 267 |
def show_bottleneck_analysis():
|
| 268 |
"""Render the resource sensitivity table (CDR gain per +1 unit of each resource).
|
| 269 |
|
| 270 |
-
Reads the latest
|
| 271 |
result is available yet.
|
| 272 |
|
| 273 |
Returns:
|
|
|
|
| 3 |
import streamlit as st
|
| 4 |
import plotly.express as px
|
| 5 |
from config.config import CO2_UNIT, hidden_resource_names, method_order, resource_order, resource_unit_map, section_intros
|
| 6 |
+
from utils.data_utils import (get_resource_to_unit, get_resource_to_group, run_expanded_optimisation,
|
| 7 |
is_other_method, get_other_method_variants, make_variant_name,
|
| 8 |
default_constraint)
|
| 9 |
from utils.ui_helpers import register_chart_data, show_bar
|
|
|
|
| 18 |
all_resources – standard resources + custom batch names.
|
| 19 |
resource_to_unit – {resource: unit} including custom batches.
|
| 20 |
"""
|
| 21 |
+
baseline_success, baseline_result = run_expanded_optimisation(
|
| 22 |
resource_caps=st.session_state.resource_caps,
|
| 23 |
method_constraints=st.session_state.constraints,
|
| 24 |
costs=st.session_state.costs,
|
|
|
|
| 98 |
actual_amount = base_cap * pct / 100
|
| 99 |
perturbed_caps[resource] = actual_amount
|
| 100 |
|
| 101 |
+
success, result = run_expanded_optimisation(
|
| 102 |
resource_caps=perturbed_caps,
|
| 103 |
method_constraints=st.session_state.constraints,
|
| 104 |
costs=st.session_state.costs,
|
|
|
|
| 162 |
|
| 163 |
# "Other"-tagged methods with custom batches attached are expanded into
|
| 164 |
# independent variants (e.g. "Enhanced weathering - Other mineral: Ganite")
|
| 165 |
+
# by run_expanded_optimisation — offer those variants as selectable
|
| 166 |
# entries too, alongside the standard method names, so their own cap can
|
| 167 |
# be swept just like any other method. An "Other"-tagged method with NO
|
| 168 |
# batch attached has no variant AND can never produce any result on its
|
|
|
|
| 226 |
perturbed_constraints[method]["cap_value"] = actual_cap
|
| 227 |
cap_label = f"{actual_cap:.2f} {CO2_UNIT}/yr"
|
| 228 |
|
| 229 |
+
success, result = run_expanded_optimisation(
|
| 230 |
resource_caps=st.session_state.resource_caps,
|
| 231 |
method_constraints=perturbed_constraints,
|
| 232 |
costs=st.session_state.costs,
|
|
|
|
| 267 |
def show_bottleneck_analysis():
|
| 268 |
"""Render the resource sensitivity table (CDR gain per +1 unit of each resource).
|
| 269 |
|
| 270 |
+
Reads the latest optimisation result from session state. Shows nothing if no
|
| 271 |
result is available yet.
|
| 272 |
|
| 273 |
Returns:
|
tabs/tab6_scenarios.py
CHANGED
|
@@ -4,7 +4,7 @@ import plotly.express as px
|
|
| 4 |
import streamlit as st
|
| 5 |
from config.config import CO2_UNIT, hidden_resource_names
|
| 6 |
from utils.data_utils import is_valid_scenario
|
| 7 |
-
from tabs.
|
| 8 |
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 9 |
|
| 10 |
|
|
|
|
| 4 |
import streamlit as st
|
| 5 |
from config.config import CO2_UNIT, hidden_resource_names
|
| 6 |
from utils.data_utils import is_valid_scenario
|
| 7 |
+
from tabs.tab4_optimisation import parse_zip_scenario
|
| 8 |
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 9 |
|
| 10 |
|
utils/data_utils.py
CHANGED
|
@@ -9,7 +9,7 @@ from config.config import (
|
|
| 9 |
resource_groups, method_groups, secondary_resources, CO2_UNIT,
|
| 10 |
hidden_resource_names,
|
| 11 |
)
|
| 12 |
-
from
|
| 13 |
|
| 14 |
def get_resource_to_group() -> dict[str, str]:
|
| 15 |
"""Map each resource name to its resource group.
|
|
@@ -245,10 +245,10 @@ def make_variant_name(method_name: str, batch_name: str) -> str:
|
|
| 245 |
return f"{method_name}: {batch_name}"
|
| 246 |
|
| 247 |
|
| 248 |
-
def
|
| 249 |
"""Apply the full Tab 4 preprocessing pipeline, then run the LP.
|
| 250 |
|
| 251 |
-
This is the same pipeline as
|
| 252 |
enabled_methods overrides, inject custom-batch coefficients, neutralise
|
| 253 |
all-zero methods, deactivate methods missing a required resource, and —
|
| 254 |
crucially — expand any "Other"-tagged method into one independent method
|
|
@@ -258,7 +258,7 @@ def run_expanded_optimization(resource_caps, method_constraints, costs, custom_r
|
|
| 258 |
|
| 259 |
Any caller that needs LP results consistent with what Tab 4 actually shows
|
| 260 |
(e.g. Tab 5's sensitivity sweeps) should use this instead of calling
|
| 261 |
-
|
| 262 |
custom resource attached to an "Other" method would be silently computed
|
| 263 |
through the wrong mechanism and never show up as its own entry.
|
| 264 |
|
|
@@ -273,7 +273,7 @@ def run_expanded_optimization(resource_caps, method_constraints, costs, custom_r
|
|
| 273 |
enabled_methods (dict): {resource: {method: bool}}.
|
| 274 |
|
| 275 |
Returns:
|
| 276 |
-
tuple[bool, dict]: same shape as
|
| 277 |
return value — (True, result_dict) or (False, {"message": str}).
|
| 278 |
"""
|
| 279 |
applied_costs = {m: dict(v) for m, v in costs.items()}
|
|
@@ -359,7 +359,7 @@ def run_expanded_optimization(resource_caps, method_constraints, costs, custom_r
|
|
| 359 |
if group_cap_is_zero and batch_group in applied_costs[method_name]:
|
| 360 |
applied_costs[method_name][batch_group] = 0.0
|
| 361 |
|
| 362 |
-
return
|
| 363 |
resource_caps=resource_caps_for_lp,
|
| 364 |
method_constraints=safe_constraints,
|
| 365 |
method_costs=applied_costs,
|
|
|
|
| 9 |
resource_groups, method_groups, secondary_resources, CO2_UNIT,
|
| 10 |
hidden_resource_names,
|
| 11 |
)
|
| 12 |
+
from optimisation.optimisation import run_optimisation
|
| 13 |
|
| 14 |
def get_resource_to_group() -> dict[str, str]:
|
| 15 |
"""Map each resource name to its resource group.
|
|
|
|
| 245 |
return f"{method_name}: {batch_name}"
|
| 246 |
|
| 247 |
|
| 248 |
+
def run_expanded_optimisation(resource_caps, method_constraints, costs, custom_resources, enabled_methods):
|
| 249 |
"""Apply the full Tab 4 preprocessing pipeline, then run the LP.
|
| 250 |
|
| 251 |
+
This is the same pipeline as tab4_optimisation.run_and_store_result: apply
|
| 252 |
enabled_methods overrides, inject custom-batch coefficients, neutralise
|
| 253 |
all-zero methods, deactivate methods missing a required resource, and —
|
| 254 |
crucially — expand any "Other"-tagged method into one independent method
|
|
|
|
| 258 |
|
| 259 |
Any caller that needs LP results consistent with what Tab 4 actually shows
|
| 260 |
(e.g. Tab 5's sensitivity sweeps) should use this instead of calling
|
| 261 |
+
run_optimisation() directly with raw session-state costs — otherwise a
|
| 262 |
custom resource attached to an "Other" method would be silently computed
|
| 263 |
through the wrong mechanism and never show up as its own entry.
|
| 264 |
|
|
|
|
| 273 |
enabled_methods (dict): {resource: {method: bool}}.
|
| 274 |
|
| 275 |
Returns:
|
| 276 |
+
tuple[bool, dict]: same shape as optimisation.run_optimisation()'s
|
| 277 |
return value — (True, result_dict) or (False, {"message": str}).
|
| 278 |
"""
|
| 279 |
applied_costs = {m: dict(v) for m, v in costs.items()}
|
|
|
|
| 359 |
if group_cap_is_zero and batch_group in applied_costs[method_name]:
|
| 360 |
applied_costs[method_name][batch_group] = 0.0
|
| 361 |
|
| 362 |
+
return run_optimisation(
|
| 363 |
resource_caps=resource_caps_for_lp,
|
| 364 |
method_constraints=safe_constraints,
|
| 365 |
method_costs=applied_costs,
|