CarlyneB commited on
Commit
60dd41c
·
1 Parent(s): afbc8e6

[FEAT] Add OR-substitutable resource family pools and rename inputs.csv

Browse files
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 inputs_aggregated.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_aggregated.csv"
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("inputs_aggregated.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)
 
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
- 3d6bb195ae77cbc0aaa32556f2219657
 
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.33,
29
- "median": 0.37,
30
- "max": 0.82
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": 350.0,
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
- "Non-forest biomass (Mt/MtCO2)": {
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
- "Non-arable land (Mha/MtCO2)": {
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": 388750.0,
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": 388750.0,
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": 388750.0,
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": 388750.0,
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 inputs_aggregated.csv."""
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/inputs_aggregated.csv")
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,False
3
- Afforestation,Forestry,Other water,Water,Physical,bcm/MtCO2,2,0.092,0.092,0.092,0.092,0.0,0.092,0.092,True
4
- Agroforestry,Forestry,Arable land,Land,Physical,Mha/MtCO2,12,0.039953,0.450767,0.064101,0.2141,0.90114,0.0025,3.030303,False
5
- Biochar - Forest biomass,Biochar,Electrical energy,Energy,Energy,MWh/MtCO2,6,87.5,167000.0,350.0,375000.0,258000.0,0.0,500000.0,True
6
- Biochar - Forest biomass,Biochar,Forest biomass,Biomass,Physical,Mt/MtCO2,6,0.34,0.472833,0.37,0.55525,0.201624,0.33,0.82,False
7
- Biochar - Forest biomass,Biochar,Other land,Land,Physical,Mha/MtCO2,4,0.056402,0.163201,0.163201,0.27,0.123321,0.056402,0.27,True
8
- Biochar - Forest biomass,Biochar,Other water,Water,Physical,bcm/MtCO2,9,0.0,0.000633,0.0001,0.0005,0.001377,-0.0005,0.003,True
9
- Biochar - Non-forest biomass,Biochar,Electrical energy,Energy,Energy,MWh/MtCO2,6,87.5,167000.0,350.0,375000.0,258000.0,0.0,500000.0,True
10
- Biochar - Non-forest biomass,Biochar,Non-forest biomass,Biomass,Physical,Mt/MtCO2,8,1.142,1.291,1.3395,1.4885,0.33202,0.818,1.667,False
11
- Biochar - Non-forest biomass,Biochar,Other land,Land,Physical,Mha/MtCO2,4,0.056402,0.163201,0.163201,0.27,0.123321,0.056402,0.27,True
12
- Biochar - Non-forest biomass,Biochar,Other water,Water,Physical,bcm/MtCO2,9,0.0,0.000633,0.0001,0.0005,0.001377,-0.0005,0.003,True
13
- Biochar - Other biomass,Biochar,Electrical energy,Energy,Energy,MWh/MtCO2,6,87.5,167000.0,350.0,375000.0,258000.0,0.0,500000.0,True
14
- Biochar - Other biomass,Biochar,Other biomass,Biomass,Physical,Mt/MtCO2,48,0.943396,1.924255,1.209221,2.522177,1.937824,0.5,12.86,False
15
- Biochar - Other biomass,Biochar,Other land,Land,Physical,Mha/MtCO2,4,0.056402,0.163201,0.163201,0.27,0.123321,0.056402,0.27,True
16
- Biochar - Other biomass,Biochar,Other water,Water,Physical,bcm/MtCO2,9,0.0,0.000633,0.0001,0.0005,0.001377,-0.0005,0.003,True
17
- BioCCS with anaerobic digestion/fermentation,Bio-CCS,Non-forest biomass,Biomass,Physical,Mt/MtCO2,6,1.428571,2.689964,1.561743,2.128274,2.62425,1.315,8.0,False
18
- BioCCS with anaerobic digestion/fermentation,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
19
- BioCCS with anaerobic digestion/fermentation,Bio-CCS,Other biomass,Biomass,Physical,Mt/MtCO2,6,1.03357,1.379365,1.14,1.785,0.484913,0.998094,2.0,False
20
- BioCCS with anaerobic digestion/fermentation,Bio-CCS,Other water,Water,Physical,bcm/MtCO2,14,0.395455,0.569805,0.565909,0.654546,0.183277,0.368182,0.913636,True
21
- BioCCS with anaerobic digestion/fermentation & growing biomass,Bio-CCS,Arable land,Land,Physical,Mha/MtCO2,16,0.037937,0.100867,0.058844,0.103034,0.113758,0.022803,0.454545,False
22
- BioCCS with anaerobic digestion/fermentation & growing biomass,Bio-CCS,Other water,Water,Physical,bcm/MtCO2,14,0.395455,0.569805,0.565909,0.654546,0.183277,0.368182,0.913636,True
23
- BioCCS with anaerobic digestion/fermentation & growing biomass,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
24
- BioCCS with combustion,Bio-CCS,Electrical energy,Energy,Energy,MWh/MtCO2,18,27777.78,130975.3,101000.0,111111.1,162410.3,0.0,500000.0,True
25
- 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
26
- BioCCS with combustion,Bio-CCS,Thermal energy other grade,Energy,Energy,MWh/MtCO2,22,347222.2,1006242.0,930611.1,1025000.0,1052440.0,0.0,4000000.0,True
27
- BioCCS with combustion,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
28
- 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.0,False
29
- BioCCS with combustion,Bio-CCS,Other chemical,Chemicals,Physical,Mt/MtCO2,20,0.00027,4.044119,0.00316,0.016,8.287794,0.00013,20.2,True
30
- BioCCS with combustion,Bio-CCS,Other water,Water,Physical,bcm/MtCO2,32,0.006475,0.312937,0.201625,0.542046,0.343155,0.00063,1.296342,True
31
- BioCCS with combustion & growing biomass,Bio-CCS,Arable land,Land,Physical,Mha/MtCO2,32,0.038,0.20054,0.0584,0.094595,0.46566,0.022803,1.9451,False
32
- BioCCS with combustion & growing biomass,Bio-CCS,Electrical energy,Energy,Energy,MWh/MtCO2,18,27777.78,130975.3,101000.0,111111.1,162410.3,0.0,500000.0,True
33
- BioCCS with combustion & growing biomass,Bio-CCS,Thermal energy other grade,Energy,Energy,MWh/MtCO2,22,347222.2,1006242.0,930611.1,1025000.0,1052440.0,0.0,4000000.0,True
34
- BioCCS with combustion & growing biomass,Bio-CCS,Geologic storage,Storage,Physical,Mt/MtCO2,1,1.0,1.0,1.0,1.0,0.0,1.0,1.0,False
35
- BioCCS with combustion & growing biomass,Bio-CCS,Other chemical,Chemicals,Physical,Mt/MtCO2,20,0.00027,4.044119,0.00316,0.016,8.287794,0.00013,20.2,True
36
- BioCCS with combustion & growing biomass,Bio-CCS,Other water,Water,Physical,bcm/MtCO2,32,0.006475,0.312937,0.201625,0.542046,0.343155,0.00063,1.296342,True
37
- "Coastal revegetation (mangrove, seaweeds, etc.)",Blue carbon,Non-arable land,Land,Physical,Mha/MtCO2,11,0.043295,0.132018,0.120566,0.197449,0.117064,0.0078,0.328289,False
38
- 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.0,False
39
- Durable biobased products,Durable biobased products,Electrical energy,Energy,Energy,MWh/MtCO2,2,4000000.0,4000000.0,4000000.0,4000000.0,0.0,4000000.0,4000000.0,True
40
- Durable biobased products,Durable biobased products,Non-arable land,Land,Physical,Mha/MtCO2,2,6.0,6.0,6.0,6.0,0.0,6.0,6.0,True
41
- Durable biobased products,Durable biobased products,Other biomass,Biomass,Physical,Mt/MtCO2,6,1.267857,2.607143,3.571429,3.892857,1.842663,0.0,4.0,False
42
- 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
43
- Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,10,333333.3,2032081.0,784125.0,2750000.0,2378833.0,252556.0,6222222.0,False
44
- Electrochemical DACCS,Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,14,0.000272,0.002662,0.000324,0.000881,0.005657,0.000223,0.016,True
45
- Electrochemical 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
46
- 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
47
- Electrochemical OAE,Ocean alkalinity enhancement,Electrical energy,Energy,Energy,MWh/MtCO2,8,1600000.0,2241250.0,2150000.0,2500000.0,1283594.0,830000.0,5000000.0,False
48
- 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
49
- Electrochemical OAE,Ocean alkalinity enhancement,Shoreline,Shoreline,Physical,km/MtCO2,2,0.012,0.012,0.012,0.012,0.0,0.012,0.012,False
50
- Electrochemical OAE,Ocean alkalinity enhancement,Other mineral,Minerals,Physical,Mt/MtCO2,4,2.4,2.95,2.95,3.5,0.635085,2.4,3.5,False
51
- Enhanced weathering - Basalt mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1E-06,2.5,True
52
- Enhanced weathering - Basalt mineral,Enhanced weathering,Basalt mineral,Minerals,Physical,Mt/MtCO2,25,3.33,5.773042,4.3,5.3,4.967901,1.0,19.0,False
53
- Enhanced weathering - Basalt mineral,Enhanced weathering,Electrical energy,Energy,Energy,MWh/MtCO2,8,92850.0,593431.3,388750.0,500000.0,904565.7,9250.0,2775000.0,True
54
- 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.0,True
55
- 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
56
- Enhanced weathering - Olivine mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1E-06,2.5,True
57
- Enhanced weathering - Olivine mineral,Enhanced weathering,Electrical energy,Energy,Energy,MWh/MtCO2,8,92850.0,593431.3,388750.0,500000.0,904565.7,9250.0,2775000.0,True
58
- 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.0,True
59
- 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
60
- 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
61
- Enhanced weathering - Wollastonite mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1E-06,2.5,True
62
- Enhanced weathering - Wollastonite mineral,Enhanced weathering,Electrical energy,Energy,Energy,MWh/MtCO2,8,92850.0,593431.3,388750.0,500000.0,904565.7,9250.0,2775000.0,True
63
- 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.0,True
64
- 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
65
- 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.0,False
66
- Enhanced weathering - Other mineral,Enhanced weathering,Arable land,Land,Physical,Mha/MtCO2,28,0.12875,0.548974,0.25,0.5,0.709916,1E-06,2.5,True
67
- Enhanced weathering - Other mineral,Enhanced weathering,Electrical energy,Energy,Energy,MWh/MtCO2,8,92850.0,593431.3,388750.0,500000.0,904565.7,9250.0,2775000.0,True
68
- 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.0,True
69
- 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.0,False
70
- 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
71
- 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
72
- High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,32,361111.1,423787.8,462961.1,500000.0,163178.3,77000.0,770000.0,False
73
- High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Non-arable land,Land,Physical,Mha/MtCO2,23,2.2e-08,0.000229,0.000223,0.000298,0.000299,1.5e-10,0.000991,False
74
- High-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Thermal energy high grade,Energy,Energy,MWh/MtCO2,38,1472222.0,1954835.0,1750000.0,2314815.0,643937.6,1125000.0,3800000.0,False
75
- High-temp DACCS (solid-sorbent/liquid-solvent),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
76
- 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,5E-09,0.05,True
77
- Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Electrical energy,Energy,Energy,MWh/MtCO2,44,218000.0,895320.2,462961.1,690000.0,1255695.0,130000.0,4611111.0,False
78
- 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,1E-11,0.000991,False
79
- Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Thermal energy low grade,Energy,Energy,MWh/MtCO2,30,1432500.0,1678215.0,1500000.0,2062500.0,516794.3,638888.9,2722222.0,False
80
- Low-temp DACCS (solid-sorbent/liquid-solvent),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
81
- Low-temp DACCS (solid-sorbent/liquid-solvent),Direct air carbon capture and storage (DACCS),Other water,Water,Physical,bcm/MtCO2,14,7.5e-10,0.003187,0.001008,0.004925,0.005238,-0.002,0.015,True
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 anaerobic digestion/fermentation & growing biomass",
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
- "Non-arable land"
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 inputs_aggregated.csv to update all
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/inputs_aggregated.csv"
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 five groups of constraints:
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 to the matrix A_ub.
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 = np.zeros(num_lp_vars)
165
  for j, m in enumerate(active_methods):
166
- row[j] = method_costs.get(m, {}).get(r, 0.0)
167
- if np.any(row[:num_methods] != 0):
168
- A_rows.append(row)
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 = np.zeros(num_lp_vars)
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 = np.zeros(num_lp_vars)
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(batch_name, 0.0))
203
- constraint_labels.append(("custom_batch", batch_name))
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, so when both could satisfy the same demand the
370
- solver picks an arbitrary split. This second pass fixes that: it pins
371
- every active method's output y_m to its pass-1 value (so total_removed and
372
- method_usage never change) and minimises Σ (c_n - c_s) · z_{m,n} across all
373
- (method, batch) pooling pairs — i.e. it prefers fully using whichever
374
- resource (standard or custom) has the lower coefficient (more MtCO2
375
- removed per unit of resource) before drawing on the less efficient one.
 
 
 
 
 
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
- if not custom_usage_pairs:
 
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 (two types, packed into one vector x):
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
- (indices num_methods..num_lp_vars-1).
 
 
 
 
 
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
- if r in covered_by_batch:
76
- continue
77
- if resource_caps.get(r, 0.0) <= 0:
78
- missing.append(r)
79
- elif not enabled_methods.get(r, {}).get(m, True):
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
- icon = "✅" if available else "❌"
167
- row = (icon, display_r, "" if available else source)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
- if (m, r) in secondary_resources:
 
 
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 = "#55C95B" if not hint else "#FF5F4D"
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 = "#55C95B" if not hint else "#FF5F4D"
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 - Optimisation results displayed.",
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("❌ Optimisation failed")
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"❌ Optimisation removed 0 {CO2_UNIT}. Relax constraints or increase resources.", icon=":material/warning:")
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>Run the optimisation tool, explore results and save scenarios for comparison or export.</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 optimisation</p>""",
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 Optimisation", icon=":material/play_arrow:", key="btn-run-optimization", type="primary")
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 optimisation first to see results here.</p>'
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">Optimisation Summary</p>', unsafe_allow_html=True)
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