CarlyneB commited on
Commit
3c60617
Β·
1 Parent(s): 9253c67

[FIX] Geologic storage fixed coeff + template improvements + UI fixes

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