Spaces:
Sleeping
Sleeping
[FEAT] Add OR-substitutable resource family pools and rename inputs.csv
Browse files- app.py +3 -3
- data/.csv_hash +1 -1
- data/cost_sliders.json +16 -166
- data/generate_model_data.py +2 -2
- data/{inputs_aggregated.csv → inputs.csv} +80 -108
- data/method_groups.json +3 -10
- data/resource_template.xlsx +0 -0
- data/secondary_resources.json +2 -94
- data/ui_text.xlsx +0 -0
- data/update.py +2 -2
- optimization/optimization.py +240 -33
- tabs/tab2_constraints.py +86 -15
- tabs/tab4_optimization.py +13 -10
- utils/data_utils.py +1 -0
app.py
CHANGED
|
@@ -12,10 +12,10 @@ from utils.data_utils import load_slider_data, strip_unit_suffix
|
|
| 12 |
from utils.state_utils import init_custom_resources
|
| 13 |
from config.config import method_order, resource_order, secondary_resources
|
| 14 |
|
| 15 |
-
# ── Auto-update: regenerate JSON files if
|
| 16 |
# Uses a hash file to detect changes — works both locally and on Hugging Face.
|
| 17 |
# Must run before importing config/tabs, which read the generated JSON files.
|
| 18 |
-
csv = "data/
|
| 19 |
hash_file = "data/.csv_hash"
|
| 20 |
generated = [
|
| 21 |
"data/cost_sliders.json",
|
|
@@ -35,7 +35,7 @@ if os.path.exists(csv):
|
|
| 35 |
missing = any(not os.path.exists(f) for f in generated)
|
| 36 |
|
| 37 |
if current_hash != stored_hash or missing:
|
| 38 |
-
print("
|
| 39 |
subprocess.run([sys.executable, "data/update.py"], check=True)
|
| 40 |
with open(hash_file, "w") as file:
|
| 41 |
file.write(current_hash)
|
|
|
|
| 12 |
from utils.state_utils import init_custom_resources
|
| 13 |
from config.config import method_order, resource_order, secondary_resources
|
| 14 |
|
| 15 |
+
# ── Auto-update: regenerate JSON files if inputs.csv has changed ───
|
| 16 |
# Uses a hash file to detect changes — works both locally and on Hugging Face.
|
| 17 |
# Must run before importing config/tabs, which read the generated JSON files.
|
| 18 |
+
csv = "data/inputs.csv"
|
| 19 |
hash_file = "data/.csv_hash"
|
| 20 |
generated = [
|
| 21 |
"data/cost_sliders.json",
|
|
|
|
| 35 |
missing = any(not os.path.exists(f) for f in generated)
|
| 36 |
|
| 37 |
if current_hash != stored_hash or missing:
|
| 38 |
+
print("inputs.csv has changed — running data/update.py ...")
|
| 39 |
subprocess.run([sys.executable, "data/update.py"], check=True)
|
| 40 |
with open(hash_file, "w") as file:
|
| 41 |
file.write(current_hash)
|
data/.csv_hash
CHANGED
|
@@ -1 +1 @@
|
|
| 1 |
-
|
|
|
|
| 1 |
+
954594a32cc4a1765412a40054f7e4fb
|
data/cost_sliders.json
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
{
|
| 2 |
-
"Afforestation": {
|
| 3 |
"Other land (Mha/MtCO2)": {
|
| 4 |
"min": 0.002,
|
| 5 |
"median": 0.094927,
|
| 6 |
"max": 0.384615
|
| 7 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
"Other water (bcm/MtCO2)": {
|
| 9 |
"min": 0.046,
|
| 10 |
"median": 0.092,
|
|
@@ -25,98 +30,29 @@
|
|
| 25 |
"max": 500000.0
|
| 26 |
},
|
| 27 |
"Forest biomass (Mt/MtCO2)": {
|
| 28 |
-
"min": 0.
|
| 29 |
-
"median":
|
| 30 |
-
"max":
|
| 31 |
-
},
|
| 32 |
-
"Other land (Mha/MtCO2)": {
|
| 33 |
-
"min": 0.056402,
|
| 34 |
-
"median": 0.163201,
|
| 35 |
-
"max": 0.27
|
| 36 |
-
},
|
| 37 |
-
"Other water (bcm/MtCO2)": {
|
| 38 |
-
"min": -0.0005,
|
| 39 |
-
"median": 0.0001,
|
| 40 |
-
"max": 0.003
|
| 41 |
-
}
|
| 42 |
-
},
|
| 43 |
-
"Bio-char - Non-forest biomass": {
|
| 44 |
-
"Electrical energy (MWh/MtCO2)": {
|
| 45 |
-
"min": 0.0,
|
| 46 |
-
"median": 350.0,
|
| 47 |
-
"max": 500000.0
|
| 48 |
-
},
|
| 49 |
-
"Non-forest biomass (Mt/MtCO2)": {
|
| 50 |
-
"min": 0.818,
|
| 51 |
-
"median": 1.3395,
|
| 52 |
-
"max": 1.667
|
| 53 |
-
},
|
| 54 |
-
"Other land (Mha/MtCO2)": {
|
| 55 |
-
"min": 0.056402,
|
| 56 |
-
"median": 0.163201,
|
| 57 |
-
"max": 0.27
|
| 58 |
-
},
|
| 59 |
-
"Other water (bcm/MtCO2)": {
|
| 60 |
-
"min": -0.0005,
|
| 61 |
-
"median": 0.0001,
|
| 62 |
-
"max": 0.003
|
| 63 |
}
|
| 64 |
},
|
| 65 |
"Bio-char - Other biomass": {
|
| 66 |
"Electrical energy (MWh/MtCO2)": {
|
| 67 |
"min": 0.0,
|
| 68 |
-
"median":
|
| 69 |
"max": 500000.0
|
| 70 |
},
|
| 71 |
"Other biomass (Mt/MtCO2)": {
|
| 72 |
"min": 0.5,
|
| 73 |
"median": 1.209221,
|
| 74 |
"max": 12.86
|
| 75 |
-
},
|
| 76 |
-
"Other land (Mha/MtCO2)": {
|
| 77 |
-
"min": 0.056402,
|
| 78 |
-
"median": 0.163201,
|
| 79 |
-
"max": 0.27
|
| 80 |
-
},
|
| 81 |
-
"Other water (bcm/MtCO2)": {
|
| 82 |
-
"min": -0.0005,
|
| 83 |
-
"median": 0.0001,
|
| 84 |
-
"max": 0.003
|
| 85 |
}
|
| 86 |
},
|
| 87 |
"BioCCS with anaerobic digestion/fermentation": {
|
| 88 |
-
"
|
| 89 |
"min": 1.315,
|
| 90 |
"median": 1.561743,
|
| 91 |
"max": 8.0
|
| 92 |
},
|
| 93 |
-
"Geologic storage (Mt/MtCO2)": {
|
| 94 |
-
"min": 1.0,
|
| 95 |
-
"median": 1.0,
|
| 96 |
-
"max": 1.0
|
| 97 |
-
},
|
| 98 |
-
"Other biomass (Mt/MtCO2)": {
|
| 99 |
-
"min": 0.998094,
|
| 100 |
-
"median": 1.14,
|
| 101 |
-
"max": 2.0
|
| 102 |
-
},
|
| 103 |
-
"Other water (bcm/MtCO2)": {
|
| 104 |
-
"min": 0.368182,
|
| 105 |
-
"median": 0.565909,
|
| 106 |
-
"max": 0.913636
|
| 107 |
-
}
|
| 108 |
-
},
|
| 109 |
-
"BioCCS with anaerobic digestion/fermentation & growing biomass": {
|
| 110 |
-
"Arable land (Mha/MtCO2)": {
|
| 111 |
-
"min": 0.022803,
|
| 112 |
-
"median": 0.058844,
|
| 113 |
-
"max": 0.454545
|
| 114 |
-
},
|
| 115 |
-
"Other water (bcm/MtCO2)": {
|
| 116 |
-
"min": 0.368182,
|
| 117 |
-
"median": 0.565909,
|
| 118 |
-
"max": 0.913636
|
| 119 |
-
},
|
| 120 |
"Geologic storage (Mt/MtCO2)": {
|
| 121 |
"min": 1.0,
|
| 122 |
"median": 1.0,
|
|
@@ -148,55 +84,6 @@
|
|
| 148 |
"min": 0.666667,
|
| 149 |
"median": 1.14,
|
| 150 |
"max": 2.0
|
| 151 |
-
},
|
| 152 |
-
"Other chemical (Mt/MtCO2)": {
|
| 153 |
-
"min": 0.00013,
|
| 154 |
-
"median": 0.00316,
|
| 155 |
-
"max": 20.2
|
| 156 |
-
},
|
| 157 |
-
"Other water (bcm/MtCO2)": {
|
| 158 |
-
"min": 0.00063,
|
| 159 |
-
"median": 0.201625,
|
| 160 |
-
"max": 1.296342
|
| 161 |
-
}
|
| 162 |
-
},
|
| 163 |
-
"BioCCS with combustion & growing biomass": {
|
| 164 |
-
"Arable land (Mha/MtCO2)": {
|
| 165 |
-
"min": 0.022803,
|
| 166 |
-
"median": 0.0584,
|
| 167 |
-
"max": 1.9451
|
| 168 |
-
},
|
| 169 |
-
"Electrical energy (MWh/MtCO2)": {
|
| 170 |
-
"min": 0.0,
|
| 171 |
-
"median": 101000.0,
|
| 172 |
-
"max": 500000.0
|
| 173 |
-
},
|
| 174 |
-
"Thermal energy other grade (MWh/MtCO2)": {
|
| 175 |
-
"min": 0.0,
|
| 176 |
-
"median": 930611.1,
|
| 177 |
-
"max": 4000000.0
|
| 178 |
-
},
|
| 179 |
-
"Geologic storage (Mt/MtCO2)": {
|
| 180 |
-
"min": 1.0,
|
| 181 |
-
"median": 1.0,
|
| 182 |
-
"max": 1.0
|
| 183 |
-
},
|
| 184 |
-
"Other chemical (Mt/MtCO2)": {
|
| 185 |
-
"min": 0.00013,
|
| 186 |
-
"median": 0.00316,
|
| 187 |
-
"max": 20.2
|
| 188 |
-
},
|
| 189 |
-
"Other water (bcm/MtCO2)": {
|
| 190 |
-
"min": 0.00063,
|
| 191 |
-
"median": 0.201625,
|
| 192 |
-
"max": 1.296342
|
| 193 |
-
}
|
| 194 |
-
},
|
| 195 |
-
"Coastal revegetation (mangrove, seaweeds, etc.)": {
|
| 196 |
-
"Non-arable land (Mha/MtCO2)": {
|
| 197 |
-
"min": 0.0078,
|
| 198 |
-
"median": 0.120566,
|
| 199 |
-
"max": 0.328289
|
| 200 |
}
|
| 201 |
},
|
| 202 |
"Cropland management": {
|
|
@@ -212,11 +99,6 @@
|
|
| 212 |
"median": 4000000.0,
|
| 213 |
"max": 4000000.0
|
| 214 |
},
|
| 215 |
-
"Non-arable land (Mha/MtCO2)": {
|
| 216 |
-
"min": 3.0,
|
| 217 |
-
"median": 6.0,
|
| 218 |
-
"max": 6.0
|
| 219 |
-
},
|
| 220 |
"Other biomass (Mt/MtCO2)": {
|
| 221 |
"min": 0.0,
|
| 222 |
"median": 3.571429,
|
|
@@ -234,7 +116,7 @@
|
|
| 234 |
"median": 784125.0,
|
| 235 |
"max": 6222222.0
|
| 236 |
},
|
| 237 |
-
"
|
| 238 |
"min": 0.000223,
|
| 239 |
"median": 0.000324,
|
| 240 |
"max": 0.016
|
|
@@ -260,16 +142,6 @@
|
|
| 260 |
"min": 0.00071,
|
| 261 |
"median": 0.00101,
|
| 262 |
"max": 0.00131
|
| 263 |
-
},
|
| 264 |
-
"Shoreline (km/MtCO2)": {
|
| 265 |
-
"min": 0.006,
|
| 266 |
-
"median": 0.012,
|
| 267 |
-
"max": 0.012
|
| 268 |
-
},
|
| 269 |
-
"Other mineral (Mt/MtCO2)": {
|
| 270 |
-
"min": 2.4,
|
| 271 |
-
"median": 2.95,
|
| 272 |
-
"max": 3.5
|
| 273 |
}
|
| 274 |
},
|
| 275 |
"Enhanced weathering - Basalt mineral": {
|
|
@@ -285,7 +157,7 @@
|
|
| 285 |
},
|
| 286 |
"Electrical energy (MWh/MtCO2)": {
|
| 287 |
"min": 9250.0,
|
| 288 |
-
"median":
|
| 289 |
"max": 2775000.0
|
| 290 |
},
|
| 291 |
"Non-arable land (Mha/MtCO2)": {
|
|
@@ -307,7 +179,7 @@
|
|
| 307 |
},
|
| 308 |
"Electrical energy (MWh/MtCO2)": {
|
| 309 |
"min": 9250.0,
|
| 310 |
-
"median":
|
| 311 |
"max": 2775000.0
|
| 312 |
},
|
| 313 |
"Non-arable land (Mha/MtCO2)": {
|
|
@@ -334,7 +206,7 @@
|
|
| 334 |
},
|
| 335 |
"Electrical energy (MWh/MtCO2)": {
|
| 336 |
"min": 9250.0,
|
| 337 |
-
"median":
|
| 338 |
"max": 2775000.0
|
| 339 |
},
|
| 340 |
"Non-arable land (Mha/MtCO2)": {
|
|
@@ -361,7 +233,7 @@
|
|
| 361 |
},
|
| 362 |
"Electrical energy (MWh/MtCO2)": {
|
| 363 |
"min": 9250.0,
|
| 364 |
-
"median":
|
| 365 |
"max": 2775000.0
|
| 366 |
},
|
| 367 |
"Non-arable land (Mha/MtCO2)": {
|
|
@@ -461,11 +333,6 @@
|
|
| 461 |
"min": 1.4,
|
| 462 |
"median": 1.743,
|
| 463 |
"max": 5.0
|
| 464 |
-
},
|
| 465 |
-
"Other water (bcm/MtCO2)": {
|
| 466 |
-
"min": 0.000339,
|
| 467 |
-
"median": 0.000678,
|
| 468 |
-
"max": 0.000678
|
| 469 |
}
|
| 470 |
},
|
| 471 |
"Mineral looping DACCS": {
|
|
@@ -511,11 +378,6 @@
|
|
| 511 |
"median": 0.000436,
|
| 512 |
"max": 0.000991
|
| 513 |
},
|
| 514 |
-
"Olivine mineral (Mt/MtCO2)": {
|
| 515 |
-
"min": 0.4,
|
| 516 |
-
"median": 0.8,
|
| 517 |
-
"max": 0.8
|
| 518 |
-
},
|
| 519 |
"Thermal energy other grade (MWh/MtCO2)": {
|
| 520 |
"min": 400000.0,
|
| 521 |
"median": 450000.0,
|
|
@@ -561,18 +423,6 @@
|
|
| 561 |
"max": 2.289744
|
| 562 |
}
|
| 563 |
},
|
| 564 |
-
"Reforestation": {
|
| 565 |
-
"Other land (Mha/MtCO2)": {
|
| 566 |
-
"min": 0.002,
|
| 567 |
-
"median": 0.094927,
|
| 568 |
-
"max": 38.8
|
| 569 |
-
},
|
| 570 |
-
"Other water (bcm/MtCO2)": {
|
| 571 |
-
"min": 0.046,
|
| 572 |
-
"median": 0.092,
|
| 573 |
-
"max": 0.092
|
| 574 |
-
}
|
| 575 |
-
},
|
| 576 |
"pH swing": {
|
| 577 |
"Electrical energy (MWh/MtCO2)": {
|
| 578 |
"min": 660000.0,
|
|
|
|
| 1 |
{
|
| 2 |
+
"Afforestation/reforestation": {
|
| 3 |
"Other land (Mha/MtCO2)": {
|
| 4 |
"min": 0.002,
|
| 5 |
"median": 0.094927,
|
| 6 |
"max": 0.384615
|
| 7 |
},
|
| 8 |
+
"Arable land (Mha/MtCO2)": {
|
| 9 |
+
"min": 0.002,
|
| 10 |
+
"median": 0.094927,
|
| 11 |
+
"max": 0.384615
|
| 12 |
+
},
|
| 13 |
"Other water (bcm/MtCO2)": {
|
| 14 |
"min": 0.046,
|
| 15 |
"median": 0.092,
|
|
|
|
| 30 |
"max": 500000.0
|
| 31 |
},
|
| 32 |
"Forest biomass (Mt/MtCO2)": {
|
| 33 |
+
"min": 0.3,
|
| 34 |
+
"median": 1.5,
|
| 35 |
+
"max": 2.2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
}
|
| 37 |
},
|
| 38 |
"Bio-char - Other biomass": {
|
| 39 |
"Electrical energy (MWh/MtCO2)": {
|
| 40 |
"min": 0.0,
|
| 41 |
+
"median": 350000.0,
|
| 42 |
"max": 500000.0
|
| 43 |
},
|
| 44 |
"Other biomass (Mt/MtCO2)": {
|
| 45 |
"min": 0.5,
|
| 46 |
"median": 1.209221,
|
| 47 |
"max": 12.86
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
}
|
| 49 |
},
|
| 50 |
"BioCCS with anaerobic digestion/fermentation": {
|
| 51 |
+
"Other biomass (Mt/MtCO2)": {
|
| 52 |
"min": 1.315,
|
| 53 |
"median": 1.561743,
|
| 54 |
"max": 8.0
|
| 55 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
"Geologic storage (Mt/MtCO2)": {
|
| 57 |
"min": 1.0,
|
| 58 |
"median": 1.0,
|
|
|
|
| 84 |
"min": 0.666667,
|
| 85 |
"median": 1.14,
|
| 86 |
"max": 2.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
}
|
| 88 |
},
|
| 89 |
"Cropland management": {
|
|
|
|
| 99 |
"median": 4000000.0,
|
| 100 |
"max": 4000000.0
|
| 101 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
"Other biomass (Mt/MtCO2)": {
|
| 103 |
"min": 0.0,
|
| 104 |
"median": 3.571429,
|
|
|
|
| 116 |
"median": 784125.0,
|
| 117 |
"max": 6222222.0
|
| 118 |
},
|
| 119 |
+
"Other land (Mha/MtCO2)": {
|
| 120 |
"min": 0.000223,
|
| 121 |
"median": 0.000324,
|
| 122 |
"max": 0.016
|
|
|
|
| 142 |
"min": 0.00071,
|
| 143 |
"median": 0.00101,
|
| 144 |
"max": 0.00131
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
},
|
| 147 |
"Enhanced weathering - Basalt mineral": {
|
|
|
|
| 157 |
},
|
| 158 |
"Electrical energy (MWh/MtCO2)": {
|
| 159 |
"min": 9250.0,
|
| 160 |
+
"median": 64795.9,
|
| 161 |
"max": 2775000.0
|
| 162 |
},
|
| 163 |
"Non-arable land (Mha/MtCO2)": {
|
|
|
|
| 179 |
},
|
| 180 |
"Electrical energy (MWh/MtCO2)": {
|
| 181 |
"min": 9250.0,
|
| 182 |
+
"median": 64795.9,
|
| 183 |
"max": 2775000.0
|
| 184 |
},
|
| 185 |
"Non-arable land (Mha/MtCO2)": {
|
|
|
|
| 206 |
},
|
| 207 |
"Electrical energy (MWh/MtCO2)": {
|
| 208 |
"min": 9250.0,
|
| 209 |
+
"median": 64795.9,
|
| 210 |
"max": 2775000.0
|
| 211 |
},
|
| 212 |
"Non-arable land (Mha/MtCO2)": {
|
|
|
|
| 233 |
},
|
| 234 |
"Electrical energy (MWh/MtCO2)": {
|
| 235 |
"min": 9250.0,
|
| 236 |
+
"median": 64795.9,
|
| 237 |
"max": 2775000.0
|
| 238 |
},
|
| 239 |
"Non-arable land (Mha/MtCO2)": {
|
|
|
|
| 333 |
"min": 1.4,
|
| 334 |
"median": 1.743,
|
| 335 |
"max": 5.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 336 |
}
|
| 337 |
},
|
| 338 |
"Mineral looping DACCS": {
|
|
|
|
| 378 |
"median": 0.000436,
|
| 379 |
"max": 0.000991
|
| 380 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
"Thermal energy other grade (MWh/MtCO2)": {
|
| 382 |
"min": 400000.0,
|
| 383 |
"median": 450000.0,
|
|
|
|
| 423 |
"max": 2.289744
|
| 424 |
}
|
| 425 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 426 |
"pH swing": {
|
| 427 |
"Electrical energy (MWh/MtCO2)": {
|
| 428 |
"min": 660000.0,
|
data/generate_model_data.py
CHANGED
|
@@ -11,7 +11,7 @@ from utils.data_utils import strip_unit_suffix
|
|
| 11 |
|
| 12 |
def build_method_groups(df):
|
| 13 |
"""Build the method_groups dict from the CSV Method group and Implementation columns.
|
| 14 |
-
Group and method order follows first appearance in
|
| 15 |
method_groups = {}
|
| 16 |
for _, row in df.drop_duplicates(subset=["Implementation"]).iterrows():
|
| 17 |
m, g = row["Implementation"], row["Method group"]
|
|
@@ -30,7 +30,7 @@ def build_secondary_resources(df):
|
|
| 30 |
|
| 31 |
|
| 32 |
def create_sliders_inputs():
|
| 33 |
-
df = pd.read_csv("data/
|
| 34 |
|
| 35 |
cost_sliders = {}
|
| 36 |
|
|
|
|
| 11 |
|
| 12 |
def build_method_groups(df):
|
| 13 |
"""Build the method_groups dict from the CSV Method group and Implementation columns.
|
| 14 |
+
Group and method order follows first appearance in inputs.csv."""
|
| 15 |
method_groups = {}
|
| 16 |
for _, row in df.drop_duplicates(subset=["Implementation"]).iterrows():
|
| 17 |
m, g = row["Implementation"], row["Method group"]
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def create_sliders_inputs():
|
| 33 |
+
df = pd.read_csv("data/inputs.csv")
|
| 34 |
|
| 35 |
cost_sliders = {}
|
| 36 |
|
data/{inputs_aggregated.csv → inputs.csv}
RENAMED
|
@@ -1,109 +1,81 @@
|
|
| 1 |
Implementation,Method group,Resource,Resource group,Type,Unit,count,quantile_25,mean,median,quantile_75,std,min,max,Is secondary
|
| 2 |
-
Afforestation,Forestry,Other land,Land,Physical,Mha/MtCO2,36,0.024593,0.115652,0.094927,0.197075,0.112368,0.002,0.384615,
|
| 3 |
-
Afforestation,Forestry,
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Electrical energy,Energy,Energy,MWh/MtCO2,12,323625.0,1153583.0,606500.0,1755000.0,1286024.0,27000.0,4000000.0,True
|
| 83 |
-
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Non-arable land,Land,Physical,Mha/MtCO2,4,0.0,0.000505,0.000355,0.00086,0.000632,0.0,0.00131,True
|
| 84 |
-
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Thermal energy other grade,Energy,Energy,MWh/MtCO2,6,1388889.0,1257630.0,1458944.0,1529000.0,542588.8,160000.0,1550000.0,True
|
| 85 |
-
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Other mineral,Minerals,Physical,Mt/MtCO2,8,1.62,2.489,1.743,2.5895,1.554598,1.4,5.0,False
|
| 86 |
-
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Other water,Water,Physical,bcm/MtCO2,2,0.000678,0.000678,0.000678,0.000678,0.0,0.000678,0.000678,True
|
| 87 |
-
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,8,83333.33,1104167.0,83333.33,1375000.0,1793811.0,0.0,4000000.0,False
|
| 88 |
-
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,12,0.000587,0.001065,0.000639,0.000873,0.001064,0.000223,0.0033,False
|
| 89 |
-
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Thermal energy other grade,Energy,Energy,MWh/MtCO2,4,1479167.0,1715278.0,1819444.0,2055556.0,534015.3,1000000.0,2222222.0,False
|
| 90 |
-
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Other mineral,Minerals,Physical,Mt/MtCO2,4,0.15,0.225,0.2,0.275,0.206155,0.0,0.5,False
|
| 91 |
-
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 92 |
-
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,6,0.000125,0.006762,0.0002,0.010068,0.011116,0.0,0.026714,True
|
| 93 |
-
Mineralisation ex-situ,Mineralisation,Electrical energy,Energy,Energy,MWh/MtCO2,8,500000.0,1062500.0,750000.0,1250000.0,979704.8,0.0,3000000.0,False
|
| 94 |
-
Mineralisation ex-situ,Mineralisation,Non-arable land,Land,Physical,Mha/MtCO2,4,0.000298,0.000522,0.000436,0.000659,0.000342,0.000223,0.000991,False
|
| 95 |
-
Mineralisation ex-situ,Mineralisation,Olivine mineral,Minerals,Physical,Mt/MtCO2,2,0.8,0.8,0.8,0.8,0.0,0.8,0.8,False
|
| 96 |
-
Mineralisation ex-situ,Mineralisation,Thermal energy other grade,Energy,Energy,MWh/MtCO2,4,400000.0,575000.0,450000.0,625000.0,287228.1,400000.0,1000000.0,False
|
| 97 |
-
Mineralisation ex-situ,Mineralisation,Other mineral,Minerals,Physical,Mt/MtCO2,22,0.4375,3.935,3.0,7.0,3.647722,0.04,10.0,False
|
| 98 |
-
Mineralisation ex-situ,Mineralisation,Other water,Water,Physical,bcm/MtCO2,14,0.0002,0.050004,0.00545,0.0145,0.106908,0.0,0.3,False
|
| 99 |
-
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,4,316000.0,324666.7,324666.7,333333.3,10007.4,316000.0,333333.3,False
|
| 100 |
-
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,8,0.000169,0.000331,0.000272,0.00038,0.000319,8E-06,0.000991,False
|
| 101 |
-
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 102 |
-
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,4,-0.00015,0.000427,0.000427,0.001,0.000662,-0.00015,0.001,False
|
| 103 |
-
Pasture management,"Soil carbon sequestration (crop, grasslands)",Arable land,Land,Physical,Mha/MtCO2,24,0.102253,0.442368,0.252422,0.522974,0.522145,0.071894,2.289744,False
|
| 104 |
-
Reforestation,Forestry,Other land,Land,Physical,Mha/MtCO2,34,0.044459,1.564102,0.094927,0.1887,6.825803,0.002,38.8,False
|
| 105 |
-
Reforestation,Forestry,Other water,Water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0.0,0.092,0.092,True
|
| 106 |
-
pH swing,Direct ocean carbon capture and storage (DOCCS),Electrical energy,Energy,Energy,MWh/MtCO2,28,980000.0,2205112.0,1768000.0,3302500.0,1381909.0,660000.0,4670000.0,False
|
| 107 |
-
pH swing,Direct ocean carbon capture and storage (DOCCS),Salt water,Water,Physical,bcm/MtCO2,4,0.435,4.78,4.78,9.125,5.017174,0.435,9.125,False
|
| 108 |
-
pH swing,Direct ocean carbon capture and storage (DOCCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
|
| 109 |
-
pH swing,Direct ocean carbon capture and storage (DOCCS),Other water,Water,Physical,bcm/MtCO2,4,0.217391,6.608696,6.608696,13.0,7.380043,0.217391,13.0,False
|
|
|
|
| 1 |
Implementation,Method group,Resource,Resource group,Type,Unit,count,quantile_25,mean,median,quantile_75,std,min,max,Is secondary
|
| 2 |
+
Afforestation/reforestation,Forestry,Other land,Land,Physical,Mha/MtCO2,36,0.024593,0.115652,0.094927,0.197075,0.112368,0.002,0.384615,FALSE
|
| 3 |
+
Afforestation/reforestation,Forestry,Arable land,Land,Physical,Mha/MtCO2,36,0.024593,0.115652,0.094927,0.197075,0.112368,0.002,0.384615,FALSE
|
| 4 |
+
Afforestation/reforestation,Forestry,Other water,Water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0,0.092,0.092,TRUE
|
| 5 |
+
Agroforestry,Forestry,Arable land,Land,Physical,Mha/MtCO2,12,0.039953,0.450767,0.064101,0.2141,0.90114,0.0025,3.030303,FALSE
|
| 6 |
+
Bio-char - Forest biomass,Bio-char,Electrical energy,Electrical,Energy,MWh/MtCO2,6,87.5,167000,350,375000,258000,0,500000,TRUE
|
| 7 |
+
Bio-char - Forest biomass,Bio-char,Forest biomass,Biomass,Physical,Mt/MtCO2,6,0.5,1.5,1.5,2,0.4,0.3,2.2,FALSE
|
| 8 |
+
Bio-char - Other biomass,Bio-char,Electrical energy,Electrical,Energy,MWh/MtCO2,6,87.5,167000,350000,375000,258000,0,500000,TRUE
|
| 9 |
+
Bio-char - Other biomass,Bio-char,Other biomass,Biomass,Physical,Mt/MtCO2,48,0.943396,1.924255,1.209221,2.522177,1.937824,0.5,12.86,FALSE
|
| 10 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Other biomass,Biomass,Physical,Mt/MtCO2,6,1.428571,2.689964,1.561743,2.128274,2.62425,1.315,8,FALSE
|
| 11 |
+
BioCCS with anaerobic digestion/fermentation,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 12 |
+
BioCCS with combustion,Bio-CCS,Electrical energy,Electrical,Energy,MWh/MtCO2,18,27777.78,130975.3,101000,111111.1,162410.3,0,500000,TRUE
|
| 13 |
+
BioCCS with combustion,Bio-CCS,Forest biomass,Biomass,Physical,Mt/MtCO2,8,0.560764,0.675481,0.59375,0.838675,0.144419,0.555556,0.8547,FALSE
|
| 14 |
+
BioCCS with combustion,Bio-CCS,Thermal energy other grade,Thermal,Energy,MWh/MtCO2,22,347222.2,1006242,930611.1,1025000,1052440,0,4000000,TRUE
|
| 15 |
+
BioCCS with combustion,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 16 |
+
BioCCS with combustion,Bio-CCS,Other biomass,Biomass,Physical,Mt/MtCO2,8,0.916667,1.264167,1.14,1.625,0.528225,0.666667,2,FALSE
|
| 17 |
+
Cropland management,"Soil carbon sequestration (crop, grasslands)",Arable land,Land,Physical,Mha/MtCO2,14,0.27062,1.277946,0.420635,1.052335,2.329779,0.082645,9,FALSE
|
| 18 |
+
Durable biobased products,Durable biobased products,Electrical energy,Electrical,Energy,MWh/MtCO2,2,4000000,4000000,4000000,4000000,0,4000000,4000000,TRUE
|
| 19 |
+
Durable biobased products,Durable biobased products,Other biomass,Biomass,Physical,Mt/MtCO2,6,1.267857,2.607143,3.571429,3.892857,1.842663,0,4,FALSE
|
| 20 |
+
Durable biobased products,Durable biobased products,Other water,Water,Physical,bcm/MtCO2,2,0.0002,0.0003,0.0003,0.0004,0.000283,0.0001,0.0005,TRUE
|
| 21 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Electrical,Energy,MWh/MtCO2,10,333333.3,2032081,784125,2750000,2378833,252556,6222222,FALSE
|
| 22 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Other land,Land,Physical,Mha/MtCO2,14,0.000272,0.002662,0.000324,0.000881,0.005657,0.000223,0.016,TRUE
|
| 23 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 24 |
+
Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,6,0.002,0.011,0.005,0.0215,0.012522,0.001,0.027,FALSE
|
| 25 |
+
Electrochemical OAE,Ocean alkalinity enhancement,Electrical energy,Electrical,Energy,MWh/MtCO2,8,1600000,2241250,2150000,2500000,1283594,830000,5000000,FALSE
|
| 26 |
+
Electrochemical OAE,Ocean alkalinity enhancement,Non-arable land,Land,Physical,Mha/MtCO2,2,0.00086,0.00101,0.00101,0.00116,0.000424,0.00071,0.00131,TRUE
|
| 27 |
+
Enhanced weathering - Basalt mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1.00E-06,2.5,TRUE
|
| 28 |
+
Enhanced weathering - Basalt mineral,Enhanced weathering,Basalt mineral,Minerals,Physical,Mt/MtCO2,25,3.33,5.773042,4.3,5.3,4.967901,1,19,FALSE
|
| 29 |
+
Enhanced weathering - Basalt mineral,Enhanced weathering,Electrical energy,Electrical,Energy,MWh/MtCO2,8,37000,150000,64795.9,92591.8,200000,9250,2775000,FALSE
|
| 30 |
+
Enhanced weathering - Basalt mineral,Enhanced weathering,Non-arable land,Land,Physical,Mha/MtCO2,12,0.004429,11.43247,0.009065,28.57143,17.2264,0.0009,40,TRUE
|
| 31 |
+
Enhanced weathering - Basalt mineral,Enhanced weathering,Other water,Water,Physical,bcm/MtCO2,5,0.0005,0.010466,0.000866,0.000866,0.022102,0.0001,0.05,TRUE
|
| 32 |
+
Enhanced weathering - Olivine mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1.00E-06,2.5,TRUE
|
| 33 |
+
Enhanced weathering - Olivine mineral,Enhanced weathering,Electrical energy,Electrical,Energy,MWh/MtCO2,8,37000,150000,64795.9,92591.8,200000,9250,2775000,FALSE
|
| 34 |
+
Enhanced weathering - Olivine mineral,Enhanced weathering,Non-arable land,Land,Physical,Mha/MtCO2,12,0.004429,11.43247,0.009065,28.57143,17.2264,0.0009,40,TRUE
|
| 35 |
+
Enhanced weathering - Olivine mineral,Enhanced weathering,Olivine mineral,Minerals,Physical,Mt/MtCO2,26,0.909091,1.429511,1.25,1.58,0.941247,0.27027,3.7,FALSE
|
| 36 |
+
Enhanced weathering - Olivine mineral,Enhanced weathering,Other water,Water,Physical,bcm/MtCO2,5,0.0005,0.010466,0.000866,0.000866,0.022102,0.0001,0.05,TRUE
|
| 37 |
+
Enhanced weathering - Wollastonite mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1.00E-06,2.5,TRUE
|
| 38 |
+
Enhanced weathering - Wollastonite mineral,Enhanced weathering,Electrical energy,Electrical,Energy,MWh/MtCO2,8,37000,150000,64795.9,92591.8,200000,9250,2775000,FALSE
|
| 39 |
+
Enhanced weathering - Wollastonite mineral,Enhanced weathering,Non-arable land,Land,Physical,Mha/MtCO2,12,0.004429,11.43247,0.009065,28.57143,17.2264,0.0009,40,TRUE
|
| 40 |
+
Enhanced weathering - Wollastonite mineral,Enhanced weathering,Other water,Water,Physical,bcm/MtCO2,5,0.0005,0.010466,0.000866,0.000866,0.022102,0.0001,0.05,TRUE
|
| 41 |
+
Enhanced weathering - Wollastonite mineral,Enhanced weathering,Wollastonite mineral,Minerals,Physical,Mt/MtCO2,8,1.321429,1.629643,1.564286,1.83,0.742366,0.56,3,FALSE
|
| 42 |
+
Enhanced weathering - Other mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1.00E-06,2.5,TRUE
|
| 43 |
+
Enhanced weathering - Other mineral,Enhanced weathering,Electrical energy,Electrical,Energy,MWh/MtCO2,8,37000,150000,64795.9,92591.8,200000,9250,2775000,FALSE
|
| 44 |
+
Enhanced weathering - Other mineral,Enhanced weathering,Non-arable land,Land,Physical,Mha/MtCO2,12,0.004429,11.43247,0.009065,28.57143,17.2264,0.0009,40,TRUE
|
| 45 |
+
Enhanced weathering - Other mineral,Enhanced weathering,Other mineral,Minerals,Physical,Mt/MtCO2,19,1.315789,6.356736,2.5,8.625,7.726415,0.5,25,FALSE
|
| 46 |
+
Enhanced weathering - Other mineral,Enhanced weathering,Other water,Water,Physical,bcm/MtCO2,5,0.0005,0.010466,0.000866,0.000866,0.022102,0.0001,0.05,TRUE
|
| 47 |
+
Forest management,Forestry,Other land,Land,Physical,Mha/MtCO2,2,0.438675,0.76865,0.76865,1.098625,0.93331,0.1087,1.4286,FALSE
|
| 48 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Electrical energy,Electrical,Energy,MWh/MtCO2,32,361111.1,423787.8,462961.1,500000,163178.3,77000,770000,FALSE
|
| 49 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,23,2.20E-08,0.000229,0.000223,0.000298,0.000299,1.50E-10,0.000991,FALSE
|
| 50 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Thermal energy high grade,Thermal,Energy,MWh/MtCO2,38,1472222,1954835,1750000,2314815,643937.6,1125000,3800000,FALSE
|
| 51 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 52 |
+
High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,16,0.002502,0.010221,0.0047,0.00818,0.015887,5.00E-09,0.05,TRUE
|
| 53 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Electrical energy,Electrical,Energy,MWh/MtCO2,44,218000,895320.2,462961.1,690000,1255695,130000,4611111,FALSE
|
| 54 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,23,6.25E-08,0.000238,0.000223,0.000298,0.000294,1.00E-11,0.000991,FALSE
|
| 55 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Thermal energy low grade,Thermal,Energy,MWh/MtCO2,30,1432500,1678215,1500000,2062500,516794.3,638888.9,2722222,FALSE
|
| 56 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 57 |
+
Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,14,7.50E-10,0.003187,0.001008,0.004925,0.005238,-0.002,0.015,TRUE
|
| 58 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Electrical energy,Electrical,Energy,MWh/MtCO2,12,323625,1153583,606500,1755000,1286024,27000,4000000,FALSE
|
| 59 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Non-arable land,Land,Physical,Mha/MtCO2,4,0,0.000505,0.000355,0.00086,0.000632,0,0.00131,TRUE
|
| 60 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Thermal energy other grade,Thermal,Energy,MWh/MtCO2,6,1388889,1257630,1458944,1529000,542588.8,160000,1550000,TRUE
|
| 61 |
+
Mineral OAE / Ocean liming,Ocean alkalinity enhancement,Other mineral,Minerals,Physical,Mt/MtCO2,8,1.62,2.489,1.743,2.5895,1.554598,1.4,5,FALSE
|
| 62 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Electrical,Energy,MWh/MtCO2,8,83333.33,1104167,83333.33,1375000,1793811,0,4000000,FALSE
|
| 63 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,12,0.000587,0.001065,0.000639,0.000873,0.001064,0.000223,0.0033,FALSE
|
| 64 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Thermal energy other grade,Thermal,Energy,MWh/MtCO2,4,1479167,1715278,1819444,2055556,534015.3,1000000,2222222,FALSE
|
| 65 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Other mineral,Minerals,Physical,Mt/MtCO2,4,0.15,0.225,0.2,0.275,0.206155,0,0.5,FALSE
|
| 66 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 67 |
+
Mineral looping DACCS,Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,6,0.000125,0.006762,0.0002,0.010068,0.011116,0,0.026714,TRUE
|
| 68 |
+
Mineralization ex-situ,Mineralization,Electrical energy,Electrical,Energy,MWh/MtCO2,8,500000,1062500,750000,1250000,979704.8,0,3000000,FALSE
|
| 69 |
+
Mineralization ex-situ,Mineralization,Non-arable land,Land,Physical,Mha/MtCO2,4,0.000298,0.000522,0.000436,0.000659,0.000342,0.000223,0.000991,FALSE
|
| 70 |
+
Mineralization ex-situ,Mineralization,Thermal energy other grade,Thermal,Energy,MWh/MtCO2,4,400000,575000,450000,625000,287228.1,400000,1000000,FALSE
|
| 71 |
+
Mineralization ex-situ,Mineralization,Other mineral,Minerals,Physical,Mt/MtCO2,22,0.4375,3.935,3,7,3.647722,0.04,10,FALSE
|
| 72 |
+
Mineralization ex-situ,Mineralization,Other water,Water,Physical,bcm/MtCO2,14,0.0002,0.050004,0.00545,0.0145,0.106908,0,0.3,FALSE
|
| 73 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Electrical,Energy,MWh/MtCO2,4,316000,324666.7,324666.7,333333.3,10007.4,316000,333333.3,FALSE
|
| 74 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,8,0.000169,0.000331,0.000272,0.00038,0.000319,8.00E-06,0.000991,FALSE
|
| 75 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 76 |
+
Moisture swing DACCS,Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,4,-0.00015,0.000427,0.000427,0.001,0.000662,-0.00015,0.001,FALSE
|
| 77 |
+
Pasture management,"Soil carbon sequestration (crop, grasslands)",Arable land,Land,Physical,Mha/MtCO2,24,0.102253,0.442368,0.252422,0.522974,0.522145,0.071894,2.289744,FALSE
|
| 78 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Electrical energy,Electrical,Energy,MWh/MtCO2,28,980000,2205112,1768000,3302500,1381909,660000,4670000,FALSE
|
| 79 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Salt water,Water,Physical,bcm/MtCO2,4,0.435,4.78,4.78,9.125,5.017174,0.435,9.125,FALSE
|
| 80 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Geologic storage,Storage,Physical,Mt/MtCO2,1,1,1,1,1,0,1,1,FALSE
|
| 81 |
+
pH swing,Direct ocean carbon capture and storage (DOCCS),Other water,Water,Physical,bcm/MtCO2,4,0.217391,6.608696,6.608696,13,7.380043,0.217391,13,FALSE
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data/method_groups.json
CHANGED
|
@@ -1,23 +1,16 @@
|
|
| 1 |
{
|
| 2 |
"Forestry": [
|
| 3 |
-
"Afforestation",
|
| 4 |
"Agroforestry",
|
| 5 |
-
"Forest management"
|
| 6 |
-
"Reforestation"
|
| 7 |
],
|
| 8 |
"Bio-char": [
|
| 9 |
"Bio-char - Forest biomass",
|
| 10 |
-
"Bio-char - Non-forest biomass",
|
| 11 |
"Bio-char - Other biomass"
|
| 12 |
],
|
| 13 |
"Bio-CCS": [
|
| 14 |
"BioCCS with anaerobic digestion/fermentation",
|
| 15 |
-
"BioCCS with
|
| 16 |
-
"BioCCS with combustion",
|
| 17 |
-
"BioCCS with combustion & growing biomass"
|
| 18 |
-
],
|
| 19 |
-
"Blue carbon": [
|
| 20 |
-
"Coastal revegetation (mangrove, seaweeds, etc.)"
|
| 21 |
],
|
| 22 |
"Soil carbon sequestration (crop, grasslands)": [
|
| 23 |
"Cropland management",
|
|
|
|
| 1 |
{
|
| 2 |
"Forestry": [
|
| 3 |
+
"Afforestation/reforestation",
|
| 4 |
"Agroforestry",
|
| 5 |
+
"Forest management"
|
|
|
|
| 6 |
],
|
| 7 |
"Bio-char": [
|
| 8 |
"Bio-char - Forest biomass",
|
|
|
|
| 9 |
"Bio-char - Other biomass"
|
| 10 |
],
|
| 11 |
"Bio-CCS": [
|
| 12 |
"BioCCS with anaerobic digestion/fermentation",
|
| 13 |
+
"BioCCS with combustion"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
],
|
| 15 |
"Soil carbon sequestration (crop, grasslands)": [
|
| 16 |
"Cropland management",
|
data/resource_template.xlsx
CHANGED
|
Binary files a/data/resource_template.xlsx and b/data/resource_template.xlsx differ
|
|
|
data/secondary_resources.json
CHANGED
|
@@ -1,99 +1,35 @@
|
|
| 1 |
[
|
| 2 |
[
|
| 3 |
-
"Afforestation",
|
| 4 |
"Other water"
|
| 5 |
],
|
| 6 |
[
|
| 7 |
"Bio-char - Forest biomass",
|
| 8 |
"Electrical energy"
|
| 9 |
],
|
| 10 |
-
[
|
| 11 |
-
"Bio-char - Forest biomass",
|
| 12 |
-
"Other land"
|
| 13 |
-
],
|
| 14 |
-
[
|
| 15 |
-
"Bio-char - Forest biomass",
|
| 16 |
-
"Other water"
|
| 17 |
-
],
|
| 18 |
-
[
|
| 19 |
-
"Bio-char - Non-forest biomass",
|
| 20 |
-
"Electrical energy"
|
| 21 |
-
],
|
| 22 |
-
[
|
| 23 |
-
"Bio-char - Non-forest biomass",
|
| 24 |
-
"Other land"
|
| 25 |
-
],
|
| 26 |
-
[
|
| 27 |
-
"Bio-char - Non-forest biomass",
|
| 28 |
-
"Other water"
|
| 29 |
-
],
|
| 30 |
[
|
| 31 |
"Bio-char - Other biomass",
|
| 32 |
"Electrical energy"
|
| 33 |
],
|
| 34 |
-
[
|
| 35 |
-
"Bio-char - Other biomass",
|
| 36 |
-
"Other land"
|
| 37 |
-
],
|
| 38 |
-
[
|
| 39 |
-
"Bio-char - Other biomass",
|
| 40 |
-
"Other water"
|
| 41 |
-
],
|
| 42 |
-
[
|
| 43 |
-
"BioCCS with anaerobic digestion/fermentation",
|
| 44 |
-
"Other water"
|
| 45 |
-
],
|
| 46 |
-
[
|
| 47 |
-
"BioCCS with anaerobic digestion/fermentation & growing biomass",
|
| 48 |
-
"Other water"
|
| 49 |
-
],
|
| 50 |
[
|
| 51 |
"BioCCS with combustion",
|
| 52 |
"Electrical energy"
|
| 53 |
],
|
| 54 |
[
|
| 55 |
"BioCCS with combustion",
|
| 56 |
-
"Other chemical"
|
| 57 |
-
],
|
| 58 |
-
[
|
| 59 |
-
"BioCCS with combustion",
|
| 60 |
-
"Other water"
|
| 61 |
-
],
|
| 62 |
-
[
|
| 63 |
-
"BioCCS with combustion",
|
| 64 |
-
"Thermal energy other grade"
|
| 65 |
-
],
|
| 66 |
-
[
|
| 67 |
-
"BioCCS with combustion & growing biomass",
|
| 68 |
-
"Electrical energy"
|
| 69 |
-
],
|
| 70 |
-
[
|
| 71 |
-
"BioCCS with combustion & growing biomass",
|
| 72 |
-
"Other chemical"
|
| 73 |
-
],
|
| 74 |
-
[
|
| 75 |
-
"BioCCS with combustion & growing biomass",
|
| 76 |
-
"Other water"
|
| 77 |
-
],
|
| 78 |
-
[
|
| 79 |
-
"BioCCS with combustion & growing biomass",
|
| 80 |
"Thermal energy other grade"
|
| 81 |
],
|
| 82 |
[
|
| 83 |
"Durable biobased products",
|
| 84 |
"Electrical energy"
|
| 85 |
],
|
| 86 |
-
[
|
| 87 |
-
"Durable biobased products",
|
| 88 |
-
"Non-arable land"
|
| 89 |
-
],
|
| 90 |
[
|
| 91 |
"Durable biobased products",
|
| 92 |
"Other water"
|
| 93 |
],
|
| 94 |
[
|
| 95 |
"Electrochemical DACCS",
|
| 96 |
-
"
|
| 97 |
],
|
| 98 |
[
|
| 99 |
"Electrochemical OAE",
|
|
@@ -103,10 +39,6 @@
|
|
| 103 |
"Enhanced weathering - Basalt mineral",
|
| 104 |
"Arable land"
|
| 105 |
],
|
| 106 |
-
[
|
| 107 |
-
"Enhanced weathering - Basalt mineral",
|
| 108 |
-
"Electrical energy"
|
| 109 |
-
],
|
| 110 |
[
|
| 111 |
"Enhanced weathering - Basalt mineral",
|
| 112 |
"Non-arable land"
|
|
@@ -119,10 +51,6 @@
|
|
| 119 |
"Enhanced weathering - Olivine mineral",
|
| 120 |
"Arable land"
|
| 121 |
],
|
| 122 |
-
[
|
| 123 |
-
"Enhanced weathering - Olivine mineral",
|
| 124 |
-
"Electrical energy"
|
| 125 |
-
],
|
| 126 |
[
|
| 127 |
"Enhanced weathering - Olivine mineral",
|
| 128 |
"Non-arable land"
|
|
@@ -135,10 +63,6 @@
|
|
| 135 |
"Enhanced weathering - Other mineral",
|
| 136 |
"Arable land"
|
| 137 |
],
|
| 138 |
-
[
|
| 139 |
-
"Enhanced weathering - Other mineral",
|
| 140 |
-
"Electrical energy"
|
| 141 |
-
],
|
| 142 |
[
|
| 143 |
"Enhanced weathering - Other mineral",
|
| 144 |
"Non-arable land"
|
|
@@ -151,10 +75,6 @@
|
|
| 151 |
"Enhanced weathering - Wollastonite mineral",
|
| 152 |
"Arable land"
|
| 153 |
],
|
| 154 |
-
[
|
| 155 |
-
"Enhanced weathering - Wollastonite mineral",
|
| 156 |
-
"Electrical energy"
|
| 157 |
-
],
|
| 158 |
[
|
| 159 |
"Enhanced weathering - Wollastonite mineral",
|
| 160 |
"Non-arable land"
|
|
@@ -171,18 +91,10 @@
|
|
| 171 |
"Low-temp DACCS (solid-sorbent/liquid-solvent)",
|
| 172 |
"Other water"
|
| 173 |
],
|
| 174 |
-
[
|
| 175 |
-
"Mineral OAE / Ocean liming",
|
| 176 |
-
"Electrical energy"
|
| 177 |
-
],
|
| 178 |
[
|
| 179 |
"Mineral OAE / Ocean liming",
|
| 180 |
"Non-arable land"
|
| 181 |
],
|
| 182 |
-
[
|
| 183 |
-
"Mineral OAE / Ocean liming",
|
| 184 |
-
"Other water"
|
| 185 |
-
],
|
| 186 |
[
|
| 187 |
"Mineral OAE / Ocean liming",
|
| 188 |
"Thermal energy other grade"
|
|
@@ -190,9 +102,5 @@
|
|
| 190 |
[
|
| 191 |
"Mineral looping DACCS",
|
| 192 |
"Other water"
|
| 193 |
-
],
|
| 194 |
-
[
|
| 195 |
-
"Reforestation",
|
| 196 |
-
"Other water"
|
| 197 |
]
|
| 198 |
]
|
|
|
|
| 1 |
[
|
| 2 |
[
|
| 3 |
+
"Afforestation/reforestation",
|
| 4 |
"Other water"
|
| 5 |
],
|
| 6 |
[
|
| 7 |
"Bio-char - Forest biomass",
|
| 8 |
"Electrical energy"
|
| 9 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
[
|
| 11 |
"Bio-char - Other biomass",
|
| 12 |
"Electrical energy"
|
| 13 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
[
|
| 15 |
"BioCCS with combustion",
|
| 16 |
"Electrical energy"
|
| 17 |
],
|
| 18 |
[
|
| 19 |
"BioCCS with combustion",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
"Thermal energy other grade"
|
| 21 |
],
|
| 22 |
[
|
| 23 |
"Durable biobased products",
|
| 24 |
"Electrical energy"
|
| 25 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
[
|
| 27 |
"Durable biobased products",
|
| 28 |
"Other water"
|
| 29 |
],
|
| 30 |
[
|
| 31 |
"Electrochemical DACCS",
|
| 32 |
+
"Other land"
|
| 33 |
],
|
| 34 |
[
|
| 35 |
"Electrochemical OAE",
|
|
|
|
| 39 |
"Enhanced weathering - Basalt mineral",
|
| 40 |
"Arable land"
|
| 41 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
[
|
| 43 |
"Enhanced weathering - Basalt mineral",
|
| 44 |
"Non-arable land"
|
|
|
|
| 51 |
"Enhanced weathering - Olivine mineral",
|
| 52 |
"Arable land"
|
| 53 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
[
|
| 55 |
"Enhanced weathering - Olivine mineral",
|
| 56 |
"Non-arable land"
|
|
|
|
| 63 |
"Enhanced weathering - Other mineral",
|
| 64 |
"Arable land"
|
| 65 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
[
|
| 67 |
"Enhanced weathering - Other mineral",
|
| 68 |
"Non-arable land"
|
|
|
|
| 75 |
"Enhanced weathering - Wollastonite mineral",
|
| 76 |
"Arable land"
|
| 77 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
[
|
| 79 |
"Enhanced weathering - Wollastonite mineral",
|
| 80 |
"Non-arable land"
|
|
|
|
| 91 |
"Low-temp DACCS (solid-sorbent/liquid-solvent)",
|
| 92 |
"Other water"
|
| 93 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
[
|
| 95 |
"Mineral OAE / Ocean liming",
|
| 96 |
"Non-arable land"
|
| 97 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
[
|
| 99 |
"Mineral OAE / Ocean liming",
|
| 100 |
"Thermal energy other grade"
|
|
|
|
| 102 |
[
|
| 103 |
"Mineral looping DACCS",
|
| 104 |
"Other water"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
]
|
| 106 |
]
|
data/ui_text.xlsx
CHANGED
|
Binary files a/data/ui_text.xlsx and b/data/ui_text.xlsx differ
|
|
|
data/update.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
Run this script after any change to
|
| 3 |
generated data files and Excel templates.
|
| 4 |
|
| 5 |
Usage:
|
|
@@ -41,7 +41,7 @@ print("All files updated successfully.")
|
|
| 41 |
print("=" * 60)
|
| 42 |
|
| 43 |
# Update the hash file so the app knows the CSV is in sync
|
| 44 |
-
csv_path = ROOT / "data/
|
| 45 |
hash_path = ROOT / "data/.csv_hash"
|
| 46 |
hasher = hashlib.md5()
|
| 47 |
with open(csv_path, "rb") as f:
|
|
|
|
| 1 |
"""
|
| 2 |
+
Run this script after any change to inputs.csv to update all
|
| 3 |
generated data files and Excel templates.
|
| 4 |
|
| 5 |
Usage:
|
|
|
|
| 41 |
print("=" * 60)
|
| 42 |
|
| 43 |
# Update the hash file so the app knows the CSV is in sync
|
| 44 |
+
csv_path = ROOT / "data/inputs.csv"
|
| 45 |
hash_path = ROOT / "data/.csv_hash"
|
| 46 |
hasher = hashlib.md5()
|
| 47 |
with open(csv_path, "rb") as f:
|
optimization/optimization.py
CHANGED
|
@@ -65,7 +65,7 @@ def build_custom_batch_structures(method_costs, resource_caps, custom_resources)
|
|
| 65 |
return custom_batch_specs, group_to_custom_batches, custom_batch_names
|
| 66 |
|
| 67 |
|
| 68 |
-
def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_caps, method_costs):
|
| 69 |
"""Build the LP variable index for custom-batch draw variables z_{m,n}.
|
| 70 |
|
| 71 |
When the user adds a custom resource batch (e.g. "degraded land patch A"),
|
|
@@ -81,6 +81,9 @@ def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_c
|
|
| 81 |
custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
|
| 82 |
resource_caps (dict): {resource: cap} — used to skip zero-cap batches.
|
| 83 |
method_costs (dict): {method: {resource: coefficient}}.
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
Returns:
|
| 86 |
tuple[list, dict, int]:
|
|
@@ -88,6 +91,7 @@ def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_c
|
|
| 88 |
custom_pair_to_index – {(method, batch_name): lp_column_index}.
|
| 89 |
num_lp_vars – total number of LP variables.
|
| 90 |
"""
|
|
|
|
| 91 |
custom_usage_pairs = []
|
| 92 |
for batch_name, specs in custom_batch_specs.items():
|
| 93 |
primary_methods = specs["primary_methods"]
|
|
@@ -96,6 +100,8 @@ def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_c
|
|
| 96 |
for m in active_methods:
|
| 97 |
if m not in primary_methods:
|
| 98 |
continue
|
|
|
|
|
|
|
| 99 |
if method_costs.get(m, {}).get(batch_name, 0.0) > 0:
|
| 100 |
custom_usage_pairs.append((m, batch_name))
|
| 101 |
|
|
@@ -104,20 +110,105 @@ def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_c
|
|
| 104 |
return custom_usage_pairs, custom_pair_to_index, num_lp_vars
|
| 105 |
|
| 106 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
def build_constraint_matrix(
|
| 108 |
active_methods, method_to_index, num_lp_vars, num_methods,
|
| 109 |
resource_caps, method_costs, method_constraints,
|
| 110 |
custom_batch_specs, group_to_custom_batches,
|
| 111 |
resources_no_batches, resources_with_batches,
|
| 112 |
custom_usage_pairs, custom_pair_to_index,
|
|
|
|
| 113 |
):
|
| 114 |
"""Assemble the LP inequality constraint matrix (A_ub · x ≤ b_ub).
|
| 115 |
|
| 116 |
-
Builds
|
| 117 |
A – standard resources with no custom substitutes.
|
| 118 |
B – standard resources that have custom-batch substitutes.
|
| 119 |
C – custom batch capacity limits.
|
| 120 |
D1/D2 – coupling constraints between y_m and z_{m,n}.
|
|
|
|
| 121 |
E – per-method output caps (percent or absolute).
|
| 122 |
|
| 123 |
Args:
|
|
@@ -134,6 +225,11 @@ def build_constraint_matrix(
|
|
| 134 |
resources_with_batches (list[str]): standard resources that have substitutes.
|
| 135 |
custom_usage_pairs (list): [(method, batch_name)].
|
| 136 |
custom_pair_to_index (dict): {(method, batch_name): lp_column_index}.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 137 |
|
| 138 |
Returns:
|
| 139 |
tuple[np.ndarray, np.ndarray, list]:
|
|
@@ -141,10 +237,20 @@ def build_constraint_matrix(
|
|
| 141 |
b_ub – constraint right-hand-side vector.
|
| 142 |
constraint_labels – list of (type_str, identifier) per row.
|
| 143 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
A_rows, b_rows, constraint_labels = [], [], []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
# ── CONSTRAINT GROUPS ─────────────────────────────────────────────────────
|
| 147 |
-
# Each group adds one row per resource/method
|
| 148 |
# The solver will find the y_m values that maximise total CO₂ removal while
|
| 149 |
# keeping every A_ub · x ≤ b_ub inequality satisfied.
|
| 150 |
#
|
|
@@ -155,52 +261,63 @@ def build_constraint_matrix(
|
|
| 155 |
# D – "Book-keeping: custom-batch draw variables can't exceed the method's
|
| 156 |
# total output, and if a method has no standard resource it must draw
|
| 157 |
# entirely from custom batches."
|
|
|
|
|
|
|
|
|
|
| 158 |
# E – "Respect each method's user-defined cap (% or absolute MtCO₂/yr)."
|
| 159 |
# ─────────────────────────────────────────────────────────────────────────
|
| 160 |
|
| 161 |
# (A) Standard resources — no custom substitutes.
|
| 162 |
# Rule: total resource consumed across all methods ≤ available amount.
|
|
|
|
|
|
|
| 163 |
for r in resources_no_batches:
|
| 164 |
-
row =
|
| 165 |
for j, m in enumerate(active_methods):
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
b_rows.append(resource_caps[r])
|
| 170 |
-
constraint_labels.append(("resource", r))
|
| 171 |
|
| 172 |
# (B) Standard resources that have custom-batch substitutes.
|
| 173 |
# Rule: only the portion of the resource NOT already covered by custom batches
|
| 174 |
# counts against the standard pool cap. Formula: Σ cost(m,r)·(y_m − Σ z_{m,n}) ≤ cap(r)
|
| 175 |
for r in resources_with_batches:
|
| 176 |
-
row =
|
| 177 |
for j, m in enumerate(active_methods):
|
|
|
|
|
|
|
| 178 |
c_s_m = method_costs.get(m, {}).get(r, 0.0) # coefficient for standard resource r
|
| 179 |
-
row[j] = c_s_m
|
| 180 |
for batch_name in group_to_custom_batches.get(r, []):
|
| 181 |
if (m, batch_name) in custom_pair_to_index:
|
| 182 |
row[custom_pair_to_index[(m, batch_name)]] -= c_s_m # subtract batch draw
|
| 183 |
-
if np.any(row != 0):
|
| 184 |
-
A_rows.append(row)
|
| 185 |
-
b_rows.append(resource_caps[r])
|
| 186 |
-
constraint_labels.append(("resource", r))
|
| 187 |
|
| 188 |
# (C) Custom batch capacity.
|
| 189 |
# Rule: total resource drawn from this specific batch ≤ batch quantity Q_n.
|
| 190 |
for batch_name, specs in custom_batch_specs.items():
|
| 191 |
if resource_caps.get(batch_name, 0.0) <= 0:
|
| 192 |
continue
|
| 193 |
-
row =
|
| 194 |
for j, m in enumerate(active_methods):
|
| 195 |
if (m, batch_name) not in custom_pair_to_index:
|
| 196 |
continue
|
| 197 |
c_n_m = method_costs.get(m, {}).get(batch_name, 0.0) # coefficient for batch n
|
| 198 |
if c_n_m > 0:
|
| 199 |
-
row[custom_pair_to_index[(m, batch_name)]] = c_n_m
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
if np.any(row != 0):
|
| 201 |
A_rows.append(row)
|
| 202 |
-
b_rows.append(resource_caps.get(
|
| 203 |
-
constraint_labels.append(("
|
| 204 |
|
| 205 |
# (D) Coupling constraints — link z_{m,n} draw variables to y_m output.
|
| 206 |
# D1: Σ z_{m,n} ≤ y_m — can't draw more from batches than the method produces.
|
|
@@ -237,6 +354,34 @@ def build_constraint_matrix(
|
|
| 237 |
b_rows.append(0.0)
|
| 238 |
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 239 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
# (E) Method output caps — respect the user's per-method limits from Tab 2.
|
| 241 |
# Absolute cap: y_m ≤ cap_value (in MtCO₂/yr).
|
| 242 |
# Percent cap: y_m ≤ cap_ratio · Σ y_all (fraction of total portfolio output).
|
|
@@ -274,6 +419,7 @@ def extract_resource_usage(
|
|
| 274 |
resource_caps, custom_batch_specs, group_to_custom_batches,
|
| 275 |
custom_pair_to_index, standard_resources,
|
| 276 |
resources_no_batches, resources_with_batches, custom_batch_names,
|
|
|
|
| 277 |
):
|
| 278 |
"""Compute per-method resource consumption from the LP solution.
|
| 279 |
|
|
@@ -291,6 +437,10 @@ def extract_resource_usage(
|
|
| 291 |
resources_no_batches (list[str]): standard resources without substitutes.
|
| 292 |
resources_with_batches (list[str]): standard resources with substitutes.
|
| 293 |
custom_batch_names (set[str]): set of all custom batch names.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
Returns:
|
| 296 |
tuple[dict, dict, dict]:
|
|
@@ -298,6 +448,9 @@ def extract_resource_usage(
|
|
| 298 |
resource_used_total – {resource: total_qty_used}.
|
| 299 |
resource_remaining – {resource: qty_remaining}.
|
| 300 |
"""
|
|
|
|
|
|
|
|
|
|
| 301 |
all_resource_names = standard_resources + list(custom_batch_names)
|
| 302 |
resource_usage = {r: {} for r in all_resource_names}
|
| 303 |
|
|
@@ -307,12 +460,16 @@ def extract_resource_usage(
|
|
| 307 |
_USAGE_EPS = 1e-14
|
| 308 |
for r in resources_no_batches:
|
| 309 |
for j, m in enumerate(active_methods):
|
|
|
|
|
|
|
| 310 |
used = method_costs.get(m, {}).get(r, 0.0) * method_outputs[j]
|
| 311 |
if used > _USAGE_EPS:
|
| 312 |
resource_usage[r][m] = float(used)
|
| 313 |
|
| 314 |
for r in resources_with_batches:
|
| 315 |
for j, m in enumerate(active_methods):
|
|
|
|
|
|
|
| 316 |
c_s_m = method_costs.get(m, {}).get(r, 0.0)
|
| 317 |
z_sum = sum(
|
| 318 |
lp_solution[custom_pair_to_index[(m, batch_name)]]
|
|
@@ -328,6 +485,8 @@ def extract_resource_usage(
|
|
| 328 |
for j, m in enumerate(active_methods):
|
| 329 |
if m not in primary_methods:
|
| 330 |
continue
|
|
|
|
|
|
|
| 331 |
c_n_m = method_costs.get(m, {}).get(batch_name, 0.0)
|
| 332 |
if c_n_m <= 0:
|
| 333 |
continue
|
|
@@ -339,6 +498,13 @@ def extract_resource_usage(
|
|
| 339 |
if used > _USAGE_EPS:
|
| 340 |
resource_usage[batch_name][m] = float(used)
|
| 341 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
_EPS = 1e-14
|
| 343 |
resource_used_total = {
|
| 344 |
r: round(sum(v.values()), 10) for r, v in resource_usage.items()
|
|
@@ -360,19 +526,25 @@ def extract_resource_usage(
|
|
| 360 |
def resolve_pooling_tiebreak(
|
| 361 |
num_methods, num_lp_vars, method_outputs, custom_usage_pairs,
|
| 362 |
custom_pair_to_index, custom_batch_specs, method_costs, A_ub, b_ub, bounds,
|
|
|
|
| 363 |
):
|
| 364 |
"""Re-solve the LP with each method's output pinned, preferring the more
|
| 365 |
-
efficient resource within each standard/custom pooling pair.
|
| 366 |
|
| 367 |
The main LP (run_optimization) maximises total CO2 removed only — nothing
|
| 368 |
in its objective distinguishes a standard resource from a custom batch
|
| 369 |
-
that substitutes for it
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
Args:
|
| 378 |
num_methods (int): number of y_m variables.
|
|
@@ -385,13 +557,16 @@ def resolve_pooling_tiebreak(
|
|
| 385 |
A_ub, b_ub: pass-1 constraint matrix/vector (reused unchanged — pass-1's
|
| 386 |
own solution already satisfies them, so this second LP is always feasible).
|
| 387 |
bounds: pass-1 variable bounds (reused unchanged).
|
|
|
|
|
|
|
| 388 |
|
| 389 |
Returns:
|
| 390 |
np.ndarray | None: the tie-broken solution vector, or None if there are
|
| 391 |
no pooling pairs to break ties on, or if the second solve fails
|
| 392 |
(in which case the caller should fall back to the pass-1 solution).
|
| 393 |
"""
|
| 394 |
-
|
|
|
|
| 395 |
return None
|
| 396 |
|
| 397 |
A_eq = np.zeros((num_methods, num_lp_vars))
|
|
@@ -406,6 +581,9 @@ def resolve_pooling_tiebreak(
|
|
| 406 |
c_n = method_costs.get(m, {}).get(batch_name, 0.0)
|
| 407 |
objective2[custom_pair_to_index[(m, batch_name)]] = c_n - c_s
|
| 408 |
|
|
|
|
|
|
|
|
|
|
| 409 |
try:
|
| 410 |
result = linprog(
|
| 411 |
objective2, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq,
|
|
@@ -452,7 +630,7 @@ def run_perturbation(constraint_labels, A_ub, b_ub, objective_coefficients, boun
|
|
| 452 |
return resource_actual_gain
|
| 453 |
|
| 454 |
|
| 455 |
-
def compute_standalone_potential(method, resource_caps, method_costs, custom_resources, enabled_methods):
|
| 456 |
"""Compute a method's own maximum achievable output, in isolation.
|
| 457 |
|
| 458 |
"Isolation" means solving as if this were the only active method — no
|
|
@@ -471,6 +649,10 @@ def compute_standalone_potential(method, resource_caps, method_costs, custom_res
|
|
| 471 |
full list — batches tied to other methods simply produce no
|
| 472 |
usage pairs once every other method is excluded).
|
| 473 |
enabled_methods (dict | None): {resource: {method: bool}}.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 474 |
|
| 475 |
Returns:
|
| 476 |
float: the method's standalone potential in MtCO₂ (0.0 if it can't
|
|
@@ -483,6 +665,7 @@ def compute_standalone_potential(method, resource_caps, method_costs, custom_res
|
|
| 483 |
method_costs={method: dict(method_costs.get(method, {}))},
|
| 484 |
custom_resources=custom_resources,
|
| 485 |
enabled_methods=enabled_methods,
|
|
|
|
| 486 |
)
|
| 487 |
return result["total_removed"] if success else 0.0
|
| 488 |
|
|
@@ -493,6 +676,7 @@ def run_optimization(
|
|
| 493 |
method_costs,
|
| 494 |
custom_resources=None,
|
| 495 |
enabled_methods=None,
|
|
|
|
| 496 |
):
|
| 497 |
"""Maximize total CO₂ removal subject to resource caps and method constraints.
|
| 498 |
|
|
@@ -500,12 +684,17 @@ def run_optimization(
|
|
| 500 |
Also runs a perturbation analysis: for each binding resource constraint,
|
| 501 |
re-solves with cap + 1 to compute the marginal CDR gain per unit.
|
| 502 |
|
| 503 |
-
LP variables (
|
| 504 |
y_m = MtCO₂ removed by CDR method m — the main "answer" (indices 0..num_methods-1).
|
| 505 |
z_{m,n} = MtCO₂ that method m draws specifically from custom batch n — a helper
|
| 506 |
variable introduced when the user adds a custom resource batch; it lets the
|
| 507 |
-
solver track which batch supplies which method without double-counting
|
| 508 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
Args:
|
| 511 |
resource_caps (dict): {resource: available amount (float)}.
|
|
@@ -515,6 +704,13 @@ def run_optimization(
|
|
| 515 |
name, group, amount, methods.
|
| 516 |
enabled_methods (dict | None): {resource: {method: bool}} — False entries
|
| 517 |
zero-out the corresponding coefficient before solving.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 518 |
|
| 519 |
Returns:
|
| 520 |
tuple[bool, dict]:
|
|
@@ -556,6 +752,7 @@ def run_optimization(
|
|
| 556 |
cap_value = max(0.0, min(100.0, c.get("cap_value", 100)))
|
| 557 |
potential = compute_standalone_potential(
|
| 558 |
m, resource_caps, method_costs, custom_resources, enabled_methods,
|
|
|
|
| 559 |
)
|
| 560 |
resolved_constraints[m] = {**c, "cap_type": "absolute", "cap_value": potential * cap_value / 100.0}
|
| 561 |
else:
|
|
@@ -605,8 +802,15 @@ def run_optimization(
|
|
| 605 |
}
|
| 606 |
return True, empty_result
|
| 607 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
custom_usage_pairs, custom_pair_to_index, num_lp_vars = build_lp_indices(
|
| 609 |
-
active_methods, num_methods, custom_batch_specs, resource_caps, method_costs
|
|
|
|
|
|
|
|
|
|
| 610 |
)
|
| 611 |
|
| 612 |
standard_resources = [r for r in resource_caps if r not in custom_batch_names]
|
|
@@ -620,6 +824,7 @@ def run_optimization(
|
|
| 620 |
custom_batch_specs, group_to_custom_batches,
|
| 621 |
resources_no_batches, resources_with_batches,
|
| 622 |
custom_usage_pairs, custom_pair_to_index,
|
|
|
|
| 623 |
)
|
| 624 |
except ValueError as e:
|
| 625 |
return False, {"message": str(e)}
|
|
@@ -645,6 +850,7 @@ def run_optimization(
|
|
| 645 |
tiebreak_solution = resolve_pooling_tiebreak(
|
| 646 |
num_methods, num_lp_vars, method_outputs, custom_usage_pairs,
|
| 647 |
custom_pair_to_index, custom_batch_specs, method_costs, A_ub, b_ub, bounds,
|
|
|
|
| 648 |
)
|
| 649 |
lp_solution_for_usage = tiebreak_solution if tiebreak_solution is not None else lp_solution
|
| 650 |
|
|
@@ -653,6 +859,7 @@ def run_optimization(
|
|
| 653 |
resource_caps, custom_batch_specs, group_to_custom_batches,
|
| 654 |
custom_pair_to_index, standard_resources,
|
| 655 |
resources_no_batches, resources_with_batches, custom_batch_names,
|
|
|
|
| 656 |
)
|
| 657 |
|
| 658 |
resource_actual_gain = run_perturbation(
|
|
|
|
| 65 |
return custom_batch_specs, group_to_custom_batches, custom_batch_names
|
| 66 |
|
| 67 |
|
| 68 |
+
def build_lp_indices(active_methods, num_methods, custom_batch_specs, resource_caps, method_costs, pool_of_entity=None):
|
| 69 |
"""Build the LP variable index for custom-batch draw variables z_{m,n}.
|
| 70 |
|
| 71 |
When the user adds a custom resource batch (e.g. "degraded land patch A"),
|
|
|
|
| 81 |
custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
|
| 82 |
resource_caps (dict): {resource: cap} — used to skip zero-cap batches.
|
| 83 |
method_costs (dict): {method: {resource: coefficient}}.
|
| 84 |
+
pool_of_entity (dict | None): {(method, entity_name): pool_id} — batches
|
| 85 |
+
already covered by a family-resource pool (see build_family_pools)
|
| 86 |
+
are excluded here since they get a w_{m,e} variable instead.
|
| 87 |
|
| 88 |
Returns:
|
| 89 |
tuple[list, dict, int]:
|
|
|
|
| 91 |
custom_pair_to_index – {(method, batch_name): lp_column_index}.
|
| 92 |
num_lp_vars – total number of LP variables.
|
| 93 |
"""
|
| 94 |
+
pool_of_entity = pool_of_entity or {}
|
| 95 |
custom_usage_pairs = []
|
| 96 |
for batch_name, specs in custom_batch_specs.items():
|
| 97 |
primary_methods = specs["primary_methods"]
|
|
|
|
| 100 |
for m in active_methods:
|
| 101 |
if m not in primary_methods:
|
| 102 |
continue
|
| 103 |
+
if (m, batch_name) in pool_of_entity:
|
| 104 |
+
continue
|
| 105 |
if method_costs.get(m, {}).get(batch_name, 0.0) > 0:
|
| 106 |
custom_usage_pairs.append((m, batch_name))
|
| 107 |
|
|
|
|
| 110 |
return custom_usage_pairs, custom_pair_to_index, num_lp_vars
|
| 111 |
|
| 112 |
|
| 113 |
+
def build_family_pools(active_methods, method_costs, resource_to_group, group_to_custom_batches, custom_batch_specs):
|
| 114 |
+
"""Group a method's own resources into OR-substitutable pools.
|
| 115 |
+
|
| 116 |
+
Two (or more) resources are pooled for a given method when they belong to
|
| 117 |
+
the same higher-level resource family (e.g. "Arable land" and "Other land"
|
| 118 |
+
are both "Land") and both currently have a non-zero coefficient for that
|
| 119 |
+
method — i.e. both are active (primary, or a secondary resource the user
|
| 120 |
+
switched on). Pooling means the method can satisfy its need from either
|
| 121 |
+
one (or a mix) instead of requiring all of them at once (AND), the same
|
| 122 |
+
way a custom batch already complements its reference standard resource.
|
| 123 |
+
|
| 124 |
+
Any custom batch attached to a pooled standard resource joins the same
|
| 125 |
+
pool, so the batch and its reference resource stay complementary exactly
|
| 126 |
+
as they already do outside a family pool.
|
| 127 |
+
|
| 128 |
+
Args:
|
| 129 |
+
active_methods (list[str]): ordered active CDR method names.
|
| 130 |
+
method_costs (dict): {method: {resource: coefficient}}.
|
| 131 |
+
resource_to_group (dict | None): {resource: family group name}. When
|
| 132 |
+
None or empty, no method has any pool (fully backward compatible).
|
| 133 |
+
group_to_custom_batches (dict): {reference_resource: [batch_name, ...]}.
|
| 134 |
+
custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
|
| 135 |
+
|
| 136 |
+
Returns:
|
| 137 |
+
tuple[dict, dict]:
|
| 138 |
+
pool_of_entity – {(method, entity_name): pool_id} for every entity
|
| 139 |
+
(standard resource or custom batch) that belongs to a real
|
| 140 |
+
pool (2+ standard resources sharing a family for that method).
|
| 141 |
+
pools – {pool_id: [entity_name, ...]}.
|
| 142 |
+
"""
|
| 143 |
+
resource_to_group = resource_to_group or {}
|
| 144 |
+
pool_of_entity = {}
|
| 145 |
+
pools = {}
|
| 146 |
+
for m in active_methods:
|
| 147 |
+
by_group = {}
|
| 148 |
+
for r, c in method_costs.get(m, {}).items():
|
| 149 |
+
if c <= 0:
|
| 150 |
+
continue
|
| 151 |
+
g = resource_to_group.get(r)
|
| 152 |
+
if g is None:
|
| 153 |
+
continue
|
| 154 |
+
by_group.setdefault(g, []).append(r)
|
| 155 |
+
for g, members in by_group.items():
|
| 156 |
+
if len(members) < 2:
|
| 157 |
+
continue
|
| 158 |
+
pool_id = (m, g)
|
| 159 |
+
entities = list(members)
|
| 160 |
+
for r in members:
|
| 161 |
+
for batch_name in group_to_custom_batches.get(r, []):
|
| 162 |
+
if m in custom_batch_specs.get(batch_name, {}).get("primary_methods", set()):
|
| 163 |
+
entities.append(batch_name)
|
| 164 |
+
pools[pool_id] = entities
|
| 165 |
+
for e in entities:
|
| 166 |
+
pool_of_entity[(m, e)] = pool_id
|
| 167 |
+
return pool_of_entity, pools
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def build_pool_indices(offset, pool_of_entity, resource_caps):
|
| 171 |
+
"""Build the LP variable index for family-pool draw variables w_{m,e}.
|
| 172 |
+
|
| 173 |
+
Analogous to build_lp_indices's z_{m,n}, but for resources pooled because
|
| 174 |
+
they share a resource family (see build_family_pools), not because a
|
| 175 |
+
custom batch substitutes a single standard resource.
|
| 176 |
+
|
| 177 |
+
Args:
|
| 178 |
+
offset (int): first free LP column index (after y_m and any z_{m,n}).
|
| 179 |
+
pool_of_entity (dict): {(method, entity_name): pool_id}.
|
| 180 |
+
resource_caps (dict): {resource: cap} — used to skip zero-cap entities.
|
| 181 |
+
|
| 182 |
+
Returns:
|
| 183 |
+
tuple[list, dict, int]:
|
| 184 |
+
pool_pairs – [(method, entity_name), ...].
|
| 185 |
+
pool_pair_to_index – {(method, entity_name): lp_column_index}.
|
| 186 |
+
num_lp_vars – total LP variable count including these columns.
|
| 187 |
+
"""
|
| 188 |
+
pool_pairs = sorted(
|
| 189 |
+
pair for pair in pool_of_entity if resource_caps.get(pair[1], 0.0) > 0
|
| 190 |
+
)
|
| 191 |
+
pool_pair_to_index = {pair: offset + i for i, pair in enumerate(pool_pairs)}
|
| 192 |
+
num_lp_vars = offset + len(pool_pairs)
|
| 193 |
+
return pool_pairs, pool_pair_to_index, num_lp_vars
|
| 194 |
+
|
| 195 |
+
|
| 196 |
def build_constraint_matrix(
|
| 197 |
active_methods, method_to_index, num_lp_vars, num_methods,
|
| 198 |
resource_caps, method_costs, method_constraints,
|
| 199 |
custom_batch_specs, group_to_custom_batches,
|
| 200 |
resources_no_batches, resources_with_batches,
|
| 201 |
custom_usage_pairs, custom_pair_to_index,
|
| 202 |
+
pool_of_entity=None, pools=None, pool_pair_to_index=None,
|
| 203 |
):
|
| 204 |
"""Assemble the LP inequality constraint matrix (A_ub · x ≤ b_ub).
|
| 205 |
|
| 206 |
+
Builds six groups of constraints:
|
| 207 |
A – standard resources with no custom substitutes.
|
| 208 |
B – standard resources that have custom-batch substitutes.
|
| 209 |
C – custom batch capacity limits.
|
| 210 |
D1/D2 – coupling constraints between y_m and z_{m,n}.
|
| 211 |
+
F – family-pool entity capacity limits, and their own D1/D2 coupling to y_m.
|
| 212 |
E – per-method output caps (percent or absolute).
|
| 213 |
|
| 214 |
Args:
|
|
|
|
| 225 |
resources_with_batches (list[str]): standard resources that have substitutes.
|
| 226 |
custom_usage_pairs (list): [(method, batch_name)].
|
| 227 |
custom_pair_to_index (dict): {(method, batch_name): lp_column_index}.
|
| 228 |
+
pool_of_entity (dict | None): {(method, entity_name): pool_id} — see
|
| 229 |
+
build_family_pools. Entities here are excluded from A/B/C's direct
|
| 230 |
+
y_m contribution; they get their own w_{m,e} variable instead.
|
| 231 |
+
pools (dict | None): {pool_id: [entity_name, ...]}.
|
| 232 |
+
pool_pair_to_index (dict | None): {(method, entity_name): lp_column_index}.
|
| 233 |
|
| 234 |
Returns:
|
| 235 |
tuple[np.ndarray, np.ndarray, list]:
|
|
|
|
| 237 |
b_ub – constraint right-hand-side vector.
|
| 238 |
constraint_labels – list of (type_str, identifier) per row.
|
| 239 |
"""
|
| 240 |
+
pool_of_entity = pool_of_entity or {}
|
| 241 |
+
pools = pools or {}
|
| 242 |
+
pool_pair_to_index = pool_pair_to_index or {}
|
| 243 |
+
|
| 244 |
A_rows, b_rows, constraint_labels = [], [], []
|
| 245 |
+
resource_rows: dict[str, np.ndarray] = {}
|
| 246 |
+
|
| 247 |
+
def get_row(name):
|
| 248 |
+
if name not in resource_rows:
|
| 249 |
+
resource_rows[name] = np.zeros(num_lp_vars)
|
| 250 |
+
return resource_rows[name]
|
| 251 |
|
| 252 |
# ── CONSTRAINT GROUPS ─────────────────────────────────────────────────────
|
| 253 |
+
# Each group adds to one row per resource/method in the matrix A_ub.
|
| 254 |
# The solver will find the y_m values that maximise total CO₂ removal while
|
| 255 |
# keeping every A_ub · x ≤ b_ub inequality satisfied.
|
| 256 |
#
|
|
|
|
| 261 |
# D – "Book-keeping: custom-batch draw variables can't exceed the method's
|
| 262 |
# total output, and if a method has no standard resource it must draw
|
| 263 |
# entirely from custom batches."
|
| 264 |
+
# F – "A method with two resources from the same family (e.g. two kinds
|
| 265 |
+
# of land) can draw from either, not both at once — each still can't
|
| 266 |
+
# exceed its own declared quantity."
|
| 267 |
# E – "Respect each method's user-defined cap (% or absolute MtCO₂/yr)."
|
| 268 |
# ─────────────────────────────────────────────────────────────────────────
|
| 269 |
|
| 270 |
# (A) Standard resources — no custom substitutes.
|
| 271 |
# Rule: total resource consumed across all methods ≤ available amount.
|
| 272 |
+
# A method whose use of r is covered by a family pool (F) is skipped here —
|
| 273 |
+
# its contribution comes from its w_{m,r} variable instead.
|
| 274 |
for r in resources_no_batches:
|
| 275 |
+
row = get_row(r)
|
| 276 |
for j, m in enumerate(active_methods):
|
| 277 |
+
if (m, r) in pool_of_entity:
|
| 278 |
+
continue
|
| 279 |
+
row[j] += method_costs.get(m, {}).get(r, 0.0)
|
|
|
|
|
|
|
| 280 |
|
| 281 |
# (B) Standard resources that have custom-batch substitutes.
|
| 282 |
# Rule: only the portion of the resource NOT already covered by custom batches
|
| 283 |
# counts against the standard pool cap. Formula: Σ cost(m,r)·(y_m − Σ z_{m,n}) ≤ cap(r)
|
| 284 |
for r in resources_with_batches:
|
| 285 |
+
row = get_row(r)
|
| 286 |
for j, m in enumerate(active_methods):
|
| 287 |
+
if (m, r) in pool_of_entity:
|
| 288 |
+
continue
|
| 289 |
c_s_m = method_costs.get(m, {}).get(r, 0.0) # coefficient for standard resource r
|
| 290 |
+
row[j] += c_s_m
|
| 291 |
for batch_name in group_to_custom_batches.get(r, []):
|
| 292 |
if (m, batch_name) in custom_pair_to_index:
|
| 293 |
row[custom_pair_to_index[(m, batch_name)]] -= c_s_m # subtract batch draw
|
|
|
|
|
|
|
|
|
|
|
|
|
| 294 |
|
| 295 |
# (C) Custom batch capacity.
|
| 296 |
# Rule: total resource drawn from this specific batch ≤ batch quantity Q_n.
|
| 297 |
for batch_name, specs in custom_batch_specs.items():
|
| 298 |
if resource_caps.get(batch_name, 0.0) <= 0:
|
| 299 |
continue
|
| 300 |
+
row = get_row(batch_name)
|
| 301 |
for j, m in enumerate(active_methods):
|
| 302 |
if (m, batch_name) not in custom_pair_to_index:
|
| 303 |
continue
|
| 304 |
c_n_m = method_costs.get(m, {}).get(batch_name, 0.0) # coefficient for batch n
|
| 305 |
if c_n_m > 0:
|
| 306 |
+
row[custom_pair_to_index[(m, batch_name)]] += c_n_m
|
| 307 |
+
|
| 308 |
+
# (F) Family-pool entities — each pooled entity (standard resource or batch)
|
| 309 |
+
# is drawn from via its own w_{m,e} variable instead of y_m directly.
|
| 310 |
+
# Rule: total resource drawn from entity e ≤ its own declared quantity —
|
| 311 |
+
# exactly like A/B/C, just keyed by the pool draw variable.
|
| 312 |
+
for (m, e), col in pool_pair_to_index.items():
|
| 313 |
+
row = get_row(e)
|
| 314 |
+
row[col] += method_costs.get(m, {}).get(e, 0.0)
|
| 315 |
+
|
| 316 |
+
for name, row in resource_rows.items():
|
| 317 |
if np.any(row != 0):
|
| 318 |
A_rows.append(row)
|
| 319 |
+
b_rows.append(resource_caps.get(name, 0.0))
|
| 320 |
+
constraint_labels.append(("resource", name))
|
| 321 |
|
| 322 |
# (D) Coupling constraints — link z_{m,n} draw variables to y_m output.
|
| 323 |
# D1: Σ z_{m,n} ≤ y_m — can't draw more from batches than the method produces.
|
|
|
|
| 354 |
b_rows.append(0.0)
|
| 355 |
constraint_labels.append(("coupling_D2", (m, ref)))
|
| 356 |
|
| 357 |
+
# (F, coupling) Every unit a method produces must come from exactly one (or
|
| 358 |
+
# a mix) of its pool's entities: Σ w_{m,e} == y_m, expressed as two ≤ rows.
|
| 359 |
+
# Unlike D1/D2 above, both directions always apply — a family pool has no
|
| 360 |
+
# single "reference" resource to silently absorb the remainder.
|
| 361 |
+
for pool_id, entities in pools.items():
|
| 362 |
+
m = pool_id[0]
|
| 363 |
+
j = method_to_index.get(m)
|
| 364 |
+
if j is None:
|
| 365 |
+
continue
|
| 366 |
+
cols = [pool_pair_to_index[(m, e)] for e in entities if (m, e) in pool_pair_to_index]
|
| 367 |
+
if not cols:
|
| 368 |
+
continue
|
| 369 |
+
row1 = np.zeros(num_lp_vars)
|
| 370 |
+
row1[j] = -1.0
|
| 371 |
+
for col in cols:
|
| 372 |
+
row1[col] = 1.0
|
| 373 |
+
A_rows.append(row1)
|
| 374 |
+
b_rows.append(0.0)
|
| 375 |
+
constraint_labels.append(("pool_D1", pool_id))
|
| 376 |
+
|
| 377 |
+
row2 = np.zeros(num_lp_vars)
|
| 378 |
+
row2[j] = 1.0
|
| 379 |
+
for col in cols:
|
| 380 |
+
row2[col] = -1.0
|
| 381 |
+
A_rows.append(row2)
|
| 382 |
+
b_rows.append(0.0)
|
| 383 |
+
constraint_labels.append(("pool_D2", pool_id))
|
| 384 |
+
|
| 385 |
# (E) Method output caps — respect the user's per-method limits from Tab 2.
|
| 386 |
# Absolute cap: y_m ≤ cap_value (in MtCO₂/yr).
|
| 387 |
# Percent cap: y_m ≤ cap_ratio · Σ y_all (fraction of total portfolio output).
|
|
|
|
| 419 |
resource_caps, custom_batch_specs, group_to_custom_batches,
|
| 420 |
custom_pair_to_index, standard_resources,
|
| 421 |
resources_no_batches, resources_with_batches, custom_batch_names,
|
| 422 |
+
pool_of_entity=None, pool_pair_to_index=None,
|
| 423 |
):
|
| 424 |
"""Compute per-method resource consumption from the LP solution.
|
| 425 |
|
|
|
|
| 437 |
resources_no_batches (list[str]): standard resources without substitutes.
|
| 438 |
resources_with_batches (list[str]): standard resources with substitutes.
|
| 439 |
custom_batch_names (set[str]): set of all custom batch names.
|
| 440 |
+
pool_of_entity (dict | None): {(method, entity_name): pool_id} — see
|
| 441 |
+
build_family_pools. Entities here report usage via their own
|
| 442 |
+
w_{m,e} variable instead of the formulas below.
|
| 443 |
+
pool_pair_to_index (dict | None): {(method, entity_name): lp_column_index}.
|
| 444 |
|
| 445 |
Returns:
|
| 446 |
tuple[dict, dict, dict]:
|
|
|
|
| 448 |
resource_used_total – {resource: total_qty_used}.
|
| 449 |
resource_remaining – {resource: qty_remaining}.
|
| 450 |
"""
|
| 451 |
+
pool_of_entity = pool_of_entity or {}
|
| 452 |
+
pool_pair_to_index = pool_pair_to_index or {}
|
| 453 |
+
|
| 454 |
all_resource_names = standard_resources + list(custom_batch_names)
|
| 455 |
resource_usage = {r: {} for r in all_resource_names}
|
| 456 |
|
|
|
|
| 460 |
_USAGE_EPS = 1e-14
|
| 461 |
for r in resources_no_batches:
|
| 462 |
for j, m in enumerate(active_methods):
|
| 463 |
+
if (m, r) in pool_of_entity:
|
| 464 |
+
continue
|
| 465 |
used = method_costs.get(m, {}).get(r, 0.0) * method_outputs[j]
|
| 466 |
if used > _USAGE_EPS:
|
| 467 |
resource_usage[r][m] = float(used)
|
| 468 |
|
| 469 |
for r in resources_with_batches:
|
| 470 |
for j, m in enumerate(active_methods):
|
| 471 |
+
if (m, r) in pool_of_entity:
|
| 472 |
+
continue
|
| 473 |
c_s_m = method_costs.get(m, {}).get(r, 0.0)
|
| 474 |
z_sum = sum(
|
| 475 |
lp_solution[custom_pair_to_index[(m, batch_name)]]
|
|
|
|
| 485 |
for j, m in enumerate(active_methods):
|
| 486 |
if m not in primary_methods:
|
| 487 |
continue
|
| 488 |
+
if (m, batch_name) in pool_of_entity:
|
| 489 |
+
continue
|
| 490 |
c_n_m = method_costs.get(m, {}).get(batch_name, 0.0)
|
| 491 |
if c_n_m <= 0:
|
| 492 |
continue
|
|
|
|
| 498 |
if used > _USAGE_EPS:
|
| 499 |
resource_usage[batch_name][m] = float(used)
|
| 500 |
|
| 501 |
+
# Family-pool entities (standard resources or batches pooled across a
|
| 502 |
+
# method's own resource family) — usage comes directly from w_{m,e}.
|
| 503 |
+
for (m, e), col in pool_pair_to_index.items():
|
| 504 |
+
used = method_costs.get(m, {}).get(e, 0.0) * lp_solution[col]
|
| 505 |
+
if used > _USAGE_EPS:
|
| 506 |
+
resource_usage.setdefault(e, {})[m] = float(used)
|
| 507 |
+
|
| 508 |
_EPS = 1e-14
|
| 509 |
resource_used_total = {
|
| 510 |
r: round(sum(v.values()), 10) for r, v in resource_usage.items()
|
|
|
|
| 526 |
def resolve_pooling_tiebreak(
|
| 527 |
num_methods, num_lp_vars, method_outputs, custom_usage_pairs,
|
| 528 |
custom_pair_to_index, custom_batch_specs, method_costs, A_ub, b_ub, bounds,
|
| 529 |
+
pool_pair_to_index=None,
|
| 530 |
):
|
| 531 |
"""Re-solve the LP with each method's output pinned, preferring the more
|
| 532 |
+
efficient resource within each standard/custom pooling pair or family pool.
|
| 533 |
|
| 534 |
The main LP (run_optimization) maximises total CO2 removed only — nothing
|
| 535 |
in its objective distinguishes a standard resource from a custom batch
|
| 536 |
+
that substitutes for it (or from a same-family alternative resource), so
|
| 537 |
+
when several could satisfy the same demand the solver picks an arbitrary
|
| 538 |
+
split. This second pass fixes that: it pins every active method's output
|
| 539 |
+
y_m to its pass-1 value (so total_removed and method_usage never change)
|
| 540 |
+
and minimises Σ (c_n - c_s) · z_{m,n} across all (method, batch) pooling
|
| 541 |
+
pairs, plus Σ c_e · w_{m,e} across all family-pool entities — i.e. it
|
| 542 |
+
prefers fully using whichever resource has the lower coefficient (more
|
| 543 |
+
MtCO2 removed per unit of resource) before drawing on the less efficient
|
| 544 |
+
one. For a two-entity pool this reduces exactly to the batch-pooling
|
| 545 |
+
formula (pinning w_other = y_m - w_e turns Σ c_e·w_e into a (c_e - c_other)
|
| 546 |
+
term up to a constant), so it generalises the original tie-break rather
|
| 547 |
+
than replacing it.
|
| 548 |
|
| 549 |
Args:
|
| 550 |
num_methods (int): number of y_m variables.
|
|
|
|
| 557 |
A_ub, b_ub: pass-1 constraint matrix/vector (reused unchanged — pass-1's
|
| 558 |
own solution already satisfies them, so this second LP is always feasible).
|
| 559 |
bounds: pass-1 variable bounds (reused unchanged).
|
| 560 |
+
pool_pair_to_index (dict | None): {(method, entity_name): lp_column_index}
|
| 561 |
+
for family-pool draw variables — see build_family_pools.
|
| 562 |
|
| 563 |
Returns:
|
| 564 |
np.ndarray | None: the tie-broken solution vector, or None if there are
|
| 565 |
no pooling pairs to break ties on, or if the second solve fails
|
| 566 |
(in which case the caller should fall back to the pass-1 solution).
|
| 567 |
"""
|
| 568 |
+
pool_pair_to_index = pool_pair_to_index or {}
|
| 569 |
+
if not custom_usage_pairs and not pool_pair_to_index:
|
| 570 |
return None
|
| 571 |
|
| 572 |
A_eq = np.zeros((num_methods, num_lp_vars))
|
|
|
|
| 581 |
c_n = method_costs.get(m, {}).get(batch_name, 0.0)
|
| 582 |
objective2[custom_pair_to_index[(m, batch_name)]] = c_n - c_s
|
| 583 |
|
| 584 |
+
for (m, e), col in pool_pair_to_index.items():
|
| 585 |
+
objective2[col] = method_costs.get(m, {}).get(e, 0.0)
|
| 586 |
+
|
| 587 |
try:
|
| 588 |
result = linprog(
|
| 589 |
objective2, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq,
|
|
|
|
| 630 |
return resource_actual_gain
|
| 631 |
|
| 632 |
|
| 633 |
+
def compute_standalone_potential(method, resource_caps, method_costs, custom_resources, enabled_methods, resource_to_group=None):
|
| 634 |
"""Compute a method's own maximum achievable output, in isolation.
|
| 635 |
|
| 636 |
"Isolation" means solving as if this were the only active method — no
|
|
|
|
| 649 |
full list — batches tied to other methods simply produce no
|
| 650 |
usage pairs once every other method is excluded).
|
| 651 |
enabled_methods (dict | None): {resource: {method: bool}}.
|
| 652 |
+
resource_to_group (dict | None): {resource: family group name} — see
|
| 653 |
+
build_family_pools. Must match what the outer run_optimization
|
| 654 |
+
call uses, so the method's standalone potential already reflects
|
| 655 |
+
OR-availability across its own same-family resources.
|
| 656 |
|
| 657 |
Returns:
|
| 658 |
float: the method's standalone potential in MtCO₂ (0.0 if it can't
|
|
|
|
| 665 |
method_costs={method: dict(method_costs.get(method, {}))},
|
| 666 |
custom_resources=custom_resources,
|
| 667 |
enabled_methods=enabled_methods,
|
| 668 |
+
resource_to_group=resource_to_group,
|
| 669 |
)
|
| 670 |
return result["total_removed"] if success else 0.0
|
| 671 |
|
|
|
|
| 676 |
method_costs,
|
| 677 |
custom_resources=None,
|
| 678 |
enabled_methods=None,
|
| 679 |
+
resource_to_group=None,
|
| 680 |
):
|
| 681 |
"""Maximize total CO₂ removal subject to resource caps and method constraints.
|
| 682 |
|
|
|
|
| 684 |
Also runs a perturbation analysis: for each binding resource constraint,
|
| 685 |
re-solves with cap + 1 to compute the marginal CDR gain per unit.
|
| 686 |
|
| 687 |
+
LP variables (three types, packed into one vector x):
|
| 688 |
y_m = MtCO₂ removed by CDR method m — the main "answer" (indices 0..num_methods-1).
|
| 689 |
z_{m,n} = MtCO₂ that method m draws specifically from custom batch n — a helper
|
| 690 |
variable introduced when the user adds a custom resource batch; it lets the
|
| 691 |
+
solver track which batch supplies which method without double-counting.
|
| 692 |
+
w_{m,e} = MtCO₂ that method m draws specifically from same-family entity e (a
|
| 693 |
+
standard resource or custom batch inside an OR-substitutable resource
|
| 694 |
+
pool — see build_family_pools); same bookkeeping role as z_{m,n}, for
|
| 695 |
+
resources that compete with each other rather than with the standard/
|
| 696 |
+
custom-batch split.
|
| 697 |
+
(z and w columns together occupy indices num_methods..num_lp_vars-1.)
|
| 698 |
|
| 699 |
Args:
|
| 700 |
resource_caps (dict): {resource: available amount (float)}.
|
|
|
|
| 704 |
name, group, amount, methods.
|
| 705 |
enabled_methods (dict | None): {resource: {method: bool}} — False entries
|
| 706 |
zero-out the corresponding coefficient before solving.
|
| 707 |
+
resource_to_group (dict | None): {resource: family group name} (e.g.
|
| 708 |
+
"Arable land" → "Land"). When a method has 2+ resources from the
|
| 709 |
+
same family with a non-zero coefficient, they're treated as
|
| 710 |
+
OR-substitutable (draw from either, or a mix) instead of both
|
| 711 |
+
being required at once — see build_family_pools. None (default)
|
| 712 |
+
disables this and preserves the original AND-of-all-resources
|
| 713 |
+
behaviour.
|
| 714 |
|
| 715 |
Returns:
|
| 716 |
tuple[bool, dict]:
|
|
|
|
| 752 |
cap_value = max(0.0, min(100.0, c.get("cap_value", 100)))
|
| 753 |
potential = compute_standalone_potential(
|
| 754 |
m, resource_caps, method_costs, custom_resources, enabled_methods,
|
| 755 |
+
resource_to_group=resource_to_group,
|
| 756 |
)
|
| 757 |
resolved_constraints[m] = {**c, "cap_type": "absolute", "cap_value": potential * cap_value / 100.0}
|
| 758 |
else:
|
|
|
|
| 802 |
}
|
| 803 |
return True, empty_result
|
| 804 |
|
| 805 |
+
pool_of_entity, pools = build_family_pools(
|
| 806 |
+
active_methods, method_costs, resource_to_group, group_to_custom_batches, custom_batch_specs
|
| 807 |
+
)
|
| 808 |
+
|
| 809 |
custom_usage_pairs, custom_pair_to_index, num_lp_vars = build_lp_indices(
|
| 810 |
+
active_methods, num_methods, custom_batch_specs, resource_caps, method_costs, pool_of_entity
|
| 811 |
+
)
|
| 812 |
+
pool_pairs, pool_pair_to_index, num_lp_vars = build_pool_indices(
|
| 813 |
+
num_lp_vars, pool_of_entity, resource_caps
|
| 814 |
)
|
| 815 |
|
| 816 |
standard_resources = [r for r in resource_caps if r not in custom_batch_names]
|
|
|
|
| 824 |
custom_batch_specs, group_to_custom_batches,
|
| 825 |
resources_no_batches, resources_with_batches,
|
| 826 |
custom_usage_pairs, custom_pair_to_index,
|
| 827 |
+
pool_of_entity, pools, pool_pair_to_index,
|
| 828 |
)
|
| 829 |
except ValueError as e:
|
| 830 |
return False, {"message": str(e)}
|
|
|
|
| 850 |
tiebreak_solution = resolve_pooling_tiebreak(
|
| 851 |
num_methods, num_lp_vars, method_outputs, custom_usage_pairs,
|
| 852 |
custom_pair_to_index, custom_batch_specs, method_costs, A_ub, b_ub, bounds,
|
| 853 |
+
pool_pair_to_index,
|
| 854 |
)
|
| 855 |
lp_solution_for_usage = tiebreak_solution if tiebreak_solution is not None else lp_solution
|
| 856 |
|
|
|
|
| 859 |
resource_caps, custom_batch_specs, group_to_custom_batches,
|
| 860 |
custom_pair_to_index, standard_resources,
|
| 861 |
resources_no_batches, resources_with_batches, custom_batch_names,
|
| 862 |
+
pool_of_entity, pool_pair_to_index,
|
| 863 |
)
|
| 864 |
|
| 865 |
resource_actual_gain = run_perturbation(
|
tabs/tab2_constraints.py
CHANGED
|
@@ -3,7 +3,7 @@ import streamlit as st
|
|
| 3 |
|
| 4 |
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips, method_notes, secondary_resources, hidden_resource_names, section_intros
|
| 5 |
from utils.ui_helpers import show_bar
|
| 6 |
-
from utils.data_utils import strip_unit_suffix, get_other_method_variants, make_variant_name, default_constraint
|
| 7 |
|
| 8 |
|
| 9 |
def _make_slider_cb(m):
|
|
@@ -56,27 +56,55 @@ def get_all_missing_required(m, cost_sliders_norm):
|
|
| 56 |
"No active methods" once every resource of a method is disabled this way.
|
| 57 |
Secondary resources and resources with median <= 0 are excluded.
|
| 58 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
Returns:
|
| 60 |
list[str]: missing required resource names (standard and hidden combined).
|
| 61 |
"""
|
| 62 |
resource_caps = st.session_state.get("resource_caps", {})
|
| 63 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 64 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
|
|
|
| 65 |
covered_by_batch = {
|
| 66 |
b["group"] for b in custom_resources
|
| 67 |
if m in b.get("methods", {})
|
| 68 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
missing = []
|
| 70 |
for r, coeff_dict in cost_sliders_norm.get(m, {}).items():
|
| 71 |
if (m, r) in secondary_resources:
|
| 72 |
continue
|
| 73 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 74 |
continue
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
elif not
|
| 80 |
missing.append(r)
|
| 81 |
return missing
|
| 82 |
|
|
@@ -128,10 +156,18 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 128 |
batch covers it.
|
| 129 |
Hidden resources: ✅ if covered by a custom batch, ❌ if not.
|
| 130 |
Secondary resources are shown in a separate section.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
"""
|
| 132 |
resource_caps = st.session_state.get("resource_caps", {})
|
| 133 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 134 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
|
|
|
| 135 |
# Map resource group → list of batch names covering it for method m (any
|
| 136 |
# named standard resource or hidden "Other X" resource can be substituted
|
| 137 |
# by a custom batch referencing it).
|
|
@@ -141,6 +177,22 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 141 |
covered_by_batch.setdefault(b["group"], []).append(b["name"])
|
| 142 |
|
| 143 |
all_res = cost_sliders_norm.get(m, {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 144 |
required_rows = []
|
| 145 |
optional_rows = []
|
| 146 |
|
|
@@ -148,14 +200,13 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 148 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 149 |
continue
|
| 150 |
batch_names = covered_by_batch.get(r)
|
|
|
|
| 151 |
if r in hidden_resource_names:
|
| 152 |
-
available = bool(batch_names)
|
| 153 |
# Show actual batch name(s) instead of the generic "Other X" group
|
| 154 |
display_r = ", ".join(batch_names) if batch_names else r
|
| 155 |
source = "Add as new resource in Resource inputs"
|
| 156 |
else:
|
| 157 |
disabled_for_method = not enabled_methods.get(r, {}).get(m, True)
|
| 158 |
-
available = (resource_caps.get(r, 0.0) > 0 and not disabled_for_method) or bool(batch_names)
|
| 159 |
display_r = ", ".join(batch_names) if (not resource_caps.get(r, 0.0) > 0 and batch_names) else r
|
| 160 |
source = (
|
| 161 |
"Re-enable this method in the Related CDR Methods popover (Resource inputs tab)"
|
|
@@ -163,10 +214,30 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 163 |
else "Set quantity in Resource inputs"
|
| 164 |
)
|
| 165 |
|
| 166 |
-
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
|
| 169 |
-
|
|
|
|
|
|
|
| 170 |
optional_rows.append(row)
|
| 171 |
else:
|
| 172 |
required_rows.append(row)
|
|
@@ -177,16 +248,16 @@ def _render_resource_status(m, cost_sliders_norm):
|
|
| 177 |
st.info(note, icon=":material/lightbulb:")
|
| 178 |
if required_rows:
|
| 179 |
st.markdown("**Required resources**")
|
| 180 |
-
for _, r, hint in required_rows:
|
| 181 |
-
color = "#
|
| 182 |
label = f'<span style="color:{color};font-weight:600"><strong>{r}</strong></span>'
|
| 183 |
if hint:
|
| 184 |
label += f' <span style="color:#888;font-style:italic;font-weight:400">: {hint}</span>'
|
| 185 |
st.markdown(label, unsafe_allow_html=True)
|
| 186 |
if optional_rows:
|
| 187 |
st.markdown("**Optional resources**")
|
| 188 |
-
for _, r, hint in optional_rows:
|
| 189 |
-
color = "#
|
| 190 |
label = f'<span style="color:{color};font-weight:600"><strong>{r}</strong></span>'
|
| 191 |
if hint:
|
| 192 |
label += f' <span style="color:#888;font-style:italic;font-weight:400">: {hint}</span>'
|
|
|
|
| 3 |
|
| 4 |
from config.config import CO2_UNIT, method_groups, method_order, method_tooltips, method_notes, secondary_resources, hidden_resource_names, section_intros
|
| 5 |
from utils.ui_helpers import show_bar
|
| 6 |
+
from utils.data_utils import strip_unit_suffix, get_other_method_variants, make_variant_name, default_constraint, get_resource_to_group
|
| 7 |
|
| 8 |
|
| 9 |
def _make_slider_cb(m):
|
|
|
|
| 56 |
"No active methods" once every resource of a method is disabled this way.
|
| 57 |
Secondary resources and resources with median <= 0 are excluded.
|
| 58 |
|
| 59 |
+
Family pooling: when two or more of the method's resources share the same
|
| 60 |
+
resource family (e.g. "Arable land" and "Other land" are both "Land" — see
|
| 61 |
+
build_family_pools in optimization.py), they are OR-substitutable rather
|
| 62 |
+
than all-required. A resource in such a family is only reported as missing
|
| 63 |
+
if every member of that family is unavailable; a family member currently
|
| 64 |
+
enabled as a secondary resource (its slider toggled on in Tab 3) also
|
| 65 |
+
counts as available, since it becomes a real substitute in the LP.
|
| 66 |
+
|
| 67 |
Returns:
|
| 68 |
list[str]: missing required resource names (standard and hidden combined).
|
| 69 |
"""
|
| 70 |
resource_caps = st.session_state.get("resource_caps", {})
|
| 71 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 72 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 73 |
+
method_costs = st.session_state.get("costs", {}).get(m, {})
|
| 74 |
covered_by_batch = {
|
| 75 |
b["group"] for b in custom_resources
|
| 76 |
if m in b.get("methods", {})
|
| 77 |
}
|
| 78 |
+
resource_to_group = get_resource_to_group()
|
| 79 |
+
|
| 80 |
+
def _available(r):
|
| 81 |
+
if r in covered_by_batch:
|
| 82 |
+
return True
|
| 83 |
+
if resource_caps.get(r, 0.0) <= 0:
|
| 84 |
+
return False
|
| 85 |
+
return enabled_methods.get(r, {}).get(m, True)
|
| 86 |
+
|
| 87 |
+
# Family membership includes this method's primary resources plus any
|
| 88 |
+
# secondary resource currently switched on (cost > 0) — both act as real
|
| 89 |
+
# substitutes for one another in the LP once active.
|
| 90 |
+
family_members: dict[str, list[str]] = {}
|
| 91 |
+
for r in cost_sliders_norm.get(m, {}):
|
| 92 |
+
is_secondary = (m, r) in secondary_resources
|
| 93 |
+
if is_secondary and method_costs.get(r, 0.0) <= 0:
|
| 94 |
+
continue
|
| 95 |
+
family_members.setdefault(resource_to_group.get(r, r), []).append(r)
|
| 96 |
+
|
| 97 |
missing = []
|
| 98 |
for r, coeff_dict in cost_sliders_norm.get(m, {}).items():
|
| 99 |
if (m, r) in secondary_resources:
|
| 100 |
continue
|
| 101 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 102 |
continue
|
| 103 |
+
siblings = family_members.get(resource_to_group.get(r, r), [r])
|
| 104 |
+
if len(siblings) > 1:
|
| 105 |
+
if not any(_available(s) for s in siblings):
|
| 106 |
+
missing.append(r)
|
| 107 |
+
elif not _available(r):
|
| 108 |
missing.append(r)
|
| 109 |
return missing
|
| 110 |
|
|
|
|
| 156 |
batch covers it.
|
| 157 |
Hidden resources: ✅ if covered by a custom batch, ❌ if not.
|
| 158 |
Secondary resources are shown in a separate section.
|
| 159 |
+
|
| 160 |
+
Family pooling: when 2+ of the method's required resources share the same
|
| 161 |
+
resource family (e.g. "Forest biomass" / "Other biomass" are both
|
| 162 |
+
"Biomass" — see build_family_pools in optimization.py), only one of them
|
| 163 |
+
is actually needed. A family member that isn't set is shown as ✅
|
| 164 |
+
"optional" (not a blocking ❌) once a sibling already satisfies the
|
| 165 |
+
family, since adding it is a bonus, not a requirement.
|
| 166 |
"""
|
| 167 |
resource_caps = st.session_state.get("resource_caps", {})
|
| 168 |
custom_resources = st.session_state.get("custom_resources", [])
|
| 169 |
enabled_methods = st.session_state.get("enabled_methods", {})
|
| 170 |
+
resource_to_group = get_resource_to_group()
|
| 171 |
# Map resource group → list of batch names covering it for method m (any
|
| 172 |
# named standard resource or hidden "Other X" resource can be substituted
|
| 173 |
# by a custom batch referencing it).
|
|
|
|
| 177 |
covered_by_batch.setdefault(b["group"], []).append(b["name"])
|
| 178 |
|
| 179 |
all_res = cost_sliders_norm.get(m, {})
|
| 180 |
+
|
| 181 |
+
def _is_available(r):
|
| 182 |
+
batch_names = covered_by_batch.get(r)
|
| 183 |
+
if r in hidden_resource_names:
|
| 184 |
+
return bool(batch_names)
|
| 185 |
+
disabled_for_method = not enabled_methods.get(r, {}).get(m, True)
|
| 186 |
+
return (resource_caps.get(r, 0.0) > 0 and not disabled_for_method) or bool(batch_names)
|
| 187 |
+
|
| 188 |
+
# Group this method's required (non-secondary) resources by family so
|
| 189 |
+
# siblings can be recognised as OR-substitutable below.
|
| 190 |
+
primary_by_family: dict[str, list[str]] = {}
|
| 191 |
+
for r, coeff_dict in all_res.items():
|
| 192 |
+
if coeff_dict.get("median", 0.0) <= 0 or (m, r) in secondary_resources:
|
| 193 |
+
continue
|
| 194 |
+
primary_by_family.setdefault(resource_to_group.get(r, r), []).append(r)
|
| 195 |
+
|
| 196 |
required_rows = []
|
| 197 |
optional_rows = []
|
| 198 |
|
|
|
|
| 200 |
if coeff_dict.get("median", 0.0) <= 0:
|
| 201 |
continue
|
| 202 |
batch_names = covered_by_batch.get(r)
|
| 203 |
+
available = _is_available(r)
|
| 204 |
if r in hidden_resource_names:
|
|
|
|
| 205 |
# Show actual batch name(s) instead of the generic "Other X" group
|
| 206 |
display_r = ", ".join(batch_names) if batch_names else r
|
| 207 |
source = "Add as new resource in Resource inputs"
|
| 208 |
else:
|
| 209 |
disabled_for_method = not enabled_methods.get(r, {}).get(m, True)
|
|
|
|
| 210 |
display_r = ", ".join(batch_names) if (not resource_caps.get(r, 0.0) > 0 and batch_names) else r
|
| 211 |
source = (
|
| 212 |
"Re-enable this method in the Related CDR Methods popover (Resource inputs tab)"
|
|
|
|
| 214 |
else "Set quantity in Resource inputs"
|
| 215 |
)
|
| 216 |
|
| 217 |
+
is_secondary = (m, r) in secondary_resources
|
| 218 |
+
siblings = primary_by_family.get(resource_to_group.get(r, r), [r]) if not is_secondary else [r]
|
| 219 |
+
available_siblings = [s for s in siblings if s != r and _is_available(s)]
|
| 220 |
+
|
| 221 |
+
if available and len(siblings) > 1:
|
| 222 |
+
icon, hint, is_blocking = "✅", "", False
|
| 223 |
+
elif not available and available_siblings:
|
| 224 |
+
# Family already satisfied by a sibling — not a blocker, just a bonus.
|
| 225 |
+
icon, is_blocking = "✅", False
|
| 226 |
+
hint = (
|
| 227 |
+
f"{', '.join(available_siblings)} already covers this "
|
| 228 |
+
f"(only one of {', '.join(siblings)} is required). You can still add "
|
| 229 |
+
f"{r} too."
|
| 230 |
+
)
|
| 231 |
+
elif not available and len(siblings) > 1:
|
| 232 |
+
icon, is_blocking = "❌", True
|
| 233 |
+
hint = f"{source} (only one of {', '.join(siblings)} is required)"
|
| 234 |
+
else:
|
| 235 |
+
icon, is_blocking = ("✅", False) if available else ("❌", True)
|
| 236 |
+
hint = "" if available else source
|
| 237 |
|
| 238 |
+
row = (icon, display_r, hint, is_blocking)
|
| 239 |
+
|
| 240 |
+
if is_secondary:
|
| 241 |
optional_rows.append(row)
|
| 242 |
else:
|
| 243 |
required_rows.append(row)
|
|
|
|
| 248 |
st.info(note, icon=":material/lightbulb:")
|
| 249 |
if required_rows:
|
| 250 |
st.markdown("**Required resources**")
|
| 251 |
+
for _, r, hint, is_blocking in required_rows:
|
| 252 |
+
color = "#FF5F4D" if is_blocking else "#55C95B"
|
| 253 |
label = f'<span style="color:{color};font-weight:600"><strong>{r}</strong></span>'
|
| 254 |
if hint:
|
| 255 |
label += f' <span style="color:#888;font-style:italic;font-weight:400">: {hint}</span>'
|
| 256 |
st.markdown(label, unsafe_allow_html=True)
|
| 257 |
if optional_rows:
|
| 258 |
st.markdown("**Optional resources**")
|
| 259 |
+
for _, r, hint, is_blocking in optional_rows:
|
| 260 |
+
color = "#FF5F4D" if is_blocking else "#55C95B"
|
| 261 |
label = f'<span style="color:{color};font-weight:600"><strong>{r}</strong></span>'
|
| 262 |
if hint:
|
| 263 |
label += f' <span style="color:#888;font-style:italic;font-weight:400">: {hint}</span>'
|
tabs/tab4_optimization.py
CHANGED
|
@@ -8,7 +8,7 @@ import openpyxl
|
|
| 8 |
import pandas as pd
|
| 9 |
import streamlit as st
|
| 10 |
import plotly.express as px
|
| 11 |
-
from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT, method_order
|
| 12 |
from optimization.optimization import run_optimization
|
| 13 |
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 14 |
from utils.data_utils import (is_valid_scenario, is_complete_scenario, load_inputs_from_excel,
|
|
@@ -206,6 +206,7 @@ def parse_zip_scenario(uploaded) -> dict | None:
|
|
| 206 |
method_costs=costs,
|
| 207 |
custom_resources=custom_resources,
|
| 208 |
enabled_methods=enabled_methods,
|
|
|
|
| 209 |
)
|
| 210 |
|
| 211 |
if neutralized:
|
|
@@ -254,7 +255,7 @@ def panel_load_from_json_or_csv():
|
|
| 254 |
|
| 255 |
if "scenario_loaded_file" in st.session_state:
|
| 256 |
st.success(
|
| 257 |
-
f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated -
|
| 258 |
icon="✅"
|
| 259 |
)
|
| 260 |
uploaded_json = st.file_uploader(
|
|
@@ -280,6 +281,7 @@ def panel_load_from_json_or_csv():
|
|
| 280 |
method_costs=data["method_costs"],
|
| 281 |
custom_resources=data.get("custom_resources", []),
|
| 282 |
enabled_methods=data.get("enabled_methods", {}),
|
|
|
|
| 283 |
)
|
| 284 |
if success:
|
| 285 |
data["resource_usage"] = result["resource_usage"]
|
|
@@ -507,10 +509,11 @@ def run_and_store_result():
|
|
| 507 |
method_costs=applied_costs,
|
| 508 |
custom_resources=custom_resources_for_lp,
|
| 509 |
enabled_methods=enabled_methods,
|
|
|
|
| 510 |
)
|
| 511 |
|
| 512 |
if not success:
|
| 513 |
-
st.error("❌
|
| 514 |
if "message" in result:
|
| 515 |
st.error(result["message"])
|
| 516 |
return
|
|
@@ -526,7 +529,7 @@ def run_and_store_result():
|
|
| 526 |
total_removed = result["total_removed"]
|
| 527 |
|
| 528 |
if total_removed <= 0:
|
| 529 |
-
st.warning(f"❌
|
| 530 |
return
|
| 531 |
|
| 532 |
total_cost = sum(
|
|
@@ -1328,9 +1331,9 @@ def render_tab():
|
|
| 1328 |
)
|
| 1329 |
|
| 1330 |
st.markdown(
|
| 1331 |
-
"""
|
| 1332 |
<div class="text_with_border">
|
| 1333 |
-
<p>
|
| 1334 |
</div>
|
| 1335 |
""",
|
| 1336 |
unsafe_allow_html=True
|
|
@@ -1355,7 +1358,7 @@ def render_tab():
|
|
| 1355 |
|
| 1356 |
with tab_run:
|
| 1357 |
st.markdown("""
|
| 1358 |
-
<p class="crra-sub-heading">Run
|
| 1359 |
unsafe_allow_html=True,
|
| 1360 |
)
|
| 1361 |
col_inputs, col_scenario = st.columns(2, gap="large")
|
|
@@ -1365,7 +1368,7 @@ def render_tab():
|
|
| 1365 |
<p class="crra-sub-sub-heading">From inputs</p>""",
|
| 1366 |
unsafe_allow_html=True,
|
| 1367 |
)
|
| 1368 |
-
run_clicked = st.button("Run
|
| 1369 |
run_msg_area = st.container()
|
| 1370 |
panel_run_from_tabs()
|
| 1371 |
panel_load_from_excel()
|
|
@@ -1381,7 +1384,7 @@ def render_tab():
|
|
| 1381 |
with run_msg_area:
|
| 1382 |
run_and_store_result()
|
| 1383 |
|
| 1384 |
-
no_results = '<p class="caption">Run the
|
| 1385 |
|
| 1386 |
latest = st.session_state.get("latest_result")
|
| 1387 |
if not latest:
|
|
@@ -1415,7 +1418,7 @@ def render_tab():
|
|
| 1415 |
|
| 1416 |
with tab_results:
|
| 1417 |
show_metrics(latest)
|
| 1418 |
-
st.markdown('<p class="crra-sub-heading">
|
| 1419 |
st.dataframe(df_display, width='stretch')
|
| 1420 |
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
| 1421 |
st.markdown(
|
|
|
|
| 8 |
import pandas as pd
|
| 9 |
import streamlit as st
|
| 10 |
import plotly.express as px
|
| 11 |
+
from config.config import CRRA_COLORS, PLOTLY_COLOR_SEQUENCE, PLOTLY_HEATMAP_SCALE, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT, method_order, section_intros
|
| 12 |
from optimization.optimization import run_optimization
|
| 13 |
from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
|
| 14 |
from utils.data_utils import (is_valid_scenario, is_complete_scenario, load_inputs_from_excel,
|
|
|
|
| 206 |
method_costs=costs,
|
| 207 |
custom_resources=custom_resources,
|
| 208 |
enabled_methods=enabled_methods,
|
| 209 |
+
resource_to_group=get_resource_to_group(),
|
| 210 |
)
|
| 211 |
|
| 212 |
if neutralized:
|
|
|
|
| 255 |
|
| 256 |
if "scenario_loaded_file" in st.session_state:
|
| 257 |
st.success(
|
| 258 |
+
f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated - Optimization results displayed.",
|
| 259 |
icon="✅"
|
| 260 |
)
|
| 261 |
uploaded_json = st.file_uploader(
|
|
|
|
| 281 |
method_costs=data["method_costs"],
|
| 282 |
custom_resources=data.get("custom_resources", []),
|
| 283 |
enabled_methods=data.get("enabled_methods", {}),
|
| 284 |
+
resource_to_group=get_resource_to_group(),
|
| 285 |
)
|
| 286 |
if success:
|
| 287 |
data["resource_usage"] = result["resource_usage"]
|
|
|
|
| 509 |
method_costs=applied_costs,
|
| 510 |
custom_resources=custom_resources_for_lp,
|
| 511 |
enabled_methods=enabled_methods,
|
| 512 |
+
resource_to_group=get_resource_to_group(),
|
| 513 |
)
|
| 514 |
|
| 515 |
if not success:
|
| 516 |
+
st.error("❌ Optimization failed")
|
| 517 |
if "message" in result:
|
| 518 |
st.error(result["message"])
|
| 519 |
return
|
|
|
|
| 529 |
total_removed = result["total_removed"]
|
| 530 |
|
| 531 |
if total_removed <= 0:
|
| 532 |
+
st.warning(f"❌ Optimization removed 0 {CO2_UNIT}. Relax constraints or increase resources.", icon=":material/warning:")
|
| 533 |
return
|
| 534 |
|
| 535 |
total_cost = sum(
|
|
|
|
| 1331 |
)
|
| 1332 |
|
| 1333 |
st.markdown(
|
| 1334 |
+
f"""
|
| 1335 |
<div class="text_with_border">
|
| 1336 |
+
<p>{section_intros.get("tab4_optimization", "")}</p>
|
| 1337 |
</div>
|
| 1338 |
""",
|
| 1339 |
unsafe_allow_html=True
|
|
|
|
| 1358 |
|
| 1359 |
with tab_run:
|
| 1360 |
st.markdown("""
|
| 1361 |
+
<p class="crra-sub-heading">Run optimization</p>""",
|
| 1362 |
unsafe_allow_html=True,
|
| 1363 |
)
|
| 1364 |
col_inputs, col_scenario = st.columns(2, gap="large")
|
|
|
|
| 1368 |
<p class="crra-sub-sub-heading">From inputs</p>""",
|
| 1369 |
unsafe_allow_html=True,
|
| 1370 |
)
|
| 1371 |
+
run_clicked = st.button("Run Optimization", icon=":material/play_arrow:", key="btn-run-optimization", type="primary")
|
| 1372 |
run_msg_area = st.container()
|
| 1373 |
panel_run_from_tabs()
|
| 1374 |
panel_load_from_excel()
|
|
|
|
| 1384 |
with run_msg_area:
|
| 1385 |
run_and_store_result()
|
| 1386 |
|
| 1387 |
+
no_results = '<p class="caption">Run the optimization first to see results here.</p>'
|
| 1388 |
|
| 1389 |
latest = st.session_state.get("latest_result")
|
| 1390 |
if not latest:
|
|
|
|
| 1418 |
|
| 1419 |
with tab_results:
|
| 1420 |
show_metrics(latest)
|
| 1421 |
+
st.markdown('<p class="crra-sub-heading">Optimization Summary</p>', unsafe_allow_html=True)
|
| 1422 |
st.dataframe(df_display, width='stretch')
|
| 1423 |
st.markdown('<p class="crra-sub-heading">Method allocation breakdown</p>', unsafe_allow_html=True)
|
| 1424 |
st.markdown(
|
utils/data_utils.py
CHANGED
|
@@ -365,6 +365,7 @@ def run_expanded_optimization(resource_caps, method_constraints, costs, custom_r
|
|
| 365 |
method_costs=applied_costs,
|
| 366 |
custom_resources=custom_resources_for_lp,
|
| 367 |
enabled_methods=enabled_methods,
|
|
|
|
| 368 |
)
|
| 369 |
|
| 370 |
|
|
|
|
| 365 |
method_costs=applied_costs,
|
| 366 |
custom_resources=custom_resources_for_lp,
|
| 367 |
enabled_methods=enabled_methods,
|
| 368 |
+
resource_to_group=get_resource_to_group(),
|
| 369 |
)
|
| 370 |
|
| 371 |
|