Spaces:
Sleeping
Sleeping
[FIX] Geologic storage fixed coeff + template improvements + UI fixes
Browse files- assets/styles.css +64 -2
- data/cost_sliders.json +50 -5
- data/generate_cost_matrix.py +2 -2
- data/generate_input_template.py +48 -3
- data/inputs_aggregated.csv +9 -1
- data/inputs_template.xlsx +0 -0
- data/resource_template.xlsx +0 -0
- optimization/optimization.py +30 -65
- tabs/tab0_welcome.py +4 -5
- tabs/tab1_inputs.py +64 -32
- tabs/tab2_constraints.py +5 -3
- tabs/tab3_coefficients copy.py +0 -164
- tabs/tab3_coefficients.py +24 -6
- tabs/tab4_optimization copy.py +0 -199
- tabs/tab4_optimization.py +82 -59
- tabs/tab5_sensitivity.py +92 -89
- tabs/tab6_scenarios.py +16 -27
- utils/data_utils.py +22 -9
assets/styles.css
CHANGED
|
@@ -376,7 +376,8 @@ h1.custom-title {
|
|
| 376 |
[data-testid="stTextArea"] label p,
|
| 377 |
[data-testid="stSelectbox"] label p,
|
| 378 |
[data-testid="stMultiSelect"] label p,
|
| 379 |
-
[data-testid="stSlider"] label p
|
|
|
|
| 380 |
font-weight: 600 !important;
|
| 381 |
color: var(--brown-grey) !important;
|
| 382 |
font-size: 0.85rem !important;
|
|
@@ -440,6 +441,7 @@ h1.custom-title {
|
|
| 440 |
/* Slider help text */
|
| 441 |
[class*="slider_"] p,
|
| 442 |
[class*="manual_"] p,
|
|
|
|
| 443 |
[data-testid="stSlider"] p {
|
| 444 |
font-style: italic;
|
| 445 |
color: var(--grey);
|
|
@@ -586,12 +588,18 @@ section[data-testid="stExpander"] summary p {
|
|
| 586 |
}
|
| 587 |
|
| 588 |
/* ββ 16. Button container layout ββββββββββββββββββββββββββββββ */
|
| 589 |
-
.st-key-container-btn
|
| 590 |
display: flex;
|
| 591 |
flex-direction: row;
|
| 592 |
gap: 1.125%;
|
| 593 |
}
|
| 594 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 595 |
/* ββ 17. Scrollbar ββββββββββββββββββββββββββββββββββββββββββββ */
|
| 596 |
::-webkit-scrollbar {
|
| 597 |
width: 6px;
|
|
@@ -608,3 +616,57 @@ section[data-testid="stExpander"] summary p {
|
|
| 608 |
::-webkit-scrollbar-thumb:hover {
|
| 609 |
background: var(--brown-grey);
|
| 610 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
[data-testid="stTextArea"] label p,
|
| 377 |
[data-testid="stSelectbox"] label p,
|
| 378 |
[data-testid="stMultiSelect"] label p,
|
| 379 |
+
[data-testid="stSlider"] label p,
|
| 380 |
+
[class="crra-resource-name"] {
|
| 381 |
font-weight: 600 !important;
|
| 382 |
color: var(--brown-grey) !important;
|
| 383 |
font-size: 0.85rem !important;
|
|
|
|
| 441 |
/* Slider help text */
|
| 442 |
[class*="slider_"] p,
|
| 443 |
[class*="manual_"] p,
|
| 444 |
+
[class="crra-resource-name"],
|
| 445 |
[data-testid="stSlider"] p {
|
| 446 |
font-style: italic;
|
| 447 |
color: var(--grey);
|
|
|
|
| 588 |
}
|
| 589 |
|
| 590 |
/* ββ 16. Button container layout ββββββββββββββββββββββββββββββ */
|
| 591 |
+
.st-key-container-btn{
|
| 592 |
display: flex;
|
| 593 |
flex-direction: row;
|
| 594 |
gap: 1.125%;
|
| 595 |
}
|
| 596 |
|
| 597 |
+
.st-key-container-btn-new {
|
| 598 |
+
display: flex;
|
| 599 |
+
flex-direction: row-reverse;
|
| 600 |
+
gap: 1.125%;
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
/* ββ 17. Scrollbar ββββββββββββββββββββββββββββββββββββββββββββ */
|
| 604 |
::-webkit-scrollbar {
|
| 605 |
width: 6px;
|
|
|
|
| 616 |
::-webkit-scrollbar-thumb:hover {
|
| 617 |
background: var(--brown-grey);
|
| 618 |
}
|
| 619 |
+
|
| 620 |
+
/* ββ 18. Welcome page titles βββββββββββββββββββββββββββββββββββ */
|
| 621 |
+
h3.wt {
|
| 622 |
+
font-weight: 700 !important;
|
| 623 |
+
font-size: clamp(13px, 3vw, 20px) !important;
|
| 624 |
+
text-transform: uppercase;
|
| 625 |
+
letter-spacing: 0.12em;
|
| 626 |
+
margin: 28px 0 10px 0 !important;
|
| 627 |
+
padding: 6px 0 6px 14px;
|
| 628 |
+
border-left: 4px solid;
|
| 629 |
+
line-height: 1.3;
|
| 630 |
+
font-style: normal !important;
|
| 631 |
+
}
|
| 632 |
+
h3.wt-1 { color: #124216 !important; border-left-color: #124216; }
|
| 633 |
+
h3.wt-2 { color: var(--green-dark) !important; border-left-color: var(--green-dark); }
|
| 634 |
+
h3.wt-3 { color: var(--green) !important; border-left-color: var(--green); }
|
| 635 |
+
h3.wt-4 { color: var(--green-light) !important; border-left-color: var(--green-light); }
|
| 636 |
+
|
| 637 |
+
/* ββ 19. Tabs ββββββββββββββββββββββββββββββββββββββββββββββββββ */
|
| 638 |
+
[data-baseweb="tab-list"] {
|
| 639 |
+
gap: 4px;
|
| 640 |
+
border-bottom: 2px solid var(--off-white);
|
| 641 |
+
padding-bottom: 0;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
[data-baseweb="tab"] {
|
| 645 |
+
padding: 10px 20px;
|
| 646 |
+
font-size: 0.875rem;
|
| 647 |
+
font-weight: 600;
|
| 648 |
+
letter-spacing: 0.02em;
|
| 649 |
+
border-radius: 6px 6px 0 0;
|
| 650 |
+
background-color: var(--off-white);
|
| 651 |
+
color: var(--brown-grey);
|
| 652 |
+
border: none;
|
| 653 |
+
transition: background-color 0.15s ease, color 0.15s ease;
|
| 654 |
+
}
|
| 655 |
+
|
| 656 |
+
[data-baseweb="tab"]:hover {
|
| 657 |
+
background-color: #e8e8e8;
|
| 658 |
+
color: var(--black);
|
| 659 |
+
}
|
| 660 |
+
|
| 661 |
+
[aria-selected="true"][data-baseweb="tab"] {
|
| 662 |
+
background-color: var(--black);
|
| 663 |
+
color: var(--white);
|
| 664 |
+
}
|
| 665 |
+
|
| 666 |
+
[data-baseweb="tab-highlight"] {
|
| 667 |
+
display: none;
|
| 668 |
+
}
|
| 669 |
+
|
| 670 |
+
[data-baseweb="tab-border"] {
|
| 671 |
+
display: none;
|
| 672 |
+
}
|
data/cost_sliders.json
CHANGED
|
@@ -19,11 +19,6 @@
|
|
| 19 |
}
|
| 20 |
},
|
| 21 |
"BioCCS with anaerobic digestion/fermentation": {
|
| 22 |
-
"Arable land (Mha/MtCO2)": {
|
| 23 |
-
"min": 0.022803,
|
| 24 |
-
"median": 0.058844,
|
| 25 |
-
"max": 0.454545
|
| 26 |
-
},
|
| 27 |
"Non-forest biomass (Mt/MtCO2)": {
|
| 28 |
"min": 1.315,
|
| 29 |
"median": 1.561743,
|
|
@@ -38,6 +33,11 @@
|
|
| 38 |
"min": 0.368182,
|
| 39 |
"median": 0.565909,
|
| 40 |
"max": 0.913636
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
}
|
| 42 |
},
|
| 43 |
"BioCCS with combustion": {
|
|
@@ -70,6 +70,11 @@
|
|
| 70 |
"min": 0.00063,
|
| 71 |
"median": 0.201625,
|
| 72 |
"max": 1.296342
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
},
|
| 75 |
"Coastal revegetation (mangrove, seaweeds, etc.)": {
|
|
@@ -123,6 +128,11 @@
|
|
| 123 |
"min": 0.001,
|
| 124 |
"median": 0.005,
|
| 125 |
"max": 0.027
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
}
|
| 127 |
},
|
| 128 |
"Electrochemical OAE": {
|
|
@@ -174,6 +184,11 @@
|
|
| 174 |
"min": 5e-09,
|
| 175 |
"median": 0.0047,
|
| 176 |
"max": 0.05
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
}
|
| 178 |
},
|
| 179 |
"Low-temp DACCS (solid-sorbent/liquid-solvent)": {
|
|
@@ -196,6 +211,11 @@
|
|
| 196 |
"min": -0.002,
|
| 197 |
"median": 0.001008,
|
| 198 |
"max": 0.015
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
}
|
| 200 |
},
|
| 201 |
"Mineral OAE / Ocean liming": {
|
|
@@ -250,6 +270,11 @@
|
|
| 250 |
"min": 0.0,
|
| 251 |
"median": 0.0002,
|
| 252 |
"max": 0.026714
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
},
|
| 255 |
"Mineralization ex-situ": {
|
|
@@ -299,6 +324,11 @@
|
|
| 299 |
"min": -0.00015,
|
| 300 |
"median": 0.000427,
|
| 301 |
"max": 0.001
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
}
|
| 303 |
},
|
| 304 |
"Pasture management": {
|
|
@@ -335,6 +365,11 @@
|
|
| 335 |
"min": 0.217391,
|
| 336 |
"median": 6.608696,
|
| 337 |
"max": 13.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 338 |
}
|
| 339 |
},
|
| 340 |
"Bio-char - Forest biomass": {
|
|
@@ -536,6 +571,11 @@
|
|
| 536 |
"min": 0.00063,
|
| 537 |
"median": 0.201625,
|
| 538 |
"max": 1.296342
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
}
|
| 540 |
},
|
| 541 |
"BioCCS with anaerobic digestion/fermentation & growing biomass": {
|
|
@@ -548,6 +588,11 @@
|
|
| 548 |
"min": 0.368182,
|
| 549 |
"median": 0.565909,
|
| 550 |
"max": 0.913636
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 551 |
}
|
| 552 |
}
|
| 553 |
}
|
|
|
|
| 19 |
}
|
| 20 |
},
|
| 21 |
"BioCCS with anaerobic digestion/fermentation": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"Non-forest biomass (Mt/MtCO2)": {
|
| 23 |
"min": 1.315,
|
| 24 |
"median": 1.561743,
|
|
|
|
| 33 |
"min": 0.368182,
|
| 34 |
"median": 0.565909,
|
| 35 |
"max": 0.913636
|
| 36 |
+
},
|
| 37 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 38 |
+
"min": 1.0,
|
| 39 |
+
"median": 1.0,
|
| 40 |
+
"max": 1.0
|
| 41 |
}
|
| 42 |
},
|
| 43 |
"BioCCS with combustion": {
|
|
|
|
| 70 |
"min": 0.00063,
|
| 71 |
"median": 0.201625,
|
| 72 |
"max": 1.296342
|
| 73 |
+
},
|
| 74 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 75 |
+
"min": 1.0,
|
| 76 |
+
"median": 1.0,
|
| 77 |
+
"max": 1.0
|
| 78 |
}
|
| 79 |
},
|
| 80 |
"Coastal revegetation (mangrove, seaweeds, etc.)": {
|
|
|
|
| 128 |
"min": 0.001,
|
| 129 |
"median": 0.005,
|
| 130 |
"max": 0.027
|
| 131 |
+
},
|
| 132 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 133 |
+
"min": 1.0,
|
| 134 |
+
"median": 1.0,
|
| 135 |
+
"max": 1.0
|
| 136 |
}
|
| 137 |
},
|
| 138 |
"Electrochemical OAE": {
|
|
|
|
| 184 |
"min": 5e-09,
|
| 185 |
"median": 0.0047,
|
| 186 |
"max": 0.05
|
| 187 |
+
},
|
| 188 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 189 |
+
"min": 1.0,
|
| 190 |
+
"median": 1.0,
|
| 191 |
+
"max": 1.0
|
| 192 |
}
|
| 193 |
},
|
| 194 |
"Low-temp DACCS (solid-sorbent/liquid-solvent)": {
|
|
|
|
| 211 |
"min": -0.002,
|
| 212 |
"median": 0.001008,
|
| 213 |
"max": 0.015
|
| 214 |
+
},
|
| 215 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 216 |
+
"min": 1.0,
|
| 217 |
+
"median": 1.0,
|
| 218 |
+
"max": 1.0
|
| 219 |
}
|
| 220 |
},
|
| 221 |
"Mineral OAE / Ocean liming": {
|
|
|
|
| 270 |
"min": 0.0,
|
| 271 |
"median": 0.0002,
|
| 272 |
"max": 0.026714
|
| 273 |
+
},
|
| 274 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 275 |
+
"min": 1.0,
|
| 276 |
+
"median": 1.0,
|
| 277 |
+
"max": 1.0
|
| 278 |
}
|
| 279 |
},
|
| 280 |
"Mineralization ex-situ": {
|
|
|
|
| 324 |
"min": -0.00015,
|
| 325 |
"median": 0.000427,
|
| 326 |
"max": 0.001
|
| 327 |
+
},
|
| 328 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 329 |
+
"min": 1.0,
|
| 330 |
+
"median": 1.0,
|
| 331 |
+
"max": 1.0
|
| 332 |
}
|
| 333 |
},
|
| 334 |
"Pasture management": {
|
|
|
|
| 365 |
"min": 0.217391,
|
| 366 |
"median": 6.608696,
|
| 367 |
"max": 13.0
|
| 368 |
+
},
|
| 369 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 370 |
+
"min": 1.0,
|
| 371 |
+
"median": 1.0,
|
| 372 |
+
"max": 1.0
|
| 373 |
}
|
| 374 |
},
|
| 375 |
"Bio-char - Forest biomass": {
|
|
|
|
| 571 |
"min": 0.00063,
|
| 572 |
"median": 0.201625,
|
| 573 |
"max": 1.296342
|
| 574 |
+
},
|
| 575 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 576 |
+
"min": 1.0,
|
| 577 |
+
"median": 1.0,
|
| 578 |
+
"max": 1.0
|
| 579 |
}
|
| 580 |
},
|
| 581 |
"BioCCS with anaerobic digestion/fermentation & growing biomass": {
|
|
|
|
| 588 |
"min": 0.368182,
|
| 589 |
"median": 0.565909,
|
| 590 |
"max": 0.913636
|
| 591 |
+
},
|
| 592 |
+
"Geologic storage (Mt/MtCO2)": {
|
| 593 |
+
"min": 1.0,
|
| 594 |
+
"median": 1.0,
|
| 595 |
+
"max": 1.0
|
| 596 |
}
|
| 597 |
}
|
| 598 |
}
|
data/generate_cost_matrix.py
CHANGED
|
@@ -33,7 +33,7 @@ def create_sliders_inputs():
|
|
| 33 |
But set the other two to zero, and keep the values in the other resources intact
|
| 34 |
"""
|
| 35 |
resource = row["Resource"] + " (" + row["Unit"] + ")"
|
| 36 |
-
if row["min"] == row["max"]:
|
| 37 |
row["min"] = row["min"] * .5
|
| 38 |
entry = {
|
| 39 |
"min": float(row["min"]),
|
|
@@ -85,7 +85,7 @@ def create_sliders_inputs():
|
|
| 85 |
pop_by_name(cost_sliders["BioCCS with combustion & growing biomass"], "Unspecified biomass")
|
| 86 |
|
| 87 |
cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"] = cost_sliders["BioCCS with anaerobic digestion/fermentation"].copy()
|
| 88 |
-
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation"], "Arable land
|
| 89 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Non-forest biomass")
|
| 90 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Unspecified biomass")
|
| 91 |
|
|
|
|
| 33 |
But set the other two to zero, and keep the values in the other resources intact
|
| 34 |
"""
|
| 35 |
resource = row["Resource"] + " (" + row["Unit"] + ")"
|
| 36 |
+
if row["min"] == row["max"] and row["Resource"] != "Geologic storage":
|
| 37 |
row["min"] = row["min"] * .5
|
| 38 |
entry = {
|
| 39 |
"min": float(row["min"]),
|
|
|
|
| 85 |
pop_by_name(cost_sliders["BioCCS with combustion & growing biomass"], "Unspecified biomass")
|
| 86 |
|
| 87 |
cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"] = cost_sliders["BioCCS with anaerobic digestion/fermentation"].copy()
|
| 88 |
+
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation"], "Arable land")
|
| 89 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Non-forest biomass")
|
| 90 |
pop_by_name(cost_sliders["BioCCS with anaerobic digestion/fermentation & growing biomass"], "Unspecified biomass")
|
| 91 |
|
data/generate_input_template.py
CHANGED
|
@@ -25,9 +25,42 @@ resource_to_unit = {
|
|
| 25 |
for r in resources
|
| 26 |
}
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
# resource_caps
|
| 29 |
-
ws1 = wb.
|
| 30 |
-
ws1.title = "resource_caps"
|
| 31 |
ws1.append(["Resource", "Unit", "Available Amount"])
|
| 32 |
ws1["A1"].font = Font(bold=True)
|
| 33 |
ws1["B1"].font = Font(bold=True)
|
|
@@ -55,7 +88,19 @@ for m in method_order:
|
|
| 55 |
ws3.append([m, strip_unit_suffix(r), conf["median"], True if is_secondary else False])
|
| 56 |
|
| 57 |
ws4 = wb.create_sheet("custom_resources")
|
| 58 |
-
ws4.append(["name", "group", "amount", "method", "min", "median", "max"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
|
| 60 |
wb.save("data/inputs_template.xlsx")
|
| 61 |
print("Template generated: data/inputs_template.xlsx")
|
|
|
|
| 25 |
for r in resources
|
| 26 |
}
|
| 27 |
|
| 28 |
+
# instructions
|
| 29 |
+
ws0 = wb.active
|
| 30 |
+
ws0.title = "instructions"
|
| 31 |
+
ws0.column_dimensions["A"].width = 25
|
| 32 |
+
ws0.column_dimensions["B"].width = 80
|
| 33 |
+
header_font = Font(bold=True)
|
| 34 |
+
ws0.append(["Sheet", "Description", "Columns to edit", "Do not modify"])
|
| 35 |
+
ws0["A1"].font = header_font
|
| 36 |
+
ws0["B1"].font = header_font
|
| 37 |
+
ws0["C1"].font = header_font
|
| 38 |
+
ws0["D1"].font = header_font
|
| 39 |
+
ws0.column_dimensions["C"].width = 35
|
| 40 |
+
ws0.column_dimensions["D"].width = 30
|
| 41 |
+
ws0.append(["resource_caps",
|
| 42 |
+
"Total available quantities of each resource for 1 year of optimization. Leave blank to exclude a resource.",
|
| 43 |
+
"Available Amount",
|
| 44 |
+
"Resource, Unit"])
|
| 45 |
+
ws0.append(["constraints",
|
| 46 |
+
"Which CDR methods are active and their maximum deployment cap (percent of total potential, or absolute value in MtCO2/yr).",
|
| 47 |
+
"active (TRUE/FALSE), cap_type (percent or absolute), cap_value",
|
| 48 |
+
"method"])
|
| 49 |
+
ws0.append(["coefficients",
|
| 50 |
+
"Resource usage coefficients for each CDR method. Adjust to reflect your country conditions.",
|
| 51 |
+
"value",
|
| 52 |
+
"method, resource, secondary"])
|
| 53 |
+
ws0.append(["custom_resources",
|
| 54 |
+
"User-defined resource batches: sub-types of an existing resource group (e.g. 'Forest biomass - Eucalyptus' within 'Forest biomass'), with their own availability and per-method coefficients. Add one row per method linked to the resource.",
|
| 55 |
+
"All columns (add new rows)",
|
| 56 |
+
"β"])
|
| 57 |
+
ws0.append(["enabled_methods",
|
| 58 |
+
"Which CDR methods are allowed to use each standard resource. Set to FALSE to prevent a method from using a resource.",
|
| 59 |
+
"enabled (TRUE/FALSE)",
|
| 60 |
+
"resource, method"])
|
| 61 |
+
|
| 62 |
# resource_caps
|
| 63 |
+
ws1 = wb.create_sheet("resource_caps")
|
|
|
|
| 64 |
ws1.append(["Resource", "Unit", "Available Amount"])
|
| 65 |
ws1["A1"].font = Font(bold=True)
|
| 66 |
ws1["B1"].font = Font(bold=True)
|
|
|
|
| 88 |
ws3.append([m, strip_unit_suffix(r), conf["median"], True if is_secondary else False])
|
| 89 |
|
| 90 |
ws4 = wb.create_sheet("custom_resources")
|
| 91 |
+
ws4.append(["name", "group", "amount", "unit", "method", "min", "median", "max"])
|
| 92 |
+
for cell in ws4[1]:
|
| 93 |
+
cell.font = Font(bold=True)
|
| 94 |
+
ws4.append(["(Example) Forest biomass - Eucalyptus", "Forest biomass", 5.0, "Mt", "Bio-char - Forest biomass", 0.33, 0.37, 0.82])
|
| 95 |
+
|
| 96 |
+
ws5 = wb.create_sheet("enabled_methods")
|
| 97 |
+
ws5.append(["resource", "method", "enabled"])
|
| 98 |
+
for cell in ws5[1]:
|
| 99 |
+
cell.font = Font(bold=True)
|
| 100 |
+
for m in method_order:
|
| 101 |
+
for r_with_unit in cost_sliders.get(m, {}):
|
| 102 |
+
r = strip_unit_suffix(r_with_unit)
|
| 103 |
+
ws5.append([r, m, True])
|
| 104 |
|
| 105 |
wb.save("data/inputs_template.xlsx")
|
| 106 |
print("Template generated: data/inputs_template.xlsx")
|
data/inputs_aggregated.csv
CHANGED
|
@@ -73,4 +73,12 @@ Reforestation,Unspecified land,Physical,Mha/MtCO2,34,0.044459,1.564102,0.094927,
|
|
| 73 |
Reforestation,Unspecified water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0,0.092,0.092
|
| 74 |
pH swing,Electrical energy,Energy,MWh/MtCO2,28,980000,2205112,1768000,3302500,1381909,660000,4670000
|
| 75 |
pH swing,Salt water,Physical,bcm/MtCO2,4,0.435,4.78,4.78,9.125,5.017174,0.435,9.125
|
| 76 |
-
pH swing,Unspecified water,Physical,bcm/MtCO2,4,0.217391,6.608696,6.608696,13,7.380043,0.217391,13
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
Reforestation,Unspecified water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0,0.092,0.092
|
| 74 |
pH swing,Electrical energy,Energy,MWh/MtCO2,28,980000,2205112,1768000,3302500,1381909,660000,4670000
|
| 75 |
pH swing,Salt water,Physical,bcm/MtCO2,4,0.435,4.78,4.78,9.125,5.017174,0.435,9.125
|
| 76 |
+
pH swing,Unspecified water,Physical,bcm/MtCO2,4,0.217391,6.608696,6.608696,13,7.380043,0.217391,13
|
| 77 |
+
BioCCS with combustion,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 78 |
+
BioCCS with anaerobic digestion/fermentation,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 79 |
+
Electrochemical DACCS,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 80 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 81 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 82 |
+
Mineral looping DACCS,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 83 |
+
Moisture swing DACCS,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
| 84 |
+
pH swing,Geologic storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1
|
data/inputs_template.xlsx
CHANGED
|
Binary files a/data/inputs_template.xlsx and b/data/inputs_template.xlsx differ
|
|
|
data/resource_template.xlsx
CHANGED
|
Binary files a/data/resource_template.xlsx and b/data/resource_template.xlsx differ
|
|
|
optimization/optimization.py
CHANGED
|
@@ -12,35 +12,25 @@ def run_optimization(
|
|
| 12 |
"""
|
| 13 |
Maximize total COβ removal subject to resource caps and method constraints.
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
split variables z_{m,n} = COβ attributed to batch n for method m, so that
|
| 19 |
-
y_m = x_{m,standard} + z_{m,n} and each pool is constrained independently.
|
| 20 |
-
|
| 21 |
-
Args:
|
| 22 |
-
resource_caps {resource: available_quantity}
|
| 23 |
-
method_constraints {method: {"active": bool, "cap_type": str, "cap_value": float}}
|
| 24 |
-
method_costs {method: {resource: usage_per_MtCO2}}
|
| 25 |
-
custom_resources list of {"name", "group", "amount", "methods": {...}}
|
| 26 |
-
enabled_methods {resource: {method: bool}} β manual allocation overrides
|
| 27 |
-
|
| 28 |
-
Returns:
|
| 29 |
-
(success: bool, result: dict)
|
| 30 |
"""
|
| 31 |
|
| 32 |
resource_caps = dict(resource_caps)
|
| 33 |
method_costs = {m: dict(v) for m, v in method_costs.items()}
|
|
|
|
| 34 |
|
| 35 |
# ββ Apply manual allocation overrides βββββββββββββββββββββββββββββββββββββ
|
| 36 |
enabled_methods = enabled_methods or {}
|
|
|
|
| 37 |
for method, resources in method_costs.items():
|
| 38 |
for resource in list(resources):
|
| 39 |
if not enabled_methods.get(resource, {}).get(method, True):
|
| 40 |
method_costs[method][resource] = 0.0
|
| 41 |
|
| 42 |
# ββ Custom batch metadata βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 43 |
-
custom_batch_info = {}
|
| 44 |
for batch in (custom_resources or []):
|
| 45 |
custom_batch_info[batch["name"]] = {
|
| 46 |
"group": batch["group"],
|
|
@@ -48,17 +38,13 @@ def run_optimization(
|
|
| 48 |
"primary_methods": set(batch.get("methods", {}).keys()),
|
| 49 |
}
|
| 50 |
custom_names = set(custom_batch_info)
|
| 51 |
-
ref_to_batches = {}
|
| 52 |
for bname, info in custom_batch_info.items():
|
| 53 |
ref_to_batches.setdefault(info["group"], []).append(bname)
|
| 54 |
if bname not in resource_caps:
|
| 55 |
resource_caps[bname] = info["amount"]
|
| 56 |
|
| 57 |
-
# Inject primary batch coefficients (fallback
|
| 58 |
-
# Tab 3 pre-init may have set the coefficient to 0; fall back to batch median.
|
| 59 |
-
# Custom pools are INDEPENDENT of the standard resource enable/disable toggle:
|
| 60 |
-
# a method can access a custom batch even if it is disabled for the standard
|
| 61 |
-
# reference resource β the D2 coupling constraint (y_m β€ Ξ£ z) bounds the LP.
|
| 62 |
for batch in (custom_resources or []):
|
| 63 |
bname = batch["name"]
|
| 64 |
for method, coefs in batch["methods"].items():
|
|
@@ -74,40 +60,36 @@ def run_optimization(
|
|
| 74 |
for method in batch["methods"]:
|
| 75 |
coef = method_costs.get(method, {}).get(r_key)
|
| 76 |
print(f" method_costs['{method}']['{r_key}'] = {coef}")
|
|
|
|
| 77 |
print(f"Total resources in matrix: {len(resource_caps)}")
|
| 78 |
print(f"Total resources in matrix: {resource_caps}")
|
| 79 |
print(f"Resources: {list(resource_caps.keys())}")
|
| 80 |
print(f"{'='*50}\n")
|
| 81 |
|
| 82 |
-
# ββ Active
|
| 83 |
methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
|
| 84 |
M = len(methods)
|
| 85 |
method_idx = {m: j for j, m in enumerate(methods)}
|
| 86 |
if not M:
|
| 87 |
return False, {"message": "No active methods."}
|
| 88 |
|
| 89 |
-
# ββ Split
|
| 90 |
-
# z_{m,n} is the COβ from method m drawn from custom pool n.
|
| 91 |
-
# y_m - Ξ£_n z_{m,n} is what method m draws from the standard pool.
|
| 92 |
-
# Custom pools are INDEPENDENT: a method can use a custom pool even when
|
| 93 |
-
# the standard resource is 0 or disabled for that method.
|
| 94 |
-
# Guard: skip batch if the batch itself has no capacity (Q_n = 0).
|
| 95 |
z_pairs = []
|
| 96 |
for bname, info in custom_batch_info.items():
|
| 97 |
ref = info["group"]
|
| 98 |
primary_methods = info["primary_methods"]
|
| 99 |
if resource_caps.get(bname, 0.0) <= 0:
|
| 100 |
-
continue
|
| 101 |
for m in methods:
|
| 102 |
if m not in primary_methods:
|
| 103 |
-
continue
|
| 104 |
c_n = method_costs.get(m, {}).get(bname, 0.0)
|
| 105 |
if c_n > 0:
|
| 106 |
z_pairs.append((m, bname))
|
| 107 |
|
| 108 |
Z = len(z_pairs)
|
| 109 |
z_idx = {pair: M + i for i, pair in enumerate(z_pairs)}
|
| 110 |
-
N = M + Z
|
| 111 |
|
| 112 |
print(f"z_pairs ({Z}): {z_pairs}")
|
| 113 |
|
|
@@ -117,11 +99,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 |
-
# 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
|
| 125 |
for r in resources_no_batches:
|
| 126 |
row = np.zeros(N)
|
| 127 |
for j, m in enumerate(methods):
|
|
@@ -131,8 +111,8 @@ def run_optimization(
|
|
| 131 |
b_rows.append(resource_caps[r])
|
| 132 |
constraint_labels.append(("resource", r))
|
| 133 |
|
| 134 |
-
# (B) Standard resources
|
| 135 |
-
#
|
| 136 |
for r in resources_with_batches:
|
| 137 |
row = np.zeros(N)
|
| 138 |
for j, m in enumerate(methods):
|
|
@@ -140,13 +120,13 @@ def run_optimization(
|
|
| 140 |
row[j] = c_s_m
|
| 141 |
for bname in ref_to_batches.get(r, []):
|
| 142 |
if (m, bname) in z_idx:
|
| 143 |
-
row[z_idx[(m, bname)]] -= c_s_m
|
| 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
|
| 150 |
for bname, info in custom_batch_info.items():
|
| 151 |
if resource_caps.get(bname, 0.0) <= 0:
|
| 152 |
continue
|
|
@@ -162,12 +142,9 @@ def run_optimization(
|
|
| 162 |
b_rows.append(resource_caps.get(bname, 0.0))
|
| 163 |
constraint_labels.append(("custom_batch", bname))
|
| 164 |
|
| 165 |
-
# (D1)
|
| 166 |
-
#
|
| 167 |
-
#
|
| 168 |
-
# When a method has no access to the standard resource (disabled or coef=0),
|
| 169 |
-
# its entire output must come from custom batches β prevents y_m from being
|
| 170 |
-
# unbounded. Together with D1 this forces y_m = Ξ£ z when c_s = 0.
|
| 171 |
coupling_done = set()
|
| 172 |
for (m, bname) in z_pairs:
|
| 173 |
ref = custom_batch_info[bname]["group"]
|
|
@@ -179,7 +156,6 @@ def run_optimization(
|
|
| 179 |
c_s_m = method_costs.get(m, {}).get(ref, 0.0)
|
| 180 |
batch_cols = [z_idx[(m, bn)] for bn in ref_to_batches.get(ref, []) if (m, bn) in z_idx]
|
| 181 |
|
| 182 |
-
# D1: Ξ£ z β€ y_m β Ξ£ z - y_m β€ 0
|
| 183 |
row = np.zeros(N)
|
| 184 |
row[j] = -1.0
|
| 185 |
for col in batch_cols:
|
|
@@ -188,7 +164,6 @@ def run_optimization(
|
|
| 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:
|
| 193 |
row2 = np.zeros(N)
|
| 194 |
row2[j] = 1.0
|
|
@@ -198,7 +173,7 @@ def run_optimization(
|
|
| 198 |
b_rows.append(0.0)
|
| 199 |
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 200 |
|
| 201 |
-
# (E) Method
|
| 202 |
for j, m in enumerate(methods):
|
| 203 |
constraint = method_constraints.get(m, {})
|
| 204 |
cap_type = constraint.get("cap_type", "percent")
|
|
@@ -221,7 +196,7 @@ def run_optimization(
|
|
| 221 |
A_ub = np.array(A_rows) if A_rows else np.empty((0, N))
|
| 222 |
b_ub = np.array(b_rows)
|
| 223 |
|
| 224 |
-
# Objective: maximize Ξ£ y_m β minimize
|
| 225 |
c_obj = np.zeros(N)
|
| 226 |
c_obj[:M] = -1.0
|
| 227 |
bounds = [(0, None)] * N
|
|
@@ -233,7 +208,7 @@ def run_optimization(
|
|
| 233 |
return False, {"message": result.message}
|
| 234 |
|
| 235 |
sol = result.x
|
| 236 |
-
y = sol[:M]
|
| 237 |
|
| 238 |
total_removed = float(y.sum())
|
| 239 |
method_usage = {methods[j]: float(y[j]) for j in range(M) if y[j] > 1e-9}
|
|
@@ -242,14 +217,12 @@ def run_optimization(
|
|
| 242 |
all_resource_names = standard_resources + list(custom_names)
|
| 243 |
resource_usage = {r: {} for r in all_resource_names}
|
| 244 |
|
| 245 |
-
# Standard resources without substitutes
|
| 246 |
for r in resources_no_batches:
|
| 247 |
for j, m in enumerate(methods):
|
| 248 |
used = method_costs.get(m, {}).get(r, 0.0) * y[j]
|
| 249 |
if used > 1e-9:
|
| 250 |
resource_usage[r][m] = float(used)
|
| 251 |
|
| 252 |
-
# Standard resources with substitutes: usage = c_s * (y_m - Ξ£ z_{m,n})
|
| 253 |
for r in resources_with_batches:
|
| 254 |
for j, m in enumerate(methods):
|
| 255 |
c_s_m = method_costs.get(m, {}).get(r, 0.0)
|
|
@@ -262,34 +235,26 @@ def run_optimization(
|
|
| 262 |
if used > 1e-9:
|
| 263 |
resource_usage[r][m] = float(used)
|
| 264 |
|
| 265 |
-
# Custom batches β only for methods explicitly linked in the dialog
|
| 266 |
for bname, info in custom_batch_info.items():
|
| 267 |
-
ref = info["group"]
|
| 268 |
primary_methods = info["primary_methods"]
|
| 269 |
for j, m in enumerate(methods):
|
| 270 |
if m not in primary_methods:
|
| 271 |
-
continue
|
| 272 |
c_n_m = method_costs.get(m, {}).get(bname, 0.0)
|
| 273 |
if c_n_m <= 0:
|
| 274 |
continue
|
| 275 |
-
if (m, bname) in z_idx
|
| 276 |
-
used = c_n_m * sol[z_idx[(m, bname)]]
|
| 277 |
-
else:
|
| 278 |
-
used = 0.0
|
| 279 |
if used > 1e-9:
|
| 280 |
resource_usage[bname][m] = float(used)
|
| 281 |
|
| 282 |
resource_used_total = {r: sum(v.values()) for r, v in resource_usage.items()}
|
| 283 |
|
| 284 |
-
all_caps
|
| 285 |
-
|
| 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 |
-
# ββ
|
| 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"):
|
|
|
|
| 12 |
"""
|
| 13 |
Maximize total COβ removal subject to resource caps and method constraints.
|
| 14 |
|
| 15 |
+
LP variables:
|
| 16 |
+
y_m = COβ removed by method m (indices 0..M-1)
|
| 17 |
+
z_{m,n} = COβ drawn from custom batch n by method m (indices M..N-1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
"""
|
| 19 |
|
| 20 |
resource_caps = dict(resource_caps)
|
| 21 |
method_costs = {m: dict(v) for m, v in method_costs.items()}
|
| 22 |
+
print("first enabled_methods ----------- ")
|
| 23 |
|
| 24 |
# ββ Apply manual allocation overrides βββββββββββββββββββββββββββββββββββββ
|
| 25 |
enabled_methods = enabled_methods or {}
|
| 26 |
+
print(enabled_methods)
|
| 27 |
for method, resources in method_costs.items():
|
| 28 |
for resource in list(resources):
|
| 29 |
if not enabled_methods.get(resource, {}).get(method, True):
|
| 30 |
method_costs[method][resource] = 0.0
|
| 31 |
|
| 32 |
# ββ Custom batch metadata βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
custom_batch_info = {}
|
| 34 |
for batch in (custom_resources or []):
|
| 35 |
custom_batch_info[batch["name"]] = {
|
| 36 |
"group": batch["group"],
|
|
|
|
| 38 |
"primary_methods": set(batch.get("methods", {}).keys()),
|
| 39 |
}
|
| 40 |
custom_names = set(custom_batch_info)
|
| 41 |
+
ref_to_batches = {}
|
| 42 |
for bname, info in custom_batch_info.items():
|
| 43 |
ref_to_batches.setdefault(info["group"], []).append(bname)
|
| 44 |
if bname not in resource_caps:
|
| 45 |
resource_caps[bname] = info["amount"]
|
| 46 |
|
| 47 |
+
# ββ Inject primary batch coefficients (fallback: use batch median if coef=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
for batch in (custom_resources or []):
|
| 49 |
bname = batch["name"]
|
| 50 |
for method, coefs in batch["methods"].items():
|
|
|
|
| 60 |
for method in batch["methods"]:
|
| 61 |
coef = method_costs.get(method, {}).get(r_key)
|
| 62 |
print(f" method_costs['{method}']['{r_key}'] = {coef}")
|
| 63 |
+
print(method_costs)
|
| 64 |
print(f"Total resources in matrix: {len(resource_caps)}")
|
| 65 |
print(f"Total resources in matrix: {resource_caps}")
|
| 66 |
print(f"Resources: {list(resource_caps.keys())}")
|
| 67 |
print(f"{'='*50}\n")
|
| 68 |
|
| 69 |
+
# ββ Active methods ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 70 |
methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
|
| 71 |
M = len(methods)
|
| 72 |
method_idx = {m: j for j, m in enumerate(methods)}
|
| 73 |
if not M:
|
| 74 |
return False, {"message": "No active methods."}
|
| 75 |
|
| 76 |
+
# ββ Split variables z_{m,n} βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
z_pairs = []
|
| 78 |
for bname, info in custom_batch_info.items():
|
| 79 |
ref = info["group"]
|
| 80 |
primary_methods = info["primary_methods"]
|
| 81 |
if resource_caps.get(bname, 0.0) <= 0:
|
| 82 |
+
continue
|
| 83 |
for m in methods:
|
| 84 |
if m not in primary_methods:
|
| 85 |
+
continue
|
| 86 |
c_n = method_costs.get(m, {}).get(bname, 0.0)
|
| 87 |
if c_n > 0:
|
| 88 |
z_pairs.append((m, bname))
|
| 89 |
|
| 90 |
Z = len(z_pairs)
|
| 91 |
z_idx = {pair: M + i for i, pair in enumerate(z_pairs)}
|
| 92 |
+
N = M + Z
|
| 93 |
|
| 94 |
print(f"z_pairs ({Z}): {z_pairs}")
|
| 95 |
|
|
|
|
| 99 |
resources_no_batches = [r for r in standard_resources if r not in ref_to_batches]
|
| 100 |
|
| 101 |
# ββ Build LP constraint matrix ββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
| 102 |
A_rows, b_rows, constraint_labels = [], [], []
|
| 103 |
|
| 104 |
+
# (A) Standard resources β no custom substitutes
|
| 105 |
for r in resources_no_batches:
|
| 106 |
row = np.zeros(N)
|
| 107 |
for j, m in enumerate(methods):
|
|
|
|
| 111 |
b_rows.append(resource_caps[r])
|
| 112 |
constraint_labels.append(("resource", r))
|
| 113 |
|
| 114 |
+
# (B) Standard resources β with custom substitutes (subtract z contribution)
|
| 115 |
+
# Ξ£_m cost(m,r) Β· (y_m β Ξ£_n z_{m,n}) β€ cap(r)
|
| 116 |
for r in resources_with_batches:
|
| 117 |
row = np.zeros(N)
|
| 118 |
for j, m in enumerate(methods):
|
|
|
|
| 120 |
row[j] = c_s_m
|
| 121 |
for bname in ref_to_batches.get(r, []):
|
| 122 |
if (m, bname) in z_idx:
|
| 123 |
+
row[z_idx[(m, bname)]] -= c_s_m
|
| 124 |
if np.any(row != 0):
|
| 125 |
A_rows.append(row)
|
| 126 |
b_rows.append(resource_caps[r])
|
| 127 |
constraint_labels.append(("resource", r))
|
| 128 |
|
| 129 |
+
# (C) Custom batch capacity: Ξ£_m cost(m,n) Β· z_{m,n} β€ Q_n
|
| 130 |
for bname, info in custom_batch_info.items():
|
| 131 |
if resource_caps.get(bname, 0.0) <= 0:
|
| 132 |
continue
|
|
|
|
| 142 |
b_rows.append(resource_caps.get(bname, 0.0))
|
| 143 |
constraint_labels.append(("custom_batch", bname))
|
| 144 |
|
| 145 |
+
# (D1) Ξ£ z_{m,n} β€ y_m β keeps standard resource usage non-negative
|
| 146 |
+
# (D2) y_m β€ Ξ£ z_{m,n} β only when c_s=0: entire output must come from batch
|
| 147 |
+
# D1 + D2 together force y_m = Ξ£z when the standard resource is unavailable
|
|
|
|
|
|
|
|
|
|
| 148 |
coupling_done = set()
|
| 149 |
for (m, bname) in z_pairs:
|
| 150 |
ref = custom_batch_info[bname]["group"]
|
|
|
|
| 156 |
c_s_m = method_costs.get(m, {}).get(ref, 0.0)
|
| 157 |
batch_cols = [z_idx[(m, bn)] for bn in ref_to_batches.get(ref, []) if (m, bn) in z_idx]
|
| 158 |
|
|
|
|
| 159 |
row = np.zeros(N)
|
| 160 |
row[j] = -1.0
|
| 161 |
for col in batch_cols:
|
|
|
|
| 164 |
b_rows.append(0.0)
|
| 165 |
constraint_labels.append(("coupling_D1", (m, ref)))
|
| 166 |
|
|
|
|
| 167 |
if c_s_m == 0:
|
| 168 |
row2 = np.zeros(N)
|
| 169 |
row2[j] = 1.0
|
|
|
|
| 173 |
b_rows.append(0.0)
|
| 174 |
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 175 |
|
| 176 |
+
# (E) Method caps
|
| 177 |
for j, m in enumerate(methods):
|
| 178 |
constraint = method_constraints.get(m, {})
|
| 179 |
cap_type = constraint.get("cap_type", "percent")
|
|
|
|
| 196 |
A_ub = np.array(A_rows) if A_rows else np.empty((0, N))
|
| 197 |
b_ub = np.array(b_rows)
|
| 198 |
|
| 199 |
+
# Objective: maximize Ξ£ y_m β minimize οΏ½οΏ½οΏ½Ξ£ y_m
|
| 200 |
c_obj = np.zeros(N)
|
| 201 |
c_obj[:M] = -1.0
|
| 202 |
bounds = [(0, None)] * N
|
|
|
|
| 208 |
return False, {"message": result.message}
|
| 209 |
|
| 210 |
sol = result.x
|
| 211 |
+
y = sol[:M]
|
| 212 |
|
| 213 |
total_removed = float(y.sum())
|
| 214 |
method_usage = {methods[j]: float(y[j]) for j in range(M) if y[j] > 1e-9}
|
|
|
|
| 217 |
all_resource_names = standard_resources + list(custom_names)
|
| 218 |
resource_usage = {r: {} for r in all_resource_names}
|
| 219 |
|
|
|
|
| 220 |
for r in resources_no_batches:
|
| 221 |
for j, m in enumerate(methods):
|
| 222 |
used = method_costs.get(m, {}).get(r, 0.0) * y[j]
|
| 223 |
if used > 1e-9:
|
| 224 |
resource_usage[r][m] = float(used)
|
| 225 |
|
|
|
|
| 226 |
for r in resources_with_batches:
|
| 227 |
for j, m in enumerate(methods):
|
| 228 |
c_s_m = method_costs.get(m, {}).get(r, 0.0)
|
|
|
|
| 235 |
if used > 1e-9:
|
| 236 |
resource_usage[r][m] = float(used)
|
| 237 |
|
|
|
|
| 238 |
for bname, info in custom_batch_info.items():
|
|
|
|
| 239 |
primary_methods = info["primary_methods"]
|
| 240 |
for j, m in enumerate(methods):
|
| 241 |
if m not in primary_methods:
|
| 242 |
+
continue
|
| 243 |
c_n_m = method_costs.get(m, {}).get(bname, 0.0)
|
| 244 |
if c_n_m <= 0:
|
| 245 |
continue
|
| 246 |
+
used = c_n_m * sol[z_idx[(m, bname)]] if (m, bname) in z_idx else 0.0
|
|
|
|
|
|
|
|
|
|
| 247 |
if used > 1e-9:
|
| 248 |
resource_usage[bname][m] = float(used)
|
| 249 |
|
| 250 |
resource_used_total = {r: sum(v.values()) for r, v in resource_usage.items()}
|
| 251 |
|
| 252 |
+
all_caps = {**resource_caps,
|
| 253 |
+
**{bn: info["amount"] for bn, info in custom_batch_info.items()}}
|
| 254 |
resource_remaining = {r: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
|
| 255 |
for r in all_caps}
|
| 256 |
|
| 257 |
+
# ββ Perturbation analysis: marginal COβ gain per +1 unit of each resource
|
|
|
|
|
|
|
|
|
|
| 258 |
resource_actual_gain = {}
|
| 259 |
for i, (ctype, cid) in enumerate(constraint_labels):
|
| 260 |
if ctype not in ("resource", "custom_batch"):
|
tabs/tab0_welcome.py
CHANGED
|
@@ -1,6 +1,5 @@
|
|
| 1 |
import streamlit as st
|
| 2 |
from config.config import CO2_UNIT
|
| 3 |
-
from utils.ui_helpers import show_bar
|
| 4 |
|
| 5 |
def render_tab():
|
| 6 |
st.markdown(f"""
|
|
@@ -8,23 +7,23 @@ def render_tab():
|
|
| 8 |
|
| 9 |
This tool helps you optimize your carbon direct removal portfolio by adjusting resource usage and constraints.
|
| 10 |
|
| 11 |
-
<
|
| 12 |
<ol>
|
| 13 |
<li><b>Resource inputs</b>: provide the total available quantities of resources for 1 year.</li>
|
| 14 |
</ol>
|
| 15 |
|
| 16 |
-
<
|
| 17 |
<ol start="2">
|
| 18 |
<li><b>Method constraints</b>: select which carbon removal methods are active and set their <b>maximum deployment cap</b> (as % of total potential or absolute value in {CO2_UNIT}/yr).</li>
|
| 19 |
<li><b>Resource efficiency coefficients</b>: adjust the resource usage for each method.</li>
|
| 20 |
</ol>
|
| 21 |
|
| 22 |
-
<
|
| 23 |
<ol start="4">
|
| 24 |
<li><b>Portfolio generation</b>: run the optimization to find the best portfolio: how much COβ is removed, what methods are used, and in what proportions.</li>
|
| 25 |
</ol>
|
| 26 |
|
| 27 |
-
<
|
| 28 |
<ol start="5">
|
| 29 |
<li><b>Sensitivity analysis</b>: analyze how changes in method caps or resource availability affect the portfolio.</li>
|
| 30 |
<li><b>Scenario comparison</b>: compare different scenarios based on the optimization results.</li>
|
|
|
|
| 1 |
import streamlit as st
|
| 2 |
from config.config import CO2_UNIT
|
|
|
|
| 3 |
|
| 4 |
def render_tab():
|
| 5 |
st.markdown(f"""
|
|
|
|
| 7 |
|
| 8 |
This tool helps you optimize your carbon direct removal portfolio by adjusting resource usage and constraints.
|
| 9 |
|
| 10 |
+
<h3 class="wt wt-1">Required:</h3>
|
| 11 |
<ol>
|
| 12 |
<li><b>Resource inputs</b>: provide the total available quantities of resources for 1 year.</li>
|
| 13 |
</ol>
|
| 14 |
|
| 15 |
+
<h3 class="wt wt-2">Optional:</h3>
|
| 16 |
<ol start="2">
|
| 17 |
<li><b>Method constraints</b>: select which carbon removal methods are active and set their <b>maximum deployment cap</b> (as % of total potential or absolute value in {CO2_UNIT}/yr).</li>
|
| 18 |
<li><b>Resource efficiency coefficients</b>: adjust the resource usage for each method.</li>
|
| 19 |
</ol>
|
| 20 |
|
| 21 |
+
<h3 class="wt wt-3">Outputs:</h3>
|
| 22 |
<ol start="4">
|
| 23 |
<li><b>Portfolio generation</b>: run the optimization to find the best portfolio: how much COβ is removed, what methods are used, and in what proportions.</li>
|
| 24 |
</ol>
|
| 25 |
|
| 26 |
+
<h3 class="wt wt-4">Explore:</h3>
|
| 27 |
<ol start="5">
|
| 28 |
<li><b>Sensitivity analysis</b>: analyze how changes in method caps or resource availability affect the portfolio.</li>
|
| 29 |
<li><b>Scenario comparison</b>: compare different scenarios based on the optimization results.</li>
|
tabs/tab1_inputs.py
CHANGED
|
@@ -41,6 +41,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 41 |
if edit_index is not None and not st.session_state.get(f"_dlg_initialized_{edit_index}"):
|
| 42 |
for key in list(st.session_state.keys()):
|
| 43 |
if key.startswith(("_dlg_existing_enable_",
|
|
|
|
| 44 |
"_dlg_new_chk_", "_dlg_new_min_", "_dlg_new_med_", "_dlg_new_max_")):
|
| 45 |
del st.session_state[key]
|
| 46 |
st.session_state[f"_dlg_initialized_{edit_index}"] = True
|
|
@@ -94,7 +95,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 94 |
group = resource_to_group.get(batch_group)
|
| 95 |
unit = resource_unit_map.get(group, "?")
|
| 96 |
|
| 97 |
-
st.markdown(f'<p class="caption">Unit for this group: <strong>{unit}</strong>
|
| 98 |
|
| 99 |
st.markdown('<p class="dialog-title">2 β Available quantity</p>', unsafe_allow_html=True)
|
| 100 |
|
|
@@ -102,9 +103,10 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 102 |
f"Available amount ({unit})",
|
| 103 |
min_value=0.0,
|
| 104 |
format="%.2f",
|
| 105 |
-
value=float(existing.get("amount"
|
| 106 |
help=f"Total quantity available for 1 year, in {unit}.",
|
| 107 |
-
key="label-input-amount"
|
|
|
|
| 108 |
)
|
| 109 |
|
| 110 |
st.markdown('<p class="dialog-title">3 β Existing CDR methods for this resource</p>', unsafe_allow_html=True)
|
|
@@ -161,16 +163,39 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 161 |
if not enabled:
|
| 162 |
continue
|
| 163 |
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
|
| 175 |
if secondary_related:
|
| 176 |
names = "\n".join(f"- **{m}**" for m in secondary_related)
|
|
@@ -206,24 +231,30 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 206 |
col_min, col_med, col_max = st.columns(3)
|
| 207 |
with col_min:
|
| 208 |
v_min = st.number_input(
|
| 209 |
-
"Min",
|
| 210 |
min_value=0.0,
|
| 211 |
-
|
|
|
|
| 212 |
key=f"_dlg_new_min_{m}",
|
|
|
|
| 213 |
)
|
| 214 |
with col_med:
|
| 215 |
v_med = st.number_input(
|
| 216 |
-
"Median
|
| 217 |
min_value=0.0,
|
| 218 |
-
|
|
|
|
| 219 |
key=f"_dlg_new_med_{m}",
|
|
|
|
| 220 |
)
|
| 221 |
with col_max:
|
| 222 |
v_max = st.number_input(
|
| 223 |
-
"
|
| 224 |
min_value=0.0,
|
| 225 |
-
|
|
|
|
| 226 |
key=f"_dlg_new_max_{m}",
|
|
|
|
| 227 |
)
|
| 228 |
method_configs[m] = {
|
| 229 |
"min": v_min,
|
|
@@ -245,11 +276,14 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
|
|
| 245 |
errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.")
|
| 246 |
elif batch_name.strip() in existing_names:
|
| 247 |
errors.append(f"**{batch_name}** already exists. Choose a different name.")
|
| 248 |
-
if batch_amount <= 0:
|
| 249 |
errors.append("Available amount must be greater than 0.")
|
| 250 |
# if not method_configs:
|
| 251 |
# errors.append("Select at least one CDR method.")
|
| 252 |
for m, c in method_configs.items():
|
|
|
|
|
|
|
|
|
|
| 253 |
if c["min"] > c["max"]:
|
| 254 |
errors.append(f"**{m}**: min ({c['min']}) cannot be greater than max ({c['max']}).")
|
| 255 |
if not (c["min"] <= c["median"] <= c["max"]):
|
|
@@ -307,7 +341,7 @@ def to_excel_bytes(batches: list) -> bytes:
|
|
| 307 |
|
| 308 |
def _parse_custom_excel(file) -> list:
|
| 309 |
df = pd.read_excel(file, sheet_name=0)
|
| 310 |
-
required = {"name", "group", "amount", "method", "min", "median", "max"}
|
| 311 |
if not required.issubset(df.columns):
|
| 312 |
raise ValueError(f"Missing columns: {required - set(df.columns)}")
|
| 313 |
batches = []
|
|
@@ -394,7 +428,7 @@ def _render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
|
|
| 394 |
amount_val = float(batch["amount"])
|
| 395 |
batch["unit"] = unit # keep in sync for Excel export
|
| 396 |
|
| 397 |
-
st.markdown(f"π **{batch['name']}** β CDR methods: **{n_methods}**")
|
| 398 |
with st.container(border=True):
|
| 399 |
key_amount = f"custom_amount_{global_idx}"
|
| 400 |
if key_amount not in st.session_state:
|
|
@@ -403,7 +437,7 @@ def _render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
|
|
| 403 |
col_amount, _, _, _ = st.columns(4)
|
| 404 |
with col_amount:
|
| 405 |
new_amount = st.number_input(
|
| 406 |
-
f"
|
| 407 |
min_value=0.0,
|
| 408 |
format="%.2f",
|
| 409 |
key=key_amount,
|
|
@@ -437,17 +471,16 @@ def _render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
|
|
| 437 |
},
|
| 438 |
)
|
| 439 |
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
type="secondary", icon=":material/edit:"):
|
| 444 |
-
dialog_add_custom_resource(cost_sliders, edit_index=global_idx)
|
| 445 |
-
with col_del:
|
| 446 |
-
if st.button("Delete", key=f"del_batch_{global_idx}", use_container_width=True,
|
| 447 |
-
type="primary", icon=":material/delete:"):
|
| 448 |
st.session_state.custom_resources.pop(global_idx)
|
| 449 |
st.session_state.pop("latest_result", None)
|
| 450 |
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
| 451 |
|
| 452 |
|
| 453 |
def panel_custom_resources_controls(cost_sliders):
|
|
@@ -524,7 +557,6 @@ def render_tab(cost_sliders):
|
|
| 524 |
label="Download Excel Template",
|
| 525 |
data=f,
|
| 526 |
file_name="resource_template.xlsx",
|
| 527 |
-
# use_container_width=True,
|
| 528 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 529 |
icon=":material/download:",
|
| 530 |
key="download_button_resource_template"
|
|
|
|
| 41 |
if edit_index is not None and not st.session_state.get(f"_dlg_initialized_{edit_index}"):
|
| 42 |
for key in list(st.session_state.keys()):
|
| 43 |
if key.startswith(("_dlg_existing_enable_",
|
| 44 |
+
"_dlg_existing_min_", "_dlg_existing_med_", "_dlg_existing_max_",
|
| 45 |
"_dlg_new_chk_", "_dlg_new_min_", "_dlg_new_med_", "_dlg_new_max_")):
|
| 46 |
del st.session_state[key]
|
| 47 |
st.session_state[f"_dlg_initialized_{edit_index}"] = True
|
|
|
|
| 95 |
group = resource_to_group.get(batch_group)
|
| 96 |
unit = resource_unit_map.get(group, "?")
|
| 97 |
|
| 98 |
+
st.markdown(f'<p class="caption">Unit for this group: <strong>{unit}</strong></p>', unsafe_allow_html=True)
|
| 99 |
|
| 100 |
st.markdown('<p class="dialog-title">2 β Available quantity</p>', unsafe_allow_html=True)
|
| 101 |
|
|
|
|
| 103 |
f"Available amount ({unit})",
|
| 104 |
min_value=0.0,
|
| 105 |
format="%.2f",
|
| 106 |
+
value=float(existing["amount"]) if existing.get("amount") and existing.get("amount") != 0.0 else None,
|
| 107 |
help=f"Total quantity available for 1 year, in {unit}.",
|
| 108 |
+
key="label-input-amount",
|
| 109 |
+
placeholder="0.0"
|
| 110 |
)
|
| 111 |
|
| 112 |
st.markdown('<p class="dialog-title">3 β Existing CDR methods for this resource</p>', unsafe_allow_html=True)
|
|
|
|
| 163 |
if not enabled:
|
| 164 |
continue
|
| 165 |
|
| 166 |
+
existing_coeff = existing.get("methods", {}).get(m, coeff)
|
| 167 |
+
|
| 168 |
+
st.info("Modifying these values only affects this resource batch: the base method coefficients are unchanged.")
|
| 169 |
+
col_min, col_med, col_max = st.columns(3)
|
| 170 |
+
with col_min:
|
| 171 |
+
e_min = st.number_input(
|
| 172 |
+
f"Min ({unit}/{CO2_UNIT})",
|
| 173 |
+
min_value=0.0,
|
| 174 |
+
format="%.10g",
|
| 175 |
+
value=float(existing_coeff["min"]),
|
| 176 |
+
key=f"_dlg_existing_min_{m}",
|
| 177 |
+
)
|
| 178 |
+
with col_med:
|
| 179 |
+
e_med = st.number_input(
|
| 180 |
+
f"Median ({unit}/{CO2_UNIT})",
|
| 181 |
+
min_value=0.0,
|
| 182 |
+
format="%.10g",
|
| 183 |
+
value=float(existing_coeff["median"]),
|
| 184 |
+
key=f"_dlg_existing_med_{m}",
|
| 185 |
+
)
|
| 186 |
+
with col_max:
|
| 187 |
+
e_max = st.number_input(
|
| 188 |
+
f"Max ({unit}/{CO2_UNIT})",
|
| 189 |
+
min_value=0.0,
|
| 190 |
+
format="%.10g",
|
| 191 |
+
value=float(existing_coeff["max"]),
|
| 192 |
+
key=f"_dlg_existing_max_{m}",
|
| 193 |
+
)
|
| 194 |
+
method_configs[m] = {
|
| 195 |
+
"min": e_min,
|
| 196 |
+
"median": e_med,
|
| 197 |
+
"max": e_max,
|
| 198 |
+
}
|
| 199 |
|
| 200 |
if secondary_related:
|
| 201 |
names = "\n".join(f"- **{m}**" for m in secondary_related)
|
|
|
|
| 231 |
col_min, col_med, col_max = st.columns(3)
|
| 232 |
with col_min:
|
| 233 |
v_min = st.number_input(
|
| 234 |
+
f"Min ({unit}/{CO2_UNIT})",
|
| 235 |
min_value=0.0,
|
| 236 |
+
value=None,
|
| 237 |
+
format="%.10g",
|
| 238 |
key=f"_dlg_new_min_{m}",
|
| 239 |
+
placeholder="0.0000",
|
| 240 |
)
|
| 241 |
with col_med:
|
| 242 |
v_med = st.number_input(
|
| 243 |
+
f"Median ({unit}/{CO2_UNIT})",
|
| 244 |
min_value=0.0,
|
| 245 |
+
value=None,
|
| 246 |
+
format="%.10g",
|
| 247 |
key=f"_dlg_new_med_{m}",
|
| 248 |
+
placeholder="0.0000",
|
| 249 |
)
|
| 250 |
with col_max:
|
| 251 |
v_max = st.number_input(
|
| 252 |
+
f"Ma ({unit}/{CO2_UNIT})",
|
| 253 |
min_value=0.0,
|
| 254 |
+
value=None,
|
| 255 |
+
format="%.10g",
|
| 256 |
key=f"_dlg_new_max_{m}",
|
| 257 |
+
placeholder="0.0000",
|
| 258 |
)
|
| 259 |
method_configs[m] = {
|
| 260 |
"min": v_min,
|
|
|
|
| 276 |
errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.")
|
| 277 |
elif batch_name.strip() in existing_names:
|
| 278 |
errors.append(f"**{batch_name}** already exists. Choose a different name.")
|
| 279 |
+
if not batch_amount or batch_amount <= 0:
|
| 280 |
errors.append("Available amount must be greater than 0.")
|
| 281 |
# if not method_configs:
|
| 282 |
# errors.append("Select at least one CDR method.")
|
| 283 |
for m, c in method_configs.items():
|
| 284 |
+
if any(v is None for v in [c["min"], c["median"], c["max"]]):
|
| 285 |
+
errors.append(f"**{m}**: all coefficient fields (min, median, max) are required.")
|
| 286 |
+
continue
|
| 287 |
if c["min"] > c["max"]:
|
| 288 |
errors.append(f"**{m}**: min ({c['min']}) cannot be greater than max ({c['max']}).")
|
| 289 |
if not (c["min"] <= c["median"] <= c["max"]):
|
|
|
|
| 341 |
|
| 342 |
def _parse_custom_excel(file) -> list:
|
| 343 |
df = pd.read_excel(file, sheet_name=0)
|
| 344 |
+
required = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
|
| 345 |
if not required.issubset(df.columns):
|
| 346 |
raise ValueError(f"Missing columns: {required - set(df.columns)}")
|
| 347 |
batches = []
|
|
|
|
| 428 |
amount_val = float(batch["amount"])
|
| 429 |
batch["unit"] = unit # keep in sync for Excel export
|
| 430 |
|
| 431 |
+
# st.markdown(f"π **{batch['name']}** β CDR methods: **{n_methods}**")
|
| 432 |
with st.container(border=True):
|
| 433 |
key_amount = f"custom_amount_{global_idx}"
|
| 434 |
if key_amount not in st.session_state:
|
|
|
|
| 437 |
col_amount, _, _, _ = st.columns(4)
|
| 438 |
with col_amount:
|
| 439 |
new_amount = st.number_input(
|
| 440 |
+
f"{batch['name']}",
|
| 441 |
min_value=0.0,
|
| 442 |
format="%.2f",
|
| 443 |
key=key_amount,
|
|
|
|
| 471 |
},
|
| 472 |
)
|
| 473 |
|
| 474 |
+
with st.container(key="container-btn-new"):
|
| 475 |
+
if st.button("Delete", key=f"del_batch_{global_idx}",
|
| 476 |
+
type="primary", icon=":material/delete:"):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
st.session_state.custom_resources.pop(global_idx)
|
| 478 |
st.session_state.pop("latest_result", None)
|
| 479 |
st.rerun()
|
| 480 |
+
if st.button("Edit", key=f"edit_batch_{global_idx}",
|
| 481 |
+
type="secondary", icon=":material/edit:"):
|
| 482 |
+
dialog_add_custom_resource(cost_sliders, edit_index=global_idx)
|
| 483 |
+
|
| 484 |
|
| 485 |
|
| 486 |
def panel_custom_resources_controls(cost_sliders):
|
|
|
|
| 557 |
label="Download Excel Template",
|
| 558 |
data=f,
|
| 559 |
file_name="resource_template.xlsx",
|
|
|
|
| 560 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 561 |
icon=":material/download:",
|
| 562 |
key="download_button_resource_template"
|
tabs/tab2_constraints.py
CHANGED
|
@@ -89,12 +89,14 @@ def render_method(m, cols, i):
|
|
| 89 |
f"Max cap ({CO2_UNIT}/yr)",
|
| 90 |
min_value=0.0,
|
| 91 |
format="%.2f",
|
|
|
|
|
|
|
| 92 |
key=manual_key,
|
| 93 |
)
|
| 94 |
|
| 95 |
-
st.session_state.constraints[m]["cap_value"] = float(value)
|
| 96 |
|
| 97 |
-
if
|
| 98 |
st.warning(f"Cap is 0 {CO2_UNIT}/yr but method is active.", icon=":material/warning:")
|
| 99 |
|
| 100 |
def reset_all_constraints():
|
|
@@ -148,7 +150,7 @@ def render_tab():
|
|
| 148 |
unsafe_allow_html=True,
|
| 149 |
)
|
| 150 |
|
| 151 |
-
with st.expander("Manage methods", expanded=
|
| 152 |
for group, method_list in method_groups.items():
|
| 153 |
st.markdown(f"""
|
| 154 |
<p class="crra-group-name">{group}</p>""",
|
|
|
|
| 89 |
f"Max cap ({CO2_UNIT}/yr)",
|
| 90 |
min_value=0.0,
|
| 91 |
format="%.2f",
|
| 92 |
+
value=None,
|
| 93 |
+
placeholder="0.00",
|
| 94 |
key=manual_key,
|
| 95 |
)
|
| 96 |
|
| 97 |
+
st.session_state.constraints[m]["cap_value"] = float(value) if value is not None else 0.0
|
| 98 |
|
| 99 |
+
if not value:
|
| 100 |
st.warning(f"Cap is 0 {CO2_UNIT}/yr but method is active.", icon=":material/warning:")
|
| 101 |
|
| 102 |
def reset_all_constraints():
|
|
|
|
| 150 |
unsafe_allow_html=True,
|
| 151 |
)
|
| 152 |
|
| 153 |
+
with st.expander("Manage methods", expanded=True, icon=":material/settings:"):
|
| 154 |
for group, method_list in method_groups.items():
|
| 155 |
st.markdown(f"""
|
| 156 |
<p class="crra-group-name">{group}</p>""",
|
tabs/tab3_coefficients copy.py
DELETED
|
@@ -1,164 +0,0 @@
|
|
| 1 |
-
import pandas as pd
|
| 2 |
-
import streamlit as st
|
| 3 |
-
|
| 4 |
-
from config.config import (method_groups, method_order, resource_groups,
|
| 5 |
-
resource_order, secondary_resources)
|
| 6 |
-
from utils.ui_helpers import format_efficiency_summary
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
def render_tab(cost_sliders):
|
| 10 |
-
# Initialize costs
|
| 11 |
-
if "costs" not in st.session_state:
|
| 12 |
-
st.session_state.costs = {}
|
| 13 |
-
for m in cost_sliders:
|
| 14 |
-
if m not in st.session_state.costs:
|
| 15 |
-
st.session_state.costs[m] = {}
|
| 16 |
-
for r in cost_sliders[m]:
|
| 17 |
-
if r not in st.session_state.costs[m]:
|
| 18 |
-
st.session_state.costs[m][r] = cost_sliders[m][r]["median"]
|
| 19 |
-
|
| 20 |
-
st.header("Resource efficiency coefficients")
|
| 21 |
-
|
| 22 |
-
st.info("""
|
| 23 |
-
**Objective:**
|
| 24 |
-
|
| 25 |
-
This section lets you adjust the resource usage coefficients for each CDR method to best fit your country conditions.
|
| 26 |
-
|
| 27 |
-
- Select a method using the dropdown
|
| 28 |
-
- Adjust the resource usage coefficients (e.g., land, energy) using sliders
|
| 29 |
-
- Optionally override the slider using a manual input (even outside bounds)
|
| 30 |
-
- Disable secondary resources (e.g., water, chemicals) if needed
|
| 31 |
-
- Reset a single method or all methods with the reset buttons
|
| 32 |
-
- View a summary table of coefficients used in the model
|
| 33 |
-
""")
|
| 34 |
-
st.info("""
|
| 35 |
-
**Information:**
|
| 36 |
-
|
| 37 |
-
Data used in this section is based on
|
| 38 |
-
""")
|
| 39 |
-
st.warning("Blue cells in the table highlight coefficients modified from their **default median values**.")
|
| 40 |
-
|
| 41 |
-
# Combined dropdown for group + method
|
| 42 |
-
method_choices = []
|
| 43 |
-
method_lookup = {}
|
| 44 |
-
for group, methods in method_groups.items():
|
| 45 |
-
for method in methods:
|
| 46 |
-
label = f"{group} | {method}"
|
| 47 |
-
method_choices.append(label)
|
| 48 |
-
method_lookup[label] = method
|
| 49 |
-
|
| 50 |
-
selected_label = st.selectbox("Select a method:", method_choices)
|
| 51 |
-
method_selected = method_lookup[selected_label]
|
| 52 |
-
|
| 53 |
-
with st.expander(f"Manage resources for the {method_selected} method", expanded=True):
|
| 54 |
-
for group_name, rlist in resource_groups.items():
|
| 55 |
-
used = [r for r in rlist if r in cost_sliders[method_selected]]
|
| 56 |
-
if not used:
|
| 57 |
-
continue
|
| 58 |
-
|
| 59 |
-
st.markdown(f"### {group_name}")
|
| 60 |
-
for r in used:
|
| 61 |
-
conf = cost_sliders[method_selected][r]
|
| 62 |
-
default_val = conf["median"]
|
| 63 |
-
min_val = conf["min"]
|
| 64 |
-
max_val = conf["max"]
|
| 65 |
-
if min_val < 0.01:
|
| 66 |
-
step = 0.0001
|
| 67 |
-
fmt = "%.4f"
|
| 68 |
-
elif min_val < 1:
|
| 69 |
-
step = 0.001
|
| 70 |
-
fmt = "%.3f"
|
| 71 |
-
elif min_val < 100:
|
| 72 |
-
step = 0.01
|
| 73 |
-
fmt = "%.2f"
|
| 74 |
-
else:
|
| 75 |
-
step = 1.0
|
| 76 |
-
fmt = "%.0f"
|
| 77 |
-
|
| 78 |
-
slider_key = f"slider_{method_selected}_{r}".replace(" ", "_").lower()
|
| 79 |
-
manual_key = f"manual_{method_selected}_{r}".replace(" ", "_").lower()
|
| 80 |
-
toggle_key = f"toggle_{method_selected}_{r}".replace(" ", "_").lower()
|
| 81 |
-
|
| 82 |
-
is_secondary = (method_selected, r) in secondary_resources
|
| 83 |
-
if is_secondary and toggle_key not in st.session_state:
|
| 84 |
-
st.session_state[toggle_key] = False
|
| 85 |
-
|
| 86 |
-
enabled = True
|
| 87 |
-
if is_secondary:
|
| 88 |
-
enabled = st.checkbox(f"Use {r}", key=toggle_key)
|
| 89 |
-
|
| 90 |
-
if enabled:
|
| 91 |
-
col1, col2 = st.columns([3, 1])
|
| 92 |
-
with col1:
|
| 93 |
-
slider_val = st.slider(
|
| 94 |
-
label=r,
|
| 95 |
-
min_value=min_val,
|
| 96 |
-
max_value=max_val,
|
| 97 |
-
value=st.session_state.costs[method_selected].get(r, default_val),
|
| 98 |
-
step=step,
|
| 99 |
-
format=fmt,
|
| 100 |
-
key=slider_key
|
| 101 |
-
)
|
| 102 |
-
with col2:
|
| 103 |
-
manual_val = st.number_input(
|
| 104 |
-
label="",
|
| 105 |
-
value=slider_val,
|
| 106 |
-
key=manual_key,
|
| 107 |
-
help="Manual override (optional, any value)"
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
# Manual overrides slider if changed
|
| 111 |
-
final_val = manual_val if manual_val != slider_val else slider_val
|
| 112 |
-
st.session_state.costs[method_selected][r] = final_val
|
| 113 |
-
else:
|
| 114 |
-
st.session_state.costs[method_selected][r] = 0.0
|
| 115 |
-
st.markdown("**DISABLED**")
|
| 116 |
-
|
| 117 |
-
# Reset buttons
|
| 118 |
-
col1, col2 = st.columns(2)
|
| 119 |
-
with col1:
|
| 120 |
-
if st.button("π Reset selected method"):
|
| 121 |
-
for r in cost_sliders[method_selected]:
|
| 122 |
-
default_val = cost_sliders[method_selected][r]["median"]
|
| 123 |
-
st.session_state.costs[method_selected][r] = default_val
|
| 124 |
-
|
| 125 |
-
# Clear UI state
|
| 126 |
-
for suffix in ["slider", "manual", "toggle"]:
|
| 127 |
-
key = f"{suffix}_{method_selected}_{r}".replace(" ", "_").lower()
|
| 128 |
-
if key in st.session_state:
|
| 129 |
-
del st.session_state[key]
|
| 130 |
-
|
| 131 |
-
st.success(f"{method_selected} reset to default values.")
|
| 132 |
-
st.rerun()
|
| 133 |
-
|
| 134 |
-
with col2:
|
| 135 |
-
if st.button("π Reset all methods"):
|
| 136 |
-
for m in method_order:
|
| 137 |
-
for r in cost_sliders.get(m, {}):
|
| 138 |
-
default_val = cost_sliders[m][r]["median"]
|
| 139 |
-
st.session_state.costs[m][r] = default_val
|
| 140 |
-
|
| 141 |
-
for suffix in ["slider", "manual", "toggle"]:
|
| 142 |
-
key = f"{suffix}_{m}_{r}".replace(" ", "_").lower()
|
| 143 |
-
if key in st.session_state:
|
| 144 |
-
del st.session_state[key]
|
| 145 |
-
|
| 146 |
-
st.success("All methods reset to default values.")
|
| 147 |
-
st.rerun()
|
| 148 |
-
|
| 149 |
-
# Summary table
|
| 150 |
-
st.subheader("Summary table: resource usage coefficients in the model")
|
| 151 |
-
|
| 152 |
-
df_default = pd.DataFrame(index=method_order, columns=resource_order)
|
| 153 |
-
df_current = pd.DataFrame(index=method_order, columns=resource_order)
|
| 154 |
-
|
| 155 |
-
for m in method_order:
|
| 156 |
-
for r in resource_order:
|
| 157 |
-
df_default.at[m, r] = cost_sliders.get(m, {}).get(r, {}).get("median", 0.0)
|
| 158 |
-
df_current.at[m, r] = st.session_state.costs.get(m, {}).get(r, 0.0)
|
| 159 |
-
|
| 160 |
-
df_default = df_default.astype(float)
|
| 161 |
-
df_current = df_current.astype(float)
|
| 162 |
-
|
| 163 |
-
styled = format_efficiency_summary(df_current, df_default)
|
| 164 |
-
st.table(styled)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tabs/tab3_coefficients.py
CHANGED
|
@@ -34,7 +34,10 @@ def _make_sync_s_to_m(slider_key, manual_key):
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
st.session_state[manual_key] = clamped
|
| 39 |
st.session_state[slider_key] = clamped
|
| 40 |
return _sync
|
|
@@ -199,11 +202,13 @@ def render_tab(cost_sliders):
|
|
| 199 |
# ββ Resource sliders βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 200 |
with st.expander(f"Manage resources for **{method}**", expanded=True):
|
| 201 |
method_sliders = cost_sliders_norm.get(method, {})
|
|
|
|
| 202 |
method_costs_norm = _normalize_costs(st.session_state.costs.get(method, {}))
|
| 203 |
custom_for_method = custom_sliders_by_method.get(method, {})
|
| 204 |
|
| 205 |
for group_name, rlist in resource_groups.items():
|
| 206 |
used = [r for r in rlist if r in method_sliders]
|
|
|
|
| 207 |
custom_in_group = [
|
| 208 |
r for r in custom_for_method
|
| 209 |
if custom_resource_to_group.get(r) == group_name
|
|
@@ -243,11 +248,18 @@ def render_tab(cost_sliders):
|
|
| 243 |
st.session_state.costs[method][r] = conf["median"]
|
| 244 |
current_val = conf["median"]
|
| 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 |
|
|
@@ -263,16 +275,19 @@ def render_tab(cost_sliders):
|
|
| 263 |
on_change=_make_sync_s_to_m(slider_key, manual_key),
|
| 264 |
)
|
| 265 |
with col2:
|
| 266 |
-
|
| 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 |
-
|
|
|
|
| 276 |
|
| 277 |
|
| 278 |
# ββ New (custom) resources linked to this method ββββββββββββββββ
|
|
@@ -326,17 +341,20 @@ def render_tab(cost_sliders):
|
|
| 326 |
on_change=_make_sync_s_to_m(slider_key, manual_key),
|
| 327 |
)
|
| 328 |
with col2:
|
| 329 |
-
|
| 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 |
-
|
|
|
|
| 340 |
|
| 341 |
|
| 342 |
# ββ Reset buttons βββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββ
|
|
|
|
| 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 |
+
val = st.session_state[manual_key]
|
| 38 |
+
if val is None:
|
| 39 |
+
return
|
| 40 |
+
clamped = max(min_val, min(max_val, val))
|
| 41 |
st.session_state[manual_key] = clamped
|
| 42 |
st.session_state[slider_key] = clamped
|
| 43 |
return _sync
|
|
|
|
| 202 |
# ββ Resource sliders βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 203 |
with st.expander(f"Manage resources for **{method}**", expanded=True):
|
| 204 |
method_sliders = cost_sliders_norm.get(method, {})
|
| 205 |
+
print(method_sliders)
|
| 206 |
method_costs_norm = _normalize_costs(st.session_state.costs.get(method, {}))
|
| 207 |
custom_for_method = custom_sliders_by_method.get(method, {})
|
| 208 |
|
| 209 |
for group_name, rlist in resource_groups.items():
|
| 210 |
used = [r for r in rlist if r in method_sliders]
|
| 211 |
+
print(used)
|
| 212 |
custom_in_group = [
|
| 213 |
r for r in custom_for_method
|
| 214 |
if custom_resource_to_group.get(r) == group_name
|
|
|
|
| 248 |
st.session_state.costs[method][r] = conf["median"]
|
| 249 |
current_val = conf["median"]
|
| 250 |
|
| 251 |
+
if conf["min"] == conf["max"]:
|
| 252 |
+
st.markdown(f"""<p class="crra-resource-name">{display_label} = <strong>{conf['median']}</strong> (fixed)</p>""", unsafe_allow_html=True)
|
| 253 |
+
st.session_state.costs[method][r] = conf["median"]
|
| 254 |
+
if st.session_state.get("resource_caps", {}).get(r, 0) <= 0:
|
| 255 |
+
st.info(f"No available quantity set for **{r}** in Resource Inputs tab: **{method}** won't be used in the optimization.", icon=":material/info:")
|
| 256 |
+
continue
|
| 257 |
+
|
| 258 |
if slider_key not in st.session_state:
|
| 259 |
st.session_state[slider_key] = current_val
|
| 260 |
if manual_key not in st.session_state:
|
| 261 |
st.session_state[manual_key] = st.session_state[slider_key]
|
| 262 |
+
print(r)
|
| 263 |
if st.session_state.get("resource_caps", {}).get(r, 0) <= 0:
|
| 264 |
st.info(f"No available quantity set for **{r}** in Resource Inputs tab: **{method}** won't be used in the optimization.", icon=":material/info:")
|
| 265 |
|
|
|
|
| 275 |
on_change=_make_sync_s_to_m(slider_key, manual_key),
|
| 276 |
)
|
| 277 |
with col2:
|
| 278 |
+
st.number_input(
|
| 279 |
label="label",
|
| 280 |
label_visibility="collapsed",
|
| 281 |
key=manual_key,
|
| 282 |
help="Manual override (optional, any value)",
|
| 283 |
format=fmt,
|
| 284 |
+
value=None,
|
| 285 |
+
placeholder=str(round(slider_val, 4)),
|
| 286 |
on_change=_make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]),
|
| 287 |
)
|
| 288 |
|
| 289 |
+
_mv = st.session_state.get(manual_key)
|
| 290 |
+
st.session_state.costs[method][r] = slider_val if _mv is None else _mv
|
| 291 |
|
| 292 |
|
| 293 |
# ββ New (custom) resources linked to this method ββββββββββββββββ
|
|
|
|
| 341 |
on_change=_make_sync_s_to_m(slider_key, manual_key),
|
| 342 |
)
|
| 343 |
with col2:
|
| 344 |
+
st.number_input(
|
| 345 |
label="label",
|
| 346 |
label_visibility="collapsed",
|
| 347 |
key=manual_key,
|
| 348 |
help="Manual override (optional, any value)",
|
| 349 |
format=fmt,
|
| 350 |
+
value=None,
|
| 351 |
+
placeholder=str(round(slider_val, 4)),
|
| 352 |
on_change=_make_sync_m_to_s(slider_key, manual_key, conf["min"], conf["max"]),
|
| 353 |
)
|
| 354 |
|
| 355 |
st.session_state.costs.setdefault(method, {})
|
| 356 |
+
_mv = st.session_state.get(manual_key)
|
| 357 |
+
st.session_state.costs[method][r] = slider_val if _mv is None else _mv
|
| 358 |
|
| 359 |
|
| 360 |
# ββ Reset buttons βββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½ββββ
|
tabs/tab4_optimization copy.py
DELETED
|
@@ -1,199 +0,0 @@
|
|
| 1 |
-
import io
|
| 2 |
-
import json
|
| 3 |
-
import zipfile
|
| 4 |
-
|
| 5 |
-
import matplotlib.pyplot as plt
|
| 6 |
-
import pandas as pd
|
| 7 |
-
import plotly.graph_objects as go
|
| 8 |
-
import seaborn as sns
|
| 9 |
-
import streamlit as st
|
| 10 |
-
|
| 11 |
-
from config.config import method_order, resource_order, resource_units
|
| 12 |
-
from optimization.optimization import run_optimization
|
| 13 |
-
from utils.ui_helpers import format_results_summary
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
def render_tab():
|
| 17 |
-
st.header("Portfolio generation")
|
| 18 |
-
st.caption("Tab 4 - Seaborn fallback for vertical histograms")
|
| 19 |
-
|
| 20 |
-
st.info("""
|
| 21 |
-
**Objective:**
|
| 22 |
-
Run the optimization tool, explore results, and save scenarios for comparison or export.
|
| 23 |
-
""")
|
| 24 |
-
|
| 25 |
-
if "constraints" not in st.session_state or "costs" not in st.session_state or "resource_caps" not in st.session_state:
|
| 26 |
-
st.error("Missing inputs. Please complete previous steps.")
|
| 27 |
-
return
|
| 28 |
-
|
| 29 |
-
if "scenarios" not in st.session_state:
|
| 30 |
-
st.session_state.scenarios = {}
|
| 31 |
-
|
| 32 |
-
if sum(st.session_state.resource_caps.values()) <= 0:
|
| 33 |
-
st.warning("\u26a0\ufe0f All resources are currently set to 0. Please increase availability.")
|
| 34 |
-
return
|
| 35 |
-
|
| 36 |
-
if st.button("Run Optimization"):
|
| 37 |
-
success, result = run_optimization(
|
| 38 |
-
resource_caps=st.session_state.resource_caps,
|
| 39 |
-
method_constraints=st.session_state.constraints,
|
| 40 |
-
method_costs=st.session_state.costs,
|
| 41 |
-
)
|
| 42 |
-
|
| 43 |
-
if success:
|
| 44 |
-
allocations = result["method_usage"]
|
| 45 |
-
total_removed = result["total_removed"]
|
| 46 |
-
|
| 47 |
-
if total_removed <= 0:
|
| 48 |
-
st.warning("\u274c Optimization removed 0 tCOβ. Relax constraints or increase resources.")
|
| 49 |
-
return
|
| 50 |
-
|
| 51 |
-
total_cost = sum(
|
| 52 |
-
allocations[m] * sum(st.session_state.costs[m].values())
|
| 53 |
-
for m in allocations
|
| 54 |
-
)
|
| 55 |
-
|
| 56 |
-
st.session_state.latest_result = {
|
| 57 |
-
"summary": {
|
| 58 |
-
"total_co2_removed": total_removed,
|
| 59 |
-
"total_cost": total_cost
|
| 60 |
-
},
|
| 61 |
-
"allocations": allocations,
|
| 62 |
-
"resource_caps": st.session_state.resource_caps.copy(),
|
| 63 |
-
"method_constraints": st.session_state.constraints,
|
| 64 |
-
"method_costs": st.session_state.costs,
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
st.success(f"\u2705 Optimization successful: {total_removed:,.0f} tCOβ removed")
|
| 68 |
-
else:
|
| 69 |
-
st.error("\u274c Optimization failed")
|
| 70 |
-
if "message" in result:
|
| 71 |
-
st.error(result["message"])
|
| 72 |
-
|
| 73 |
-
if "latest_result" not in st.session_state:
|
| 74 |
-
return
|
| 75 |
-
|
| 76 |
-
latest = st.session_state.latest_result
|
| 77 |
-
allocations = latest["allocations"]
|
| 78 |
-
resource_caps = latest["resource_caps"]
|
| 79 |
-
method_costs = latest["method_costs"]
|
| 80 |
-
|
| 81 |
-
if not allocations or sum(allocations.values()) == 0:
|
| 82 |
-
return
|
| 83 |
-
|
| 84 |
-
col1, col2 = st.columns(2)
|
| 85 |
-
col1.metric("Total COβ Removed", f"{latest['summary']['total_co2_removed']:,.0f} t")
|
| 86 |
-
col2.metric("Methods Used", len(allocations))
|
| 87 |
-
|
| 88 |
-
st.subheader("Optimization Summary")
|
| 89 |
-
df_result = pd.DataFrame.from_dict(allocations, orient="index", columns=["tCOβ removed"])
|
| 90 |
-
df_result["tCOβ removed"] = pd.to_numeric(df_result["tCOβ removed"], errors="coerce")
|
| 91 |
-
total = df_result["tCOβ removed"].sum()
|
| 92 |
-
if total > 0:
|
| 93 |
-
df_result["Share (%)"] = (df_result["tCOβ removed"] / total * 100).round(2)
|
| 94 |
-
df_result = df_result.sort_values(by="tCOβ removed", ascending=False)
|
| 95 |
-
st.table(format_results_summary(df_result))
|
| 96 |
-
|
| 97 |
-
st.markdown("### Method allocation breakdown")
|
| 98 |
-
fig_donut = go.Figure(data=[go.Pie(
|
| 99 |
-
labels=df_result.index.tolist(),
|
| 100 |
-
values=df_result["tCOβ removed"].tolist(),
|
| 101 |
-
hole=0.4,
|
| 102 |
-
textinfo="label+percent",
|
| 103 |
-
textposition="outside",
|
| 104 |
-
marker=dict(line=dict(color="#000000", width=1)),
|
| 105 |
-
showlegend=False
|
| 106 |
-
)])
|
| 107 |
-
fig_donut.update_layout(margin=dict(t=10, b=10))
|
| 108 |
-
st.plotly_chart(fig_donut, use_container_width=True)
|
| 109 |
-
|
| 110 |
-
st.markdown("### Resource usage heatmap (% of cap)")
|
| 111 |
-
heatmap_matrix, heatmap_y = [], []
|
| 112 |
-
available_resources = [r for r in resource_order if resource_caps.get(r, 0) > 0]
|
| 113 |
-
for method in df_result.index:
|
| 114 |
-
row = [((method_costs.get(method, {}).get(r, 0.0) * allocations.get(method, 0.0)) / resource_caps[r]) * 100 if resource_caps[r] > 0 else 0 for r in available_resources]
|
| 115 |
-
heatmap_matrix.append(row)
|
| 116 |
-
heatmap_y.append(method)
|
| 117 |
-
fig_heatmap = go.Figure(data=go.Heatmap(
|
| 118 |
-
z=heatmap_matrix,
|
| 119 |
-
x=available_resources,
|
| 120 |
-
y=heatmap_y,
|
| 121 |
-
colorscale="Blues",
|
| 122 |
-
hovertemplate="Method: %{y}<br>Resource: %{x}<br>% Used: %{z:.2f}%<extra></extra>",
|
| 123 |
-
))
|
| 124 |
-
fig_heatmap.update_layout(title="Resource Usage vs. Cap", xaxis_title="Resource", yaxis_title="Method", height=400)
|
| 125 |
-
st.plotly_chart(fig_heatmap, use_container_width=True)
|
| 126 |
-
|
| 127 |
-
st.markdown("### Absolute resource usage by unit (Seaborn)")
|
| 128 |
-
df_abs = []
|
| 129 |
-
for m, co2_removed in allocations.items():
|
| 130 |
-
for unit, resources in resource_units.items():
|
| 131 |
-
for r in resources:
|
| 132 |
-
if r in method_costs.get(m, {}):
|
| 133 |
-
used = method_costs[m][r] * co2_removed
|
| 134 |
-
if used > 0:
|
| 135 |
-
df_abs.append({"Method": m, "Resource": r, "Used": used, "Unit": unit})
|
| 136 |
-
|
| 137 |
-
if df_abs:
|
| 138 |
-
df_abs = pd.DataFrame(df_abs)
|
| 139 |
-
for unit in df_abs["Unit"].unique():
|
| 140 |
-
st.markdown(f"#### Resources in {unit}")
|
| 141 |
-
df_unit = df_abs[df_abs["Unit"] == unit]
|
| 142 |
-
fig, ax = plt.subplots(figsize=(8, 4 + 0.3 * df_unit["Resource"].nunique()))
|
| 143 |
-
sns.barplot(data=df_unit, x="Resource", y="Used", hue="Method", ax=ax)
|
| 144 |
-
ax.set_ylabel(f"Used ({unit})")
|
| 145 |
-
ax.set_xlabel("Resource")
|
| 146 |
-
ax.tick_params(axis='x', rotation=45)
|
| 147 |
-
ax.legend(title="Method", bbox_to_anchor=(1.05, 1), loc='upper left')
|
| 148 |
-
st.pyplot(fig)
|
| 149 |
-
|
| 150 |
-
st.markdown("### Save scenario")
|
| 151 |
-
st.text_input("Scenario name", key="scenario_name")
|
| 152 |
-
|
| 153 |
-
if st.button("Save Scenario"):
|
| 154 |
-
name = st.session_state.get("scenario_name", "").strip()
|
| 155 |
-
if not name:
|
| 156 |
-
st.warning("Please enter a name before saving.")
|
| 157 |
-
elif name in st.session_state.scenarios:
|
| 158 |
-
st.warning("Scenario name already exists.")
|
| 159 |
-
else:
|
| 160 |
-
st.session_state.scenarios[name] = latest
|
| 161 |
-
st.success(f"Scenario '{name}' saved.")
|
| 162 |
-
|
| 163 |
-
current_name = st.session_state.get("scenario_name", "").strip()
|
| 164 |
-
if current_name and current_name in st.session_state.scenarios:
|
| 165 |
-
scenario = st.session_state.scenarios[current_name]
|
| 166 |
-
st.markdown(f"#### Scenario: {current_name}")
|
| 167 |
-
df_saved = pd.DataFrame.from_dict(scenario["allocations"], orient="index", columns=["tCOβ removed"])
|
| 168 |
-
df_saved["tCOβ removed"] = pd.to_numeric(df_saved["tCOβ removed"], errors="coerce")
|
| 169 |
-
total = df_saved["tCOβ removed"].sum()
|
| 170 |
-
if total > 0:
|
| 171 |
-
df_saved["Share (%)"] = (df_saved["tCOβ removed"] / total * 100).round(2)
|
| 172 |
-
df_saved = df_saved.sort_values(by="tCOβ removed", ascending=False)
|
| 173 |
-
st.table(format_results_summary(df_saved))
|
| 174 |
-
|
| 175 |
-
# Export as JSON
|
| 176 |
-
json_data = json.dumps({current_name: scenario}, indent=2)
|
| 177 |
-
st.download_button("Download Scenario as JSON", data=json_data, file_name=f"{current_name}_scenario.json", mime="application/json")
|
| 178 |
-
|
| 179 |
-
# Export as CSV zip
|
| 180 |
-
df_alloc = pd.DataFrame.from_dict(scenario["allocations"], orient="index", columns=["tCOβ removed"])
|
| 181 |
-
df_alloc.index.name = "Method"
|
| 182 |
-
df_alloc.reset_index(inplace=True)
|
| 183 |
-
df_caps = pd.DataFrame(list(scenario["resource_caps"].items()), columns=["Resource", "Cap"])
|
| 184 |
-
constraints_list = []
|
| 185 |
-
for method, values in scenario["method_constraints"].items():
|
| 186 |
-
entry = {"Method": method, "min_share": values.get("min_share", ""), "max_share": values.get("max_share", ""), "resources": values.get("resources", {})}
|
| 187 |
-
constraints_list.append(entry)
|
| 188 |
-
df_constraints = pd.json_normalize(constraints_list, sep=".").fillna("")
|
| 189 |
-
zip_buffer = io.BytesIO()
|
| 190 |
-
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED) as zf:
|
| 191 |
-
zf.writestr("allocations.csv", df_alloc.to_csv(index=False))
|
| 192 |
-
zf.writestr("resource_caps.csv", df_caps.to_csv(index=False))
|
| 193 |
-
zf.writestr("method_constraints.csv", df_constraints.to_csv(index=False))
|
| 194 |
-
st.download_button(
|
| 195 |
-
label="\ud83d\udcc5 Download Scenario as CSV (ZIP)",
|
| 196 |
-
data=zip_buffer.getvalue(),
|
| 197 |
-
file_name=f"{current_name}_scenario.zip",
|
| 198 |
-
mime="application/zip"
|
| 199 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tabs/tab4_optimization.py
CHANGED
|
@@ -69,6 +69,8 @@ def panel_load_from_excel():
|
|
| 69 |
|
| 70 |
if "excel_loaded_file" in st.session_state:
|
| 71 |
st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="β
")
|
|
|
|
|
|
|
| 72 |
|
| 73 |
uploaded = st.file_uploader("Upload inputs (.xlsx)", type=["xlsx"], key=f"inputs_loader_{st.session_state.excel_uploader_key}")
|
| 74 |
|
|
@@ -80,7 +82,7 @@ def panel_load_from_excel():
|
|
| 80 |
if st.button("β©οΈ Load inputs into all tabs", use_container_width=True):
|
| 81 |
st.session_state.setdefault("input_file_name_suggestion", default_name)
|
| 82 |
try:
|
| 83 |
-
resource_caps, constraints, costs, custom_resources, enabled_methods = load_inputs_from_excel(uploaded)
|
| 84 |
st.session_state.resource_caps = resource_caps
|
| 85 |
st.session_state.constraints = constraints
|
| 86 |
st.session_state.costs = costs
|
|
@@ -94,6 +96,7 @@ def panel_load_from_excel():
|
|
| 94 |
st.session_state["excel_loaded_file"] = uploaded.name
|
| 95 |
st.session_state["input_file_name"] = default_name
|
| 96 |
st.session_state["scenario_uploader_key"] += 1 # force reload in scenario uploader
|
|
|
|
| 97 |
st.rerun()
|
| 98 |
except Exception as e:
|
| 99 |
st.error(f"Error loading file: {e}")
|
|
@@ -520,40 +523,60 @@ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None,
|
|
| 520 |
st.info("No resources with non-zero availability to display.")
|
| 521 |
return
|
| 522 |
|
| 523 |
-
|
| 524 |
for method in df_result.index:
|
| 525 |
-
|
| 526 |
for r in available:
|
| 527 |
cap = full_caps.get(r, 0)
|
| 528 |
if cap <= 0:
|
| 529 |
-
|
|
|
|
| 530 |
continue
|
| 531 |
if resource_usage is not None:
|
| 532 |
-
# Use actual LP usage (correctly handles z-split for custom batches
|
| 533 |
-
# and the reduced standard usage when a batch absorbs part of the load)
|
| 534 |
used = resource_usage.get(r, {}).get(method, 0.0)
|
| 535 |
else:
|
| 536 |
-
# Fallback for legacy scenarios without resource_usage
|
| 537 |
used = normalized_costs.get(method, {}).get(r, 0.0) * df_result.at[method, f"{CO2_UNIT} removed"]
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
| 540 |
y_labels.append(method)
|
| 541 |
|
| 542 |
-
df_heat = pd.DataFrame(
|
|
|
|
| 543 |
|
| 544 |
fig = px.imshow(
|
| 545 |
df_heat, color_continuous_scale=PLOTLY_HEATMAP_SCALE, text_auto=".1f",
|
| 546 |
labels=dict(x="Resource", y="Method", color="% Used"),
|
| 547 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 548 |
apply_chart_style(fig, height=max(400, 60 + 30 * len(df_heat.index)))
|
| 549 |
st.plotly_chart(fig, use_container_width=True)
|
| 550 |
|
| 551 |
-
|
| 552 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 553 |
with st.expander("π View / download chart data"):
|
| 554 |
-
st.dataframe(
|
| 555 |
|
| 556 |
-
register_chart_data("Resource usage heatmap (% of cap)",
|
|
|
|
| 557 |
|
| 558 |
def show_absolute_usage(allocations, method_costs, custom_resources=None, resource_usage=None):
|
| 559 |
# Normalize method_costs keys: strip legacy unit suffixes
|
|
@@ -615,7 +638,7 @@ def show_absolute_usage(allocations, method_costs, custom_resources=None, resour
|
|
| 615 |
register_chart_data("Absolute resource usage by method", df)
|
| 616 |
|
| 617 |
def show_resource_balance(latest):
|
| 618 |
-
st.markdown(
|
| 619 |
resource_caps = latest["resource_caps"]
|
| 620 |
used_total = latest["resource_used_total"]
|
| 621 |
remaining = latest["resource_remaining"]
|
|
@@ -667,7 +690,7 @@ def show_resource_balance(latest):
|
|
| 667 |
register_chart_data("Resource balance (used vs remaining)", df)
|
| 668 |
|
| 669 |
def show_bottleneck_analysis(latest):
|
| 670 |
-
st.markdown(
|
| 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.'
|
|
@@ -703,7 +726,7 @@ def show_bottleneck_analysis(latest):
|
|
| 703 |
# βββ Save / export / reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 704 |
|
| 705 |
def save_scenario(latest):
|
| 706 |
-
st.markdown(
|
| 707 |
st.markdown('<p class="caption">Save the current scenario with inputs and outputs for comparison and/or export.</p>', unsafe_allow_html=True)
|
| 708 |
|
| 709 |
suggested = st.session_state.get("scenario_loaded_file", "").rsplit(".", 1)[0]
|
|
@@ -890,7 +913,7 @@ def export_inputs_only():
|
|
| 890 |
for group, resources in resource_groups.items()
|
| 891 |
for r in resources
|
| 892 |
}
|
| 893 |
-
st.markdown(
|
| 894 |
st.markdown('<p class="caption">Export only the resource capabilities, method constraints and coefficients β without results.</p>', unsafe_allow_html=True)
|
| 895 |
|
| 896 |
suggested = st.session_state.get("excel_loaded_file", "").rsplit(".", 1)[0]
|
|
@@ -987,7 +1010,7 @@ def export_inputs_only():
|
|
| 987 |
)
|
| 988 |
|
| 989 |
def export_all_charts():
|
| 990 |
-
st.markdown(
|
| 991 |
st.markdown(
|
| 992 |
'<p class="caption">Download a single file containing the underlying data '
|
| 993 |
'of every chart shown in this session.</p>',
|
|
@@ -1047,9 +1070,10 @@ def export_all_charts():
|
|
| 1047 |
)
|
| 1048 |
|
| 1049 |
def clear_all_data():
|
| 1050 |
-
for key in ["resource_caps", "constraints", "costs", "scenarios",
|
| 1051 |
-
"latest_result", "scenario_loaded_file", "scenario_name",
|
| 1052 |
-
"excel_loaded_file", "input_file_name", "resource_last_loaded_file",
|
|
|
|
| 1053 |
st.session_state.pop(key, None)
|
| 1054 |
|
| 1055 |
for uploader_key in ["resource_uploader_key", "excel_uploader_key", "scenario_uploader_key"]:
|
|
@@ -1073,7 +1097,7 @@ def confirm_clear_all():
|
|
| 1073 |
|
| 1074 |
def reset_scenario():
|
| 1075 |
st.divider()
|
| 1076 |
-
st.markdown(
|
| 1077 |
st.markdown('<p class="caption">Clear all current inputs, results and saved scenarios from the session.</p>', unsafe_allow_html=True)
|
| 1078 |
|
| 1079 |
if st.button("Clear all data", key="reset_scenario", use_container_width=False, type="primary", icon=":material/delete:"):
|
|
@@ -1152,47 +1176,46 @@ def render_tab():
|
|
| 1152 |
|
| 1153 |
# ββ Results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1154 |
show_metrics(latest)
|
|
|
|
| 1155 |
df_result = build_results_df(
|
| 1156 |
allocations,
|
| 1157 |
method_constraints=latest.get("method_constraints")
|
| 1158 |
)
|
| 1159 |
-
|
| 1160 |
df_display = df_result.copy()
|
| 1161 |
-
|
| 1162 |
df_display[f"{CO2_UNIT} removed"] = df_display[f"{CO2_UNIT} removed"].map(
|
| 1163 |
-
lambda x: f"{x:,.
|
| 1164 |
)
|
| 1165 |
|
| 1166 |
-
st.
|
| 1167 |
-
|
| 1168 |
-
|
| 1169 |
-
|
| 1170 |
-
|
| 1171 |
-
)
|
| 1172 |
-
|
| 1173 |
-
|
| 1174 |
-
|
| 1175 |
-
|
| 1176 |
-
|
| 1177 |
-
|
| 1178 |
-
|
| 1179 |
-
|
| 1180 |
-
|
| 1181 |
-
|
| 1182 |
-
|
| 1183 |
-
|
| 1184 |
-
|
| 1185 |
-
|
| 1186 |
-
|
| 1187 |
-
|
| 1188 |
-
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
|
| 1192 |
-
|
| 1193 |
-
|
| 1194 |
-
|
| 1195 |
-
|
| 1196 |
-
|
| 1197 |
-
|
| 1198 |
-
|
|
|
|
| 69 |
|
| 70 |
if "excel_loaded_file" in st.session_state:
|
| 71 |
st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="β
")
|
| 72 |
+
for w in st.session_state.pop("import_warnings", []):
|
| 73 |
+
st.warning(w, icon=":material/search:")
|
| 74 |
|
| 75 |
uploaded = st.file_uploader("Upload inputs (.xlsx)", type=["xlsx"], key=f"inputs_loader_{st.session_state.excel_uploader_key}")
|
| 76 |
|
|
|
|
| 82 |
if st.button("β©οΈ Load inputs into all tabs", use_container_width=True):
|
| 83 |
st.session_state.setdefault("input_file_name_suggestion", default_name)
|
| 84 |
try:
|
| 85 |
+
resource_caps, constraints, costs, custom_resources, enabled_methods, import_warnings = load_inputs_from_excel(uploaded)
|
| 86 |
st.session_state.resource_caps = resource_caps
|
| 87 |
st.session_state.constraints = constraints
|
| 88 |
st.session_state.costs = costs
|
|
|
|
| 96 |
st.session_state["excel_loaded_file"] = uploaded.name
|
| 97 |
st.session_state["input_file_name"] = default_name
|
| 98 |
st.session_state["scenario_uploader_key"] += 1 # force reload in scenario uploader
|
| 99 |
+
st.session_state["import_warnings"] = import_warnings
|
| 100 |
st.rerun()
|
| 101 |
except Exception as e:
|
| 102 |
st.error(f"Error loading file: {e}")
|
|
|
|
| 523 |
st.info("No resources with non-zero availability to display.")
|
| 524 |
return
|
| 525 |
|
| 526 |
+
matrix_pct, matrix_exact, y_labels = [], [], []
|
| 527 |
for method in df_result.index:
|
| 528 |
+
row_pct, row_exact = [], []
|
| 529 |
for r in available:
|
| 530 |
cap = full_caps.get(r, 0)
|
| 531 |
if cap <= 0:
|
| 532 |
+
row_pct.append(0)
|
| 533 |
+
row_exact.append(0)
|
| 534 |
continue
|
| 535 |
if resource_usage is not None:
|
|
|
|
|
|
|
| 536 |
used = resource_usage.get(r, {}).get(method, 0.0)
|
| 537 |
else:
|
|
|
|
| 538 |
used = normalized_costs.get(method, {}).get(r, 0.0) * df_result.at[method, f"{CO2_UNIT} removed"]
|
| 539 |
+
row_pct.append(used / cap * 100)
|
| 540 |
+
row_exact.append(used)
|
| 541 |
+
matrix_pct.append(row_pct)
|
| 542 |
+
matrix_exact.append(row_exact)
|
| 543 |
y_labels.append(method)
|
| 544 |
|
| 545 |
+
df_heat = pd.DataFrame(matrix_pct, index=y_labels, columns=available)
|
| 546 |
+
df_exact = pd.DataFrame(matrix_exact, index=y_labels, columns=available)
|
| 547 |
|
| 548 |
fig = px.imshow(
|
| 549 |
df_heat, color_continuous_scale=PLOTLY_HEATMAP_SCALE, text_auto=".1f",
|
| 550 |
labels=dict(x="Resource", y="Method", color="% Used"),
|
| 551 |
)
|
| 552 |
+
matrix_exact_fmt = [[f"{v:.4f}" for v in row] for row in matrix_exact]
|
| 553 |
+
fig.update_traces(
|
| 554 |
+
customdata=matrix_exact_fmt,
|
| 555 |
+
hovertemplate="<b>%{y}</b><br>%{x}<br>% of cap: %{z:.1f}%<br>Exact: %{customdata}<extra></extra>",
|
| 556 |
+
)
|
| 557 |
apply_chart_style(fig, height=max(400, 60 + 30 * len(df_heat.index)))
|
| 558 |
st.plotly_chart(fig, use_container_width=True)
|
| 559 |
|
| 560 |
+
resource_to_group = {r: g for g, rlist in resource_groups.items() for r in rlist}
|
| 561 |
+
unit_map = {r: resource_unit_map.get(resource_to_group.get(r), "?") for r in available}
|
| 562 |
+
for batch in (custom_resources or []):
|
| 563 |
+
unit_map[batch["name"]] = resource_unit_map.get(
|
| 564 |
+
resource_to_group.get(batch["group"]), batch.get("unit", "?")
|
| 565 |
+
)
|
| 566 |
+
col_rename = {r: f"{r} ({unit_map.get(r, '?')})" for r in available}
|
| 567 |
+
|
| 568 |
+
df_export_pct = df_heat.reset_index().rename(columns={"index": "Method"})
|
| 569 |
+
df_export_exact = (
|
| 570 |
+
df_exact.reset_index()
|
| 571 |
+
.rename(columns={"index": "Method"})
|
| 572 |
+
.rename(columns=col_rename)
|
| 573 |
+
)
|
| 574 |
+
|
| 575 |
with st.expander("π View / download chart data"):
|
| 576 |
+
st.dataframe(df_export_exact, use_container_width=True, hide_index=True)
|
| 577 |
|
| 578 |
+
register_chart_data("Resource usage heatmap (% of cap)", df_export_pct)
|
| 579 |
+
register_chart_data("Resource usage exact values", df_export_exact)
|
| 580 |
|
| 581 |
def show_absolute_usage(allocations, method_costs, custom_resources=None, resource_usage=None):
|
| 582 |
# Normalize method_costs keys: strip legacy unit suffixes
|
|
|
|
| 638 |
register_chart_data("Absolute resource usage by method", df)
|
| 639 |
|
| 640 |
def show_resource_balance(latest):
|
| 641 |
+
st.markdown('<p class="crra-sub-heading">Resource balance: used vs available</p>', unsafe_allow_html=True)
|
| 642 |
resource_caps = latest["resource_caps"]
|
| 643 |
used_total = latest["resource_used_total"]
|
| 644 |
remaining = latest["resource_remaining"]
|
|
|
|
| 690 |
register_chart_data("Resource balance (used vs remaining)", df)
|
| 691 |
|
| 692 |
def show_bottleneck_analysis(latest):
|
| 693 |
+
st.markdown('<p class="crra-sub-heading">Resource sensitivity</p>', unsafe_allow_html=True)
|
| 694 |
st.markdown(
|
| 695 |
'<p class="caption">'
|
| 696 |
'For every resource: actual CDR gain if you add exactly +1 unit, recomputed with all other constraints in place.'
|
|
|
|
| 726 |
# βββ Save / export / reset βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 727 |
|
| 728 |
def save_scenario(latest):
|
| 729 |
+
st.markdown('<p class="crra-sub-heading">Save scenario</p>', unsafe_allow_html=True)
|
| 730 |
st.markdown('<p class="caption">Save the current scenario with inputs and outputs for comparison and/or export.</p>', unsafe_allow_html=True)
|
| 731 |
|
| 732 |
suggested = st.session_state.get("scenario_loaded_file", "").rsplit(".", 1)[0]
|
|
|
|
| 913 |
for group, resources in resource_groups.items()
|
| 914 |
for r in resources
|
| 915 |
}
|
| 916 |
+
st.markdown('<p class="crra-sub-heading">Export inputs</p>', unsafe_allow_html=True)
|
| 917 |
st.markdown('<p class="caption">Export only the resource capabilities, method constraints and coefficients β without results.</p>', unsafe_allow_html=True)
|
| 918 |
|
| 919 |
suggested = st.session_state.get("excel_loaded_file", "").rsplit(".", 1)[0]
|
|
|
|
| 1010 |
)
|
| 1011 |
|
| 1012 |
def export_all_charts():
|
| 1013 |
+
st.markdown('<p class="crra-sub-heading">Export all chart data</p>', unsafe_allow_html=True)
|
| 1014 |
st.markdown(
|
| 1015 |
'<p class="caption">Download a single file containing the underlying data '
|
| 1016 |
'of every chart shown in this session.</p>',
|
|
|
|
| 1070 |
)
|
| 1071 |
|
| 1072 |
def clear_all_data():
|
| 1073 |
+
for key in ["resource_caps", "constraints", "costs", "scenarios",
|
| 1074 |
+
"latest_result", "scenario_loaded_file", "scenario_name",
|
| 1075 |
+
"excel_loaded_file", "input_file_name", "resource_last_loaded_file",
|
| 1076 |
+
"resource_uploader_key", "custom_resources"]:
|
| 1077 |
st.session_state.pop(key, None)
|
| 1078 |
|
| 1079 |
for uploader_key in ["resource_uploader_key", "excel_uploader_key", "scenario_uploader_key"]:
|
|
|
|
| 1097 |
|
| 1098 |
def reset_scenario():
|
| 1099 |
st.divider()
|
| 1100 |
+
st.markdown('<p class="crra-sub-heading">Reset</p>', unsafe_allow_html=True)
|
| 1101 |
st.markdown('<p class="caption">Clear all current inputs, results and saved scenarios from the session.</p>', unsafe_allow_html=True)
|
| 1102 |
|
| 1103 |
if st.button("Clear all data", key="reset_scenario", use_container_width=False, type="primary", icon=":material/delete:"):
|
|
|
|
| 1176 |
|
| 1177 |
# ββ Results ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 1178 |
show_metrics(latest)
|
| 1179 |
+
|
| 1180 |
df_result = build_results_df(
|
| 1181 |
allocations,
|
| 1182 |
method_constraints=latest.get("method_constraints")
|
| 1183 |
)
|
|
|
|
| 1184 |
df_display = df_result.copy()
|
|
|
|
| 1185 |
df_display[f"{CO2_UNIT} removed"] = df_display[f"{CO2_UNIT} removed"].map(
|
| 1186 |
+
lambda x: f"{x:,.4f}" if pd.notna(x) else x
|
| 1187 |
)
|
| 1188 |
|
| 1189 |
+
tab_results, tab_resources, tab_bottleneck, tab_export = st.tabs([
|
| 1190 |
+
"π Method allocation",
|
| 1191 |
+
"πΏ Resource usage",
|
| 1192 |
+
"π Bottleneck analysis",
|
| 1193 |
+
"πΎ Export & Save",
|
| 1194 |
+
])
|
| 1195 |
+
|
| 1196 |
+
with tab_results:
|
| 1197 |
+
st.markdown('<p class="crra-sub-heading">Optimization Summary</p>', unsafe_allow_html=True)
|
| 1198 |
+
st.dataframe(df_display, use_container_width=True)
|
| 1199 |
+
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
| 1200 |
+
show_donut(df_result)
|
| 1201 |
+
|
| 1202 |
+
with tab_resources:
|
| 1203 |
+
st.markdown('<p class="crra-sub-heading">Resource usage heatmap (% of cap)</p>', unsafe_allow_html=True)
|
| 1204 |
+
show_heatmap(df_result, resource_caps, method_costs,
|
| 1205 |
+
custom_resources=latest.get("custom_resources", []),
|
| 1206 |
+
resource_usage=latest.get("resource_usage"))
|
| 1207 |
+
st.markdown('<p class="crra-sub-heading">Absolute resource usage by unit</p>', unsafe_allow_html=True)
|
| 1208 |
+
show_absolute_usage(allocations, method_costs,
|
| 1209 |
+
custom_resources=latest.get("custom_resources", []),
|
| 1210 |
+
resource_usage=latest.get("resource_usage"))
|
| 1211 |
+
show_resource_balance(latest)
|
| 1212 |
+
|
| 1213 |
+
with tab_bottleneck:
|
| 1214 |
+
show_bottleneck_analysis(latest)
|
| 1215 |
+
|
| 1216 |
+
with tab_export:
|
| 1217 |
+
save_scenario(latest)
|
| 1218 |
+
export_scenario()
|
| 1219 |
+
export_inputs_only()
|
| 1220 |
+
export_all_charts()
|
| 1221 |
+
reset_scenario()
|
tabs/tab5_sensitivity.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
-
import
|
| 2 |
import pandas as pd
|
| 3 |
-
import seaborn as sns
|
| 4 |
import streamlit as st
|
| 5 |
import plotly.express as px
|
| 6 |
|
| 7 |
-
from config.config import CO2_UNIT, method_order, resource_order
|
| 8 |
from optimization.optimization import run_optimization
|
| 9 |
from utils.ui_helpers import register_chart_data, show_bar
|
| 10 |
|
|
@@ -42,10 +41,17 @@ def render_tab():
|
|
| 42 |
)
|
| 43 |
baseline_removed = baseline_result["total_removed"] if baseline_success else 0
|
| 44 |
|
| 45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
print(resource_order)
|
| 48 |
-
print(st.session_state.get("custom_resources", []))
|
| 49 |
tab1, tab2 = st.tabs(["π Resource sensitivity", "π Method sensitivity"])
|
| 50 |
|
| 51 |
with tab1:
|
|
@@ -53,7 +59,7 @@ def render_tab():
|
|
| 53 |
<p class="crra-sub-heading">Impact of resource availability</p>""",
|
| 54 |
unsafe_allow_html=True,
|
| 55 |
)
|
| 56 |
-
selected_resources = st.multiselect("Select one or more resources:",
|
| 57 |
|
| 58 |
if not selected_resources:
|
| 59 |
st.warning("Please select at least one resource to run the sensitivity analysis.")
|
|
@@ -61,11 +67,18 @@ def render_tab():
|
|
| 61 |
percentages = list(range(0, 210, 10))
|
| 62 |
all_data = []
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
for resource in selected_resources:
|
|
|
|
|
|
|
| 65 |
for pct in percentages:
|
| 66 |
temp_caps = st.session_state.resource_caps.copy()
|
| 67 |
-
|
| 68 |
-
temp_caps[resource] =
|
| 69 |
|
| 70 |
success, result = run_optimization(
|
| 71 |
resource_caps=temp_caps,
|
|
@@ -78,105 +91,95 @@ def render_tab():
|
|
| 78 |
all_data.append({
|
| 79 |
"Input": resource,
|
| 80 |
"% of cap": pct,
|
|
|
|
|
|
|
| 81 |
f"COβ removed ({CO2_UNIT})": co2_removed
|
| 82 |
})
|
| 83 |
|
| 84 |
df = pd.DataFrame(all_data)
|
| 85 |
st.markdown("#### Sensitivity to resource availability")
|
| 86 |
-
fig = px.line(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
fig.add_hline(y=baseline_removed, line_dash="dash", line_color="gray",
|
| 88 |
annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
|
| 89 |
st.plotly_chart(fig, use_container_width=True)
|
| 90 |
with st.expander("π View / download chart data"):
|
| 91 |
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 92 |
-
register_chart_data("Sensitivity to resource availability
|
| 93 |
|
| 94 |
with tab2:
|
| 95 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 96 |
selected_methods = st.multiselect("Select one or more methods:", method_order, key="crra-select-method")
|
| 97 |
|
| 98 |
if not selected_methods:
|
| 99 |
st.warning("Please select at least one method to run the sensitivity analysis.")
|
| 100 |
else:
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
if percent_methods:
|
| 105 |
-
st.markdown("#### Sensitivity to % cap")
|
| 106 |
-
percentages = list(range(0, 110, 10))
|
| 107 |
-
all_data = []
|
| 108 |
-
|
| 109 |
-
for method in percent_methods:
|
| 110 |
-
for pct in percentages:
|
| 111 |
-
temp_constraints = st.session_state.constraints.copy()
|
| 112 |
-
temp_constraints[method]["cap_type"] = "percent"
|
| 113 |
-
temp_constraints[method]["cap_value"] = pct
|
| 114 |
-
|
| 115 |
-
success, result = run_optimization(
|
| 116 |
-
resource_caps=st.session_state.resource_caps,
|
| 117 |
-
method_constraints=temp_constraints,
|
| 118 |
-
method_costs=st.session_state.costs,
|
| 119 |
-
custom_resources=st.session_state.get("custom_resources", []),
|
| 120 |
-
)
|
| 121 |
-
co2_removed = result["total_removed"] if success else 0
|
| 122 |
-
all_data.append({
|
| 123 |
-
"Input": method,
|
| 124 |
-
"% max allocation": pct,
|
| 125 |
-
f"COβ removed ({CO2_UNIT})": co2_removed
|
| 126 |
-
})
|
| 127 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
df = pd.DataFrame(all_data)
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
st.plotly_chart(fig, use_container_width=True)
|
| 139 |
with st.expander("π View / download chart data"):
|
| 140 |
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 141 |
-
register_chart_data("Sensitivity to
|
| 142 |
-
|
| 143 |
-
if absolute_methods:
|
| 144 |
-
st.markdown(f"#### Sensitivity to absolute cap ({CO2_UNIT}/yr)")
|
| 145 |
-
all_data = []
|
| 146 |
-
|
| 147 |
-
for method in absolute_methods:
|
| 148 |
-
current_cap = st.session_state.constraints[method].get("cap_value", 0.0)
|
| 149 |
-
# Varier de 0 Γ 200% de la valeur actuelle en 11 points
|
| 150 |
-
if current_cap > 0:
|
| 151 |
-
values = [current_cap * pct / 100 for pct in range(0, 210, 20)]
|
| 152 |
-
else:
|
| 153 |
-
st.warning(f"Method **{method}** has an absolute cap of 0 β set a value first.", icon="β οΈ")
|
| 154 |
-
continue
|
| 155 |
-
|
| 156 |
-
for val in values:
|
| 157 |
-
temp_constraints = st.session_state.constraints.copy()
|
| 158 |
-
temp_constraints[method]["cap_type"] = "absolute"
|
| 159 |
-
temp_constraints[method]["cap_value"] = val
|
| 160 |
-
|
| 161 |
-
success, result = run_optimization(
|
| 162 |
-
resource_caps=st.session_state.resource_caps,
|
| 163 |
-
method_constraints=temp_constraints,
|
| 164 |
-
method_costs=st.session_state.costs,
|
| 165 |
-
custom_resources=st.session_state.get("custom_resources", []),
|
| 166 |
-
)
|
| 167 |
-
co2_removed = result["total_removed"] if success else 0
|
| 168 |
-
all_data.append({
|
| 169 |
-
"Input": method,
|
| 170 |
-
f"Cap ({CO2_UNIT}/yr)": val,
|
| 171 |
-
f"COβ removed ({CO2_UNIT})": co2_removed
|
| 172 |
-
})
|
| 173 |
-
|
| 174 |
-
if all_data:
|
| 175 |
-
df = pd.DataFrame(all_data)
|
| 176 |
-
fig = px.line(df, x="Cap ({CO2_UNIT}/yr)", y=f"COβ removed ({CO2_UNIT})", color="Input", markers=True)
|
| 177 |
-
fig.add_hline(y=baseline_removed, line_dash="dash", line_color="gray",
|
| 178 |
-
annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
|
| 179 |
-
st.plotly_chart(fig, use_container_width=True)
|
| 180 |
-
with st.expander("π View / download chart data"):
|
| 181 |
-
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 182 |
-
register_chart_data("Sensitivity to absolute cap", df)
|
|
|
|
| 1 |
+
import copy
|
| 2 |
import pandas as pd
|
|
|
|
| 3 |
import streamlit as st
|
| 4 |
import plotly.express as px
|
| 5 |
|
| 6 |
+
from config.config import CO2_UNIT, method_order, resource_groups, resource_order, resource_unit_map
|
| 7 |
from optimization.optimization import run_optimization
|
| 8 |
from utils.ui_helpers import register_chart_data, show_bar
|
| 9 |
|
|
|
|
| 41 |
)
|
| 42 |
baseline_removed = baseline_result["total_removed"] if baseline_success else 0
|
| 43 |
|
| 44 |
+
custom_resource_names = [b["name"] for b in st.session_state.get("custom_resources", [])]
|
| 45 |
+
all_resources = resource_order + custom_resource_names
|
| 46 |
+
|
| 47 |
+
resource_to_unit = {
|
| 48 |
+
r: resource_unit_map[group]
|
| 49 |
+
for group, resources in resource_groups.items()
|
| 50 |
+
for r in resources
|
| 51 |
+
}
|
| 52 |
+
for b in st.session_state.get("custom_resources", []):
|
| 53 |
+
resource_to_unit[b["name"]] = b.get("unit", "")
|
| 54 |
|
|
|
|
|
|
|
| 55 |
tab1, tab2 = st.tabs(["π Resource sensitivity", "π Method sensitivity"])
|
| 56 |
|
| 57 |
with tab1:
|
|
|
|
| 59 |
<p class="crra-sub-heading">Impact of resource availability</p>""",
|
| 60 |
unsafe_allow_html=True,
|
| 61 |
)
|
| 62 |
+
selected_resources = st.multiselect("Select one or more resources:", all_resources, key="crra-select-resource")
|
| 63 |
|
| 64 |
if not selected_resources:
|
| 65 |
st.warning("Please select at least one resource to run the sensitivity analysis.")
|
|
|
|
| 67 |
percentages = list(range(0, 210, 10))
|
| 68 |
all_data = []
|
| 69 |
|
| 70 |
+
custom_amount_by_name = {
|
| 71 |
+
b["name"]: float(b.get("amount", 0))
|
| 72 |
+
for b in st.session_state.get("custom_resources", [])
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
for resource in selected_resources:
|
| 76 |
+
unit = resource_to_unit.get(resource, "")
|
| 77 |
+
base_cap = st.session_state.resource_caps.get(resource) or custom_amount_by_name.get(resource, 0)
|
| 78 |
for pct in percentages:
|
| 79 |
temp_caps = st.session_state.resource_caps.copy()
|
| 80 |
+
actual_amount = base_cap * pct / 100
|
| 81 |
+
temp_caps[resource] = actual_amount
|
| 82 |
|
| 83 |
success, result = run_optimization(
|
| 84 |
resource_caps=temp_caps,
|
|
|
|
| 91 |
all_data.append({
|
| 92 |
"Input": resource,
|
| 93 |
"% of cap": pct,
|
| 94 |
+
"Actual amount": round(actual_amount, 4),
|
| 95 |
+
"Unit": unit,
|
| 96 |
f"COβ removed ({CO2_UNIT})": co2_removed
|
| 97 |
})
|
| 98 |
|
| 99 |
df = pd.DataFrame(all_data)
|
| 100 |
st.markdown("#### Sensitivity to resource availability")
|
| 101 |
+
fig = px.line(
|
| 102 |
+
df, x="% of cap", y=f"COβ removed ({CO2_UNIT})", color="Input", markers=True,
|
| 103 |
+
hover_data={"Actual amount": ":.4f", "Unit": True},
|
| 104 |
+
)
|
| 105 |
+
fig.update_layout(
|
| 106 |
+
xaxis_title="Resource availability (% of current cap)",
|
| 107 |
+
yaxis_title=f"Total portfolio COβ removal ({CO2_UNIT})",
|
| 108 |
+
)
|
| 109 |
fig.add_hline(y=baseline_removed, line_dash="dash", line_color="gray",
|
| 110 |
annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
|
| 111 |
st.plotly_chart(fig, use_container_width=True)
|
| 112 |
with st.expander("π View / download chart data"):
|
| 113 |
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 114 |
+
register_chart_data("Sensitivity to resource availability", df)
|
| 115 |
|
| 116 |
with tab2:
|
| 117 |
+
st.markdown("""
|
| 118 |
+
<p class="crra-sub-heading">Impact of max allocation per method</p>""",
|
| 119 |
+
unsafe_allow_html=True,
|
| 120 |
+
)
|
| 121 |
selected_methods = st.multiselect("Select one or more methods:", method_order, key="crra-select-method")
|
| 122 |
|
| 123 |
if not selected_methods:
|
| 124 |
st.warning("Please select at least one method to run the sensitivity analysis.")
|
| 125 |
else:
|
| 126 |
+
percentages = list(range(0, 110, 10))
|
| 127 |
+
all_data = []
|
| 128 |
+
skipped = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
+
for method in selected_methods:
|
| 131 |
+
cap_type = st.session_state.constraints[method].get("cap_type", "percent")
|
| 132 |
+
current_cap = st.session_state.constraints[method].get("cap_value", 100 if cap_type == "percent" else 0.0)
|
| 133 |
+
|
| 134 |
+
if current_cap <= 0:
|
| 135 |
+
skipped.append(method)
|
| 136 |
+
continue
|
| 137 |
+
|
| 138 |
+
for pct in percentages:
|
| 139 |
+
temp_constraints = copy.deepcopy(st.session_state.constraints)
|
| 140 |
+
if cap_type == "percent":
|
| 141 |
+
actual_cap = min(100, current_cap * pct / 100)
|
| 142 |
+
temp_constraints[method]["cap_value"] = actual_cap
|
| 143 |
+
cap_label = f"{actual_cap:.0f}% of potential"
|
| 144 |
+
else:
|
| 145 |
+
actual_cap = current_cap * pct / 100
|
| 146 |
+
temp_constraints[method]["cap_value"] = actual_cap
|
| 147 |
+
cap_label = f"{actual_cap:.2f} {CO2_UNIT}/yr"
|
| 148 |
+
|
| 149 |
+
success, result = run_optimization(
|
| 150 |
+
resource_caps=st.session_state.resource_caps,
|
| 151 |
+
method_constraints=temp_constraints,
|
| 152 |
+
method_costs=st.session_state.costs,
|
| 153 |
+
custom_resources=st.session_state.get("custom_resources", []),
|
| 154 |
+
)
|
| 155 |
+
co2_removed = result["total_removed"] if success else 0
|
| 156 |
+
all_data.append({
|
| 157 |
+
"Method": method,
|
| 158 |
+
"Cap type": cap_type,
|
| 159 |
+
"% of current cap": pct,
|
| 160 |
+
"Actual cap": cap_label,
|
| 161 |
+
f"COβ removed ({CO2_UNIT})": co2_removed,
|
| 162 |
+
})
|
| 163 |
+
|
| 164 |
+
for m in skipped:
|
| 165 |
+
st.warning(f"Method **{m}** has a cap of 0 β set a value first to include it.", icon="β οΈ")
|
| 166 |
+
|
| 167 |
+
if all_data:
|
| 168 |
df = pd.DataFrame(all_data)
|
| 169 |
+
fig = px.line(
|
| 170 |
+
df, x="% of current cap", y=f"COβ removed ({CO2_UNIT})",
|
| 171 |
+
color="Method", markers=True,
|
| 172 |
+
hover_data={"Actual cap": True, "Cap type": True},
|
| 173 |
+
)
|
| 174 |
+
fig.add_hline(
|
| 175 |
+
y=baseline_removed, line_dash="dash", line_color="lightgray",
|
| 176 |
+
annotation_text=f"Baseline: {baseline_removed:,} {CO2_UNIT}",
|
| 177 |
+
)
|
| 178 |
+
fig.update_layout(
|
| 179 |
+
xaxis_title="% of current cap setting",
|
| 180 |
+
yaxis_title=f"Total portfolio COβ removal ({CO2_UNIT})",
|
| 181 |
+
)
|
| 182 |
st.plotly_chart(fig, use_container_width=True)
|
| 183 |
with st.expander("π View / download chart data"):
|
| 184 |
st.dataframe(df, use_container_width=True, hide_index=True)
|
| 185 |
+
register_chart_data("Sensitivity to method cap", df)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tabs/tab6_scenarios.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
import json
|
| 2 |
|
| 3 |
-
import matplotlib.pyplot as plt
|
| 4 |
import pandas as pd
|
| 5 |
-
import
|
| 6 |
import streamlit as st
|
| 7 |
from config.config import CO2_UNIT
|
| 8 |
from utils.data_utils import is_valid_scenario
|
|
@@ -94,13 +93,10 @@ def _prepare_comparison_data(selected):
|
|
| 94 |
def _show_method_allocations(df):
|
| 95 |
if df.empty:
|
| 96 |
return
|
| 97 |
-
st.
|
| 98 |
-
fig
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
ax.set_ylabel(f"{CO2_UNIT}")
|
| 102 |
-
plt.xticks(rotation=45, ha="right")
|
| 103 |
-
st.pyplot(fig)
|
| 104 |
|
| 105 |
|
| 106 |
|
|
@@ -110,25 +106,18 @@ def _show_resource_usage(df):
|
|
| 110 |
st.info("No resource usage data available (method_costs missing from scenarios?).")
|
| 111 |
return
|
| 112 |
|
| 113 |
-
st.
|
| 114 |
for method in df["Method"].unique():
|
| 115 |
df_m = df[df["Method"] == method]
|
| 116 |
-
fig
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
plt.xticks(rotation=45, ha="right")
|
| 122 |
-
st.pyplot(fig)
|
| 123 |
-
|
| 124 |
-
st.subheader("Total Resource Use per Scenario")
|
| 125 |
df_total = df.groupby(["Scenario", "Resource"]).sum(numeric_only=True).reset_index()
|
| 126 |
-
fig
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
ax.set_ylabel("Total Used")
|
| 130 |
-
plt.xticks(rotation=45, ha="right")
|
| 131 |
-
st.pyplot(fig)
|
| 132 |
|
| 133 |
|
| 134 |
# βββ Main tab entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -146,7 +135,7 @@ def render_tab():
|
|
| 146 |
return
|
| 147 |
|
| 148 |
# Select & manage scenarios
|
| 149 |
-
st.markdown("
|
| 150 |
scenario_names = list(st.session_state.scenarios.keys())
|
| 151 |
selected = st.multiselect("Select scenarios", scenario_names, default=scenario_names)
|
| 152 |
|
|
@@ -166,7 +155,7 @@ def render_tab():
|
|
| 166 |
# Comparison
|
| 167 |
df_summary, df_methods, df_resources = _prepare_comparison_data(selected)
|
| 168 |
|
| 169 |
-
st.
|
| 170 |
st.dataframe(df_summary.style.format(na_rep="-", thousands=","), use_container_width=True)
|
| 171 |
st.download_button(
|
| 172 |
"Export Comparison CSV",
|
|
|
|
| 1 |
import json
|
| 2 |
|
|
|
|
| 3 |
import pandas as pd
|
| 4 |
+
import plotly.express as px
|
| 5 |
import streamlit as st
|
| 6 |
from config.config import CO2_UNIT
|
| 7 |
from utils.data_utils import is_valid_scenario
|
|
|
|
| 93 |
def _show_method_allocations(df):
|
| 94 |
if df.empty:
|
| 95 |
return
|
| 96 |
+
st.markdown("""<p class="crra-sub-heading">Method Allocations</p>""", unsafe_allow_html=True)
|
| 97 |
+
fig = px.bar(df, x="Method", y=f"{CO2_UNIT}", color="Scenario", barmode="group")
|
| 98 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 99 |
+
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
|
| 102 |
|
|
|
|
| 106 |
st.info("No resource usage data available (method_costs missing from scenarios?).")
|
| 107 |
return
|
| 108 |
|
| 109 |
+
st.markdown("""<p class="crra-sub-heading">Resource Use per Method</p>""", unsafe_allow_html=True)
|
| 110 |
for method in df["Method"].unique():
|
| 111 |
df_m = df[df["Method"] == method]
|
| 112 |
+
fig = px.bar(df_m, x="Resource", y="Used", color="Scenario", barmode="group", title=method)
|
| 113 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 114 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 115 |
+
|
| 116 |
+
st.markdown("""<p class="crra-sub-heading">Total Resource Use per Scenario</p>""", unsafe_allow_html=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
df_total = df.groupby(["Scenario", "Resource"]).sum(numeric_only=True).reset_index()
|
| 118 |
+
fig = px.bar(df_total, x="Scenario", y="Used", color="Resource", barmode="group")
|
| 119 |
+
fig.update_layout(xaxis_tickangle=-45)
|
| 120 |
+
st.plotly_chart(fig, use_container_width=True)
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
|
| 123 |
# βββ Main tab entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 135 |
return
|
| 136 |
|
| 137 |
# Select & manage scenarios
|
| 138 |
+
st.markdown("""<p class="crra-sub-heading">Select Scenarios to Compare</p>""", unsafe_allow_html=True)
|
| 139 |
scenario_names = list(st.session_state.scenarios.keys())
|
| 140 |
selected = st.multiselect("Select scenarios", scenario_names, default=scenario_names)
|
| 141 |
|
|
|
|
| 155 |
# Comparison
|
| 156 |
df_summary, df_methods, df_resources = _prepare_comparison_data(selected)
|
| 157 |
|
| 158 |
+
st.markdown("""<p class="crra-sub-heading">Summary Table</p>""", unsafe_allow_html=True)
|
| 159 |
st.dataframe(df_summary.style.format(na_rep="-", thousands=","), use_container_width=True)
|
| 160 |
st.download_button(
|
| 161 |
"Export Comparison CSV",
|
utils/data_utils.py
CHANGED
|
@@ -5,7 +5,7 @@ import pandas as pd
|
|
| 5 |
import streamlit as st
|
| 6 |
import openpyxl
|
| 7 |
from openpyxl.styles import PatternFill, Font
|
| 8 |
-
from config.config import resource_unit_map
|
| 9 |
|
| 10 |
@st.cache_data
|
| 11 |
def load_slider_data():
|
|
@@ -19,17 +19,20 @@ def render_resource_input(name_clean, r, tooltip=None):
|
|
| 19 |
widget_key = f"cap_{r}"
|
| 20 |
|
| 21 |
if widget_key not in st.session_state:
|
| 22 |
-
|
|
|
|
| 23 |
|
| 24 |
cap = st.number_input(
|
| 25 |
label=name_clean,
|
| 26 |
min_value=0.0,
|
| 27 |
format="%.2f",
|
|
|
|
|
|
|
| 28 |
key=widget_key,
|
| 29 |
help=tooltip
|
| 30 |
)
|
| 31 |
|
| 32 |
-
st.session_state.resource_caps[r] = cap
|
| 33 |
|
| 34 |
def is_valid_scenario(scenario):
|
| 35 |
base_ok = (
|
|
@@ -76,23 +79,33 @@ def load_inputs_from_excel(file):
|
|
| 76 |
costs.setdefault(m, {})[r] = v
|
| 77 |
|
| 78 |
custom_resources = []
|
|
|
|
|
|
|
|
|
|
| 79 |
if "custom_resources" in xl:
|
| 80 |
df_custom = xl["custom_resources"]
|
| 81 |
required_custom = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
|
| 82 |
if required_custom.issubset(df_custom.columns):
|
| 83 |
for bname in df_custom["name"].dropna().unique():
|
| 84 |
df_b = df_custom[df_custom["name"] == bname]
|
| 85 |
-
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
"min": float(row["min"]),
|
| 88 |
"median": float(row["median"]),
|
| 89 |
"max": float(row["max"]),
|
| 90 |
}
|
| 91 |
-
for _, row in df_b.iterrows()
|
| 92 |
-
}
|
| 93 |
custom_resources.append({
|
| 94 |
"name": str(bname),
|
| 95 |
-
"group":
|
| 96 |
"amount": float(df_b["amount"].iloc[0]),
|
| 97 |
"unit": str(df_b["unit"].iloc[0]),
|
| 98 |
"methods": methods,
|
|
@@ -110,7 +123,7 @@ def load_inputs_from_excel(file):
|
|
| 110 |
r, m = row["resource"], row["method"]
|
| 111 |
enabled_methods.setdefault(r, {})[m] = bool(row["enabled"])
|
| 112 |
|
| 113 |
-
return resource_caps, constraints, costs, custom_resources, enabled_methods
|
| 114 |
|
| 115 |
def strip_unit_suffix(resource_name: str) -> str:
|
| 116 |
"""Remove legacy unit suffix from resource name, e.g. 'Unspecified land (ha/tCO2)' -> 'Unspecified land'"""
|
|
|
|
| 5 |
import streamlit as st
|
| 6 |
import openpyxl
|
| 7 |
from openpyxl.styles import PatternFill, Font
|
| 8 |
+
from config.config import resource_unit_map, resource_order, method_order
|
| 9 |
|
| 10 |
@st.cache_data
|
| 11 |
def load_slider_data():
|
|
|
|
| 19 |
widget_key = f"cap_{r}"
|
| 20 |
|
| 21 |
if widget_key not in st.session_state:
|
| 22 |
+
existing = st.session_state.resource_caps.get(r, 0.0)
|
| 23 |
+
st.session_state[widget_key] = existing if existing else None
|
| 24 |
|
| 25 |
cap = st.number_input(
|
| 26 |
label=name_clean,
|
| 27 |
min_value=0.0,
|
| 28 |
format="%.2f",
|
| 29 |
+
value=None,
|
| 30 |
+
placeholder="0.00",
|
| 31 |
key=widget_key,
|
| 32 |
help=tooltip
|
| 33 |
)
|
| 34 |
|
| 35 |
+
st.session_state.resource_caps[r] = cap if cap is not None else 0.0
|
| 36 |
|
| 37 |
def is_valid_scenario(scenario):
|
| 38 |
base_ok = (
|
|
|
|
| 79 |
costs.setdefault(m, {})[r] = v
|
| 80 |
|
| 81 |
custom_resources = []
|
| 82 |
+
warnings = []
|
| 83 |
+
valid_resources = set(resource_order)
|
| 84 |
+
valid_methods = set(method_order)
|
| 85 |
if "custom_resources" in xl:
|
| 86 |
df_custom = xl["custom_resources"]
|
| 87 |
required_custom = {"name", "group", "amount", "unit", "method", "min", "median", "max"}
|
| 88 |
if required_custom.issubset(df_custom.columns):
|
| 89 |
for bname in df_custom["name"].dropna().unique():
|
| 90 |
df_b = df_custom[df_custom["name"] == bname]
|
| 91 |
+
group = str(df_b["group"].iloc[0])
|
| 92 |
+
if group not in valid_resources:
|
| 93 |
+
warnings.append(f"New resource \"{bname}\" - group \"{group}\" not found: check spelling.")
|
| 94 |
+
continue
|
| 95 |
+
methods = {}
|
| 96 |
+
for _, row in df_b.iterrows():
|
| 97 |
+
m = str(row["method"])
|
| 98 |
+
if m not in valid_methods:
|
| 99 |
+
warnings.append(f"New resource \"{bname}\" - method \"{m}\" not found: check spelling.")
|
| 100 |
+
continue
|
| 101 |
+
methods[m] = {
|
| 102 |
"min": float(row["min"]),
|
| 103 |
"median": float(row["median"]),
|
| 104 |
"max": float(row["max"]),
|
| 105 |
}
|
|
|
|
|
|
|
| 106 |
custom_resources.append({
|
| 107 |
"name": str(bname),
|
| 108 |
+
"group": group,
|
| 109 |
"amount": float(df_b["amount"].iloc[0]),
|
| 110 |
"unit": str(df_b["unit"].iloc[0]),
|
| 111 |
"methods": methods,
|
|
|
|
| 123 |
r, m = row["resource"], row["method"]
|
| 124 |
enabled_methods.setdefault(r, {})[m] = bool(row["enabled"])
|
| 125 |
|
| 126 |
+
return resource_caps, constraints, costs, custom_resources, enabled_methods, warnings
|
| 127 |
|
| 128 |
def strip_unit_suffix(resource_name: str) -> str:
|
| 129 |
"""Remove legacy unit suffix from resource name, e.g. 'Unspecified land (ha/tCO2)' -> 'Unspecified land'"""
|