CarlyneB commited on
Commit
a58aedd
·
1 Parent(s): 357d192

[FEAT] objective 2: More flexible resources input

Browse files
app.py CHANGED
@@ -35,7 +35,33 @@ with st.sidebar:
35
  ],
36
  icons=["house", "upload", "gear", "sliders", "graph-up-arrow", "card-checklist", "search"],
37
  default_index=0,
38
- orientation="vertical"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  )
40
 
41
  # Initialize data to default values
@@ -44,7 +70,7 @@ if "constraints" not in st.session_state:
44
  st.session_state.constraints = {}
45
  for m in method_order:
46
  if m not in st.session_state.constraints:
47
- st.session_state.constraints[m] = {"active": True, "cap_type": "percent", "cap_value": 1.0}
48
 
49
  # Initialize cost_sliders
50
  cost_sliders = load_slider_data()
@@ -60,7 +86,7 @@ if "costs" not in st.session_state or st.session_state.costs == {}:
60
  if selected == "Welcome":
61
  tab0_welcome.render_tab()
62
  elif selected == "Resource inputs":
63
- tab1_inputs.render_tab()
64
  elif selected == "Method constraints":
65
  tab2_constraints.render_tab()
66
  elif selected == "Resource coefficients":
 
35
  ],
36
  icons=["house", "upload", "gear", "sliders", "graph-up-arrow", "card-checklist", "search"],
37
  default_index=0,
38
+ orientation="vertical",
39
+ key="crra-menu",
40
+ styles={
41
+ "container":{
42
+ "border-radius":"0px",
43
+ },
44
+ "menu-title":{
45
+ "font-family": "Segoe UI, sans-serif",
46
+ "font-weight":"bold",
47
+ "color": "#ff5f4d",
48
+ "text-transform": "uppercase",
49
+ "font-style": "italic",
50
+ },
51
+ "nav-link": {
52
+ "font-family": "Segoe UI, sans-serif",
53
+ "margin": "4px 0",
54
+ "--hover-color": "#F5F5F5",
55
+ "text-transform": "uppercase",
56
+ "font-weight":"600",
57
+ "font-style": "italic",
58
+ },
59
+ "nav-link-selected": {
60
+ "background-color": "#eae9d8",
61
+ "color": "#298231",
62
+ "font-weight": "900",
63
+ },
64
+ }
65
  )
66
 
67
  # Initialize data to default values
 
70
  st.session_state.constraints = {}
71
  for m in method_order:
72
  if m not in st.session_state.constraints:
73
+ st.session_state.constraints[m] = {"active": True, "cap_type": "percent", "cap_value": 100}
74
 
75
  # Initialize cost_sliders
76
  cost_sliders = load_slider_data()
 
86
  if selected == "Welcome":
87
  tab0_welcome.render_tab()
88
  elif selected == "Resource inputs":
89
+ tab1_inputs.render_tab(cost_sliders)
90
  elif selected == "Method constraints":
91
  tab2_constraints.render_tab()
92
  elif selected == "Resource coefficients":
assets/styles.css CHANGED
@@ -1,13 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  .cdr-table, .cdr-table-res, .resources-summary {
2
  width: 100%;
3
  border-collapse: separate;
4
  border-spacing: 0;
5
  border-radius: 10px;
6
  overflow: hidden;
 
7
  }
8
 
9
  .cdr-table thead th, .cdr-table-res thead th, .resources-summary thead th{
10
- background-color: #3d86e9;
11
  color: white;
12
  font-weight: bold;
13
  padding: 8px 12px;
@@ -16,7 +31,7 @@
16
 
17
  .cdr-table tbody th, .cdr-table-res tbody th, .resources-summary tbody th{
18
  text-align: center;
19
- color: #949393;
20
  font-weight: 100;
21
  }
22
 
@@ -35,24 +50,40 @@
35
  /* tab2 */
36
  [class*="st-key-toggle_type_"] p {
37
  font-weight: 600;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  }
39
 
 
 
40
  h1.custom-title {
41
- font-family: "Segoe UI", sans-serif;
42
  font-size: 60px;
43
  font-style: italic;
44
  line-height: 0.95em;
45
  }
46
 
47
  .crra-hl {
48
- background: linear-gradient(90deg, #D9E210 0%, #92EF9B 99%);
49
  -webkit-text-fill-color: transparent;
50
  background-clip: text;
51
  padding-right: 6px;
52
  }
53
 
54
  div.custom-corps {
55
- font-family: "Segoe UI", sans-serif;
56
  font-size: 18px;
57
  }
58
 
@@ -61,17 +92,15 @@ div.custom-corps {
61
  }
62
 
63
  .text_with_border {
64
- border-left: 3px solid #FF5F4D;
65
  padding-left: 16px;
66
- font-family: "Segoe UI", sans-serif;
67
- /* font-style: italic; */
68
- font-weight: 600;
69
  font-size: 18px !important;
70
  }
71
 
72
  .objective_title {
73
- color: #FF5F4D;
74
- font-family: "Segoe UI", sans-serif;
75
  font-size: 14px;
76
  font-weight: 700;
77
  text-transform: uppercase;
@@ -80,25 +109,27 @@ div.custom-corps {
80
  }
81
 
82
  .st-key-warning_resources [data-testid="stAlert"] {
83
- background-color: #EAE9D8!important;
84
  clip-path: polygon(1.5% 0, 100% 0, 98.5% 100%, 0 100%);
85
  }
86
 
87
  .st-key-warning_resources p{
88
- font-family: 'Segoe UI', sans-serif;
89
  font-style: italic;
90
  font-size: 16px;
91
- font-weight: 600;
92
- color: #665d5b;
 
 
93
  }
94
 
95
  .st-key-warning_resources span{
96
  font-style: italic;
97
- color: #665d5b;
98
  }
99
 
100
  .crra-heading {
101
- font-family: 'Segoe UI', sans-serif !important;
102
  font-weight: 700;
103
  font-style: italic;
104
  font-size: clamp(32px, 2.5vw, 55px) !important;
@@ -112,41 +143,275 @@ div.custom-corps {
112
  }
113
 
114
  .crra-sub-heading {
115
- font-family: 'Segoe UI', sans-serif !important;
116
  font-weight: 700;
117
  font-style: italic;
118
  font-size: clamp(25px, 2vw, 32px) !important;
119
  line-height: 1.25em;
120
- color: #665D5B !important;
121
  margin-bottom: 15px;
122
  margin-top: 30px !important;
123
  }
124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  /* buttons */
127
- .st-key-btn-reset button {
128
  border-width: 0px !important;
129
  border-radius: 0px;
130
- color: #FFFFFF;
131
- background: #FF5F4D;
132
- display: inline-flex;
133
- align-items: center;
134
  height: clamp(52px, 3.125vw, 74px);
135
- padding: 0 clamp(20px, 2.625vw, 42px);
136
  clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
137
  transition: background 0.25s ease;
138
  }
139
 
140
- .st-key-btn-reset button p{
141
  font-weight: 700 !important;
142
- font-family: "Segoe UI", sans-serif;
143
- font-size: clamp(14px, 0.900vw, 18px);
144
  letter-spacing: 0.094444em;
145
  text-decoration: none;
146
  text-transform: uppercase;
 
147
  }
148
 
149
- .st-key-btn-reset button:hover{
150
  background: #E5503F;
151
  color: #FFFFFF;
152
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --off-white: #eae9d8;
3
+ --red: #ff5f4d;
4
+ --green-dark: #298231;
5
+ --green: #55c95b;
6
+ --green-light: #92ef9b;
7
+ --yellow: #d9e210;
8
+ --white: #ffffff;
9
+ --brown-grey: #665d5b;
10
+ --grey: #a0a0a0;
11
+ --black: #000000;
12
+ --font: 'Segoe UI', sans-serif;
13
+ }
14
+
15
  .cdr-table, .cdr-table-res, .resources-summary {
16
  width: 100%;
17
  border-collapse: separate;
18
  border-spacing: 0;
19
  border-radius: 10px;
20
  overflow: hidden;
21
+ font-family: var(--font);
22
  }
23
 
24
  .cdr-table thead th, .cdr-table-res thead th, .resources-summary thead th{
25
+ background-color: var(--green);
26
  color: white;
27
  font-weight: bold;
28
  padding: 8px 12px;
 
31
 
32
  .cdr-table tbody th, .cdr-table-res tbody th, .resources-summary tbody th{
33
  text-align: center;
34
+ color: var(--grey);
35
  font-weight: 100;
36
  }
37
 
 
50
  /* tab2 */
51
  [class*="st-key-toggle_type_"] p {
52
  font-weight: 600;
53
+ font-family: var(--font);
54
+ text-transform: uppercase;
55
+ letter-spacing: 1.25px;
56
+ color: var(--brown-grey);
57
+ }
58
+
59
+ [class*="cap_type_"] p {
60
+ font-family: var(--font);
61
+ }
62
+
63
+ [class*="slider_"] p, [class*="manual_"] p{
64
+ font-family: var(--font);
65
+ font-style: italic;
66
+ color: var(--grey);
67
  }
68
 
69
+ /* tab2 end */
70
+
71
  h1.custom-title {
72
+ font-family: var(--font);
73
  font-size: 60px;
74
  font-style: italic;
75
  line-height: 0.95em;
76
  }
77
 
78
  .crra-hl {
79
+ background: linear-gradient(90deg, var(--yellow) 0%, var(--green-light) 99%);
80
  -webkit-text-fill-color: transparent;
81
  background-clip: text;
82
  padding-right: 6px;
83
  }
84
 
85
  div.custom-corps {
86
+ font-family: var(--font);
87
  font-size: 18px;
88
  }
89
 
 
92
  }
93
 
94
  .text_with_border {
95
+ border-left: 3px solid var(--red);
96
  padding-left: 16px;
97
+ font-family: var(--font);
 
 
98
  font-size: 18px !important;
99
  }
100
 
101
  .objective_title {
102
+ color: var(--red);
103
+ font-family: var(--font);
104
  font-size: 14px;
105
  font-weight: 700;
106
  text-transform: uppercase;
 
109
  }
110
 
111
  .st-key-warning_resources [data-testid="stAlert"] {
112
+ /* background-color: var(--off-white)!important; */
113
  clip-path: polygon(1.5% 0, 100% 0, 98.5% 100%, 0 100%);
114
  }
115
 
116
  .st-key-warning_resources p{
117
+ font-family: var(--font);
118
  font-style: italic;
119
  font-size: 16px;
120
+ line-height: 1.5;
121
+ letter-spacing: 0.01em;
122
+ /* font-weight: 600; */
123
+ /* color: #var(--brown-grey); */
124
  }
125
 
126
  .st-key-warning_resources span{
127
  font-style: italic;
128
+ /* color: #var(--brown-grey); */
129
  }
130
 
131
  .crra-heading {
132
+ font-family: var(--font) !important;
133
  font-weight: 700;
134
  font-style: italic;
135
  font-size: clamp(32px, 2.5vw, 55px) !important;
 
143
  }
144
 
145
  .crra-sub-heading {
146
+ font-family: var(--font) !important;
147
  font-weight: 700;
148
  font-style: italic;
149
  font-size: clamp(25px, 2vw, 32px) !important;
150
  line-height: 1.25em;
151
+ color: var(--brown-grey) !important;
152
  margin-bottom: 15px;
153
  margin-top: 30px !important;
154
  }
155
 
156
+ .dialog-title {
157
+ font-family: var(--font) !important;
158
+ font-weight: 600;
159
+ font-size: clamp(20px, 1vw, 30px) !important;
160
+ line-height: 1.25em;
161
+ margin-bottom: 15px;
162
+ margin-top: 10px !important;
163
+ }
164
+
165
+ .crra-sub-sub-heading {
166
+ font-family: var(--font) !important;
167
+ font-weight: 600;
168
+ font-style: italic;
169
+ font-size: clamp(20px, 1.5vw, 30px) !important;
170
+ line-height: 1.25em;
171
+ color: var(--grey) !important;
172
+ margin-bottom: 15px;
173
+ margin-top: 20px !important;
174
+ }
175
+
176
+ .crra-group-name{
177
+ font-family: var(--font) !important;
178
+ font-weight: 600;
179
+ font-style: italic;
180
+ font-size: clamp(20px, 1.5vw, 30px) !important;
181
+ line-height: 1.25em;
182
+ }
183
 
184
  /* buttons */
185
+ .st-key-btn-reset button, [class*="btn-reset-"] button {
186
  border-width: 0px !important;
187
  border-radius: 0px;
188
+ color: var(--white);
189
+ background: var(--red);
 
 
190
  height: clamp(52px, 3.125vw, 74px);
191
+ padding: 0 clamp(20px, 2vw, 42px);
192
  clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
193
  transition: background 0.25s ease;
194
  }
195
 
196
+ .st-key-btn-reset button p, [class*="btn-reset-"] button p{
197
  font-weight: 700 !important;
198
+ font-family: var(--font);
199
+ font-size: clamp(13px, 0.800vw, 18px);
200
  letter-spacing: 0.094444em;
201
  text-decoration: none;
202
  text-transform: uppercase;
203
+ line-height: 1.2;
204
  }
205
 
206
+ .st-key-btn-reset button:hover, [class*="btn-reset-"] button:hover{
207
  background: #E5503F;
208
  color: #FFFFFF;
209
+ }
210
+
211
+ .st-key-btn-load button {
212
+ border-radius: 0px;
213
+ color: var(--off-white);
214
+ background: var(--green-dark);
215
+ height: clamp(52px, 3.125vw, 74px);
216
+ padding: 0 clamp(20px, 2vw, 42px);
217
+ clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
218
+ transition: background 0.25s ease;
219
+ }
220
+ .st-key-btn-load button p{
221
+ font-weight: 700 !important;
222
+ font-family: var(--font);
223
+ font-size: clamp(13px, 0.800vw, 18px);
224
+ letter-spacing: 0.094444em;
225
+ text-decoration: none;
226
+ text-transform: uppercase;
227
+ line-height: 1.2;
228
+ }
229
+
230
+ .st-key-btn-load button:hover{
231
+ background: #01740f;
232
+ color: var(--off-white);
233
+ }
234
+
235
+ [class*="btn-new-"] button {
236
+ border-radius: 0px;
237
+ border: 0px;
238
+ color: var(--red);
239
+ background: var(--off-white);
240
+ height: clamp(52px, 3.125vw, 74px);
241
+ padding: 0 clamp(20px, 2vw, 42px);
242
+ clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
243
+ transition: background 0.25s ease;
244
+ }
245
+
246
+ [class*="btn-new-"] button p{
247
+ font-weight: 700 !important;
248
+ font-family: var(--font);
249
+ font-size: clamp(13px, 0.800vw, 18px);
250
+ letter-spacing: 0.094444em;
251
+ text-decoration: none;
252
+ text-transform: uppercase;
253
+ line-height: 1.2;
254
+ }
255
+
256
+ [class*="btn-new-"] button:hover{
257
+ background: #D8D7C4;
258
+ color: var(--red);
259
+ }
260
+
261
+ .stSidebar {
262
+ background: var(--off-white) !important;
263
+ }
264
+
265
+ section[data-testid="stExpander"] summary {
266
+ font-family: var(--font);
267
+ font-weight: 600;
268
+ text-transform: uppercase;
269
+ }
270
+
271
+ [class*="crra-select"] [data-baseweb="select"], .st-key-crra-select p{
272
+ font-family: var(--font);
273
+ font-size: 16px;
274
+ }
275
+
276
+ [class*="crra-select"] p{
277
+ font-style: italic;
278
+ font-weight: bold;
279
+ }
280
+
281
+ /* .st-key-crra-select [data-baseweb="select"] div{
282
+ background-color: var(--off-white);
283
+ } */
284
+
285
+ /* tab1 */
286
+ [class*="_dlg_edit_existing_"] p {
287
+ font-family: var(--font);
288
+ color: var(--red);
289
+ font-weight: bold;
290
+ font-style: italic;
291
+ }
292
+
293
+ .caption {
294
+ font-family: var(--font);
295
+ color:#a0a0a0;
296
+ font-size: 0.88rem !important;
297
+ letter-spacing: 0.05rem;
298
+ }
299
+
300
+ [class*="_dlg_existing_enable_"] p, [class*="label-input-"] p {
301
+ font-family: var(--font);
302
+ }
303
+
304
+ .st-key-container-btn {
305
+ display: flex;
306
+ flex-direction: row;
307
+ gap: 1.125%;
308
+ }
309
+
310
+ [class*="cap_"] p {
311
+ font-family: var(--font);
312
+ }
313
+
314
+ /* .stDownloadButton {
315
+ place-self: center;
316
+ } */
317
+
318
+ [class*="st-key-download_button_"] button {
319
+ border-width: 0px !important;
320
+ border-radius: 0px;
321
+ color: var(--green-dark);
322
+ background: var(--off-white);
323
+ height: clamp(52px, 3.125vw, 74px);
324
+ padding: 0 clamp(20px, 2vw, 42px);
325
+ clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
326
+ transition: background 0.25s ease;
327
+ }
328
+
329
+ [class*="st-key-download_button_save_custom"] button {
330
+ float:inline-end;
331
+ }
332
+
333
+ [class*="st-key-download_button_"] button p{
334
+ font-weight: 700 !important;
335
+ font-family: var(--font);
336
+ font-size: clamp(13px, 0.800vw, 18px);
337
+ letter-spacing: 0.094444em;
338
+ text-decoration: none;
339
+ text-transform: uppercase;
340
+ line-height: 1.2;
341
+ }
342
+
343
+ [class*="st-key-download_button_"] button:hover{
344
+ background: #D8D7C4;
345
+ color: #FFFFFF;
346
+ }
347
+
348
+ [class*="resource_uploader_"],
349
+ [class*="inputs_loader_"],
350
+ [class*="scenario_loader_"],
351
+ .st-key-comparison_loader
352
+ {
353
+ font-family: var(--font);
354
+ }
355
+
356
+ [class*="resource_uploader_"] p,
357
+ [class*="inputs_loader_"] p,
358
+ [class*="scenario_loader_"] p,
359
+ .st-key-comparison_loader p
360
+ {
361
+ font-family: var(--font);
362
+ color: var(--grey);
363
+ font-style: italic;
364
+ }
365
+
366
+ [class*="resource_uploader_"] [data-testid="stFileUploaderDropzone"],
367
+ [class*="inputs_loader_"] [data-testid="stFileUploaderDropzone"],
368
+ [class*="scenario_loader_"] [data-testid="stFileUploaderDropzone"],
369
+ .st-key-comparison_loader [data-testid="stFileUploaderDropzone"]
370
+ {
371
+ background-color: var(--off-white);
372
+ /* clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%); */
373
+ /* border-radius: 0px; */
374
+ /* box-shadow: 0px 4px 20px 0px rgba(0, 0, 0, 0.20); */
375
+ color: var(--green-dark);
376
+ font-weight: 700 !important;
377
+ letter-spacing: 0.094444em;
378
+ text-decoration: none;
379
+ text-transform: uppercase;
380
+ /* justify-self: center; */
381
+ width: stretch;
382
+ font-size: 0.875rem;;
383
+ }
384
+
385
+ /* [class*="resource_uploader_"] [data-testid="stFileUploaderDropzone"] [data-testid="stFileUploaderDropzoneInstructions"]{
386
+ padding-inline: 3rem;
387
+ } */
388
+
389
+ [class*="resource_uploader_"] [data-testid="stFileUploaderDropzone"] [data-testid="stFileUploaderDropzoneInstructions"] span svg,
390
+ [class*="inputs_loader_"] [data-testid="stFileUploaderDropzone"] [data-testid="stFileUploaderDropzoneInstructions"] span svg,
391
+ [class*="scenario_loader_"] [data-testid="stFileUploaderDropzone"] [data-testid="stFileUploaderDropzoneInstructions"] span svg,
392
+ .st-key-comparison_loader [data-testid="stFileUploaderDropzone"] [data-testid="stFileUploaderDropzoneInstructions"] span svg
393
+ {
394
+ color: var(--green-dark);
395
+ }
396
+
397
+ [class*="resource_uploader_"] [data-testid="stBaseButton-secondary"],
398
+ [class*="inputs_loader_"] [data-testid="stBaseButton-secondary"],
399
+ [class*="scenario_loader_"] [data-testid="stBaseButton-secondary"]{
400
+ color: var(--off-white);
401
+ font-weight: 700 !important;
402
+ text-transform: uppercase;
403
+ font-style: italic;
404
+ background-color: var(--green-dark);
405
+ border: none;
406
+ }
407
+
408
+ div.custom-corps-light {
409
+ font-family: var(--font);
410
+ color: var(--grey);
411
+ font-style: italic;
412
+ font-weight: 400;
413
+ font-size: 0.875rem;
414
+ /* margin-bottom: 0.25rem; */
415
+ }
416
+
417
+ /* tab1 end */
data/generate_input_template.py CHANGED
@@ -5,8 +5,10 @@ import openpyxl
5
  from openpyxl.styles import Font
6
 
7
  from pathlib import Path
 
8
  sys.path.insert(0, str(Path(__file__).parent.parent))
9
  from config.config import (resource_order, method_order, resource_unit_map, resource_groups)
 
10
 
11
  # Data
12
  with open("data/cost_sliders.json") as f:
@@ -41,18 +43,19 @@ ws2["B1"].font = Font(bold=True)
41
  ws2["C1"].font = Font(bold=True)
42
  ws2["D1"].font = Font(bold=True)
43
  for m in method_order:
44
- ws2.append([m, True, "percent", 1.0])
45
 
46
  ws3 = wb.create_sheet("coefficients")
47
  ws3.append(["method", "resource", "value", "secondary"])
48
- ws3["A1"].font = Font(bold=True)
49
- ws3["B1"].font = Font(bold=True)
50
- ws3["C1"].font = Font(bold=True)
51
- ws3["D1"].font = Font(bold=True)
52
  for m in method_order:
53
  for r, conf in cost_sliders.get(m, {}).items():
54
- is_secondary = (m, r) in secondary_resources
55
- ws3.append([m, r, conf["median"], True if is_secondary else False])
 
 
 
56
 
57
  wb.save("data/inputs_template.xlsx")
58
  print("Template generated: data/inputs_template.xlsx")
 
5
  from openpyxl.styles import Font
6
 
7
  from pathlib import Path
8
+
9
  sys.path.insert(0, str(Path(__file__).parent.parent))
10
  from config.config import (resource_order, method_order, resource_unit_map, resource_groups)
11
+ from utils.data_utils import strip_unit_suffix
12
 
13
  # Data
14
  with open("data/cost_sliders.json") as f:
 
43
  ws2["C1"].font = Font(bold=True)
44
  ws2["D1"].font = Font(bold=True)
45
  for m in method_order:
46
+ ws2.append([m, True, "percent", 100])
47
 
48
  ws3 = wb.create_sheet("coefficients")
49
  ws3.append(["method", "resource", "value", "secondary"])
50
+ for cell in ws3[1]:
51
+ cell.font = Font(bold=True)
 
 
52
  for m in method_order:
53
  for r, conf in cost_sliders.get(m, {}).items():
54
+ is_secondary = (m, strip_unit_suffix(r)) in secondary_resources
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")
data/inputs_template.xlsx CHANGED
Binary files a/data/inputs_template.xlsx and b/data/inputs_template.xlsx differ
 
optimization/optimization.py CHANGED
@@ -9,7 +9,7 @@ def run_optimization(resource_caps, method_constraints, method_costs, custom_res
9
  Args:
10
  resource_caps (dict): {resource: available_quantity}
11
  method_constraints (dict): {method: {"active": bool, "cap_type": str, "cap_value": float}}
12
- cap_type: "percent" → cap_value is a fraction of total (0 to 1)
13
  cap_type: "absolute" → cap_value is a hard upper bound in CO₂ units
14
  method_costs (dict): {method: {resource: usage_per_tCO2}}
15
  custom_resources (list | None): list of custom batch dicts: [{"name": str, "group": str, "amount": float, "methods": {method: {"min", "median", "max"}}}]
@@ -33,6 +33,19 @@ def run_optimization(resource_caps, method_constraints, method_costs, custom_res
33
  else:
34
  method_costs[method] = {r_key: coefs["median"]}
35
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  # ── Standard optimisation setup ───────────────────────────────────────────
38
 
@@ -56,7 +69,7 @@ def run_optimization(resource_caps, method_constraints, method_costs, custom_res
56
  for i, m in enumerate(methods):
57
  constraint = method_constraints.get(m, {})
58
  cap_type = constraint.get("cap_type", "percent")
59
- cap_value = constraint.get("cap_value", 1.0)
60
 
61
  # x_i <= cap_value
62
  if cap_type == "absolute":
@@ -64,10 +77,11 @@ def run_optimization(resource_caps, method_constraints, method_costs, custom_res
64
  row[i] = 1
65
  A_share.append(row)
66
  b_share.append(cap_value)
67
- # x_i <= cap_value * sum(x) → x_i - cap_value * sum(x) <= 0
68
  else:
 
69
  row = np.zeros(len(methods))
70
- row += -cap_value
71
  row[i] += 1
72
  A_share.append(row)
73
  b_share.append(0)
@@ -83,9 +97,10 @@ def run_optimization(resource_caps, method_constraints, method_costs, custom_res
83
  try:
84
  result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method="highs")
85
  if result.success:
86
- x = np.floor(result.x).astype(int)
87
- total_removed = int(x.sum())
88
- method_usage = {m: int(x[i]) for i, m in enumerate(methods) if x[i] > 0}
 
89
  return True, {"total_removed": total_removed, "method_usage": method_usage}
90
  else:
91
  return False, {"message": result.message}
 
9
  Args:
10
  resource_caps (dict): {resource: available_quantity}
11
  method_constraints (dict): {method: {"active": bool, "cap_type": str, "cap_value": float}}
12
+ cap_type: "percent" → cap_value is a fraction of total (0 to 100)
13
  cap_type: "absolute" → cap_value is a hard upper bound in CO₂ units
14
  method_costs (dict): {method: {resource: usage_per_tCO2}}
15
  custom_resources (list | None): list of custom batch dicts: [{"name": str, "group": str, "amount": float, "methods": {method: {"min", "median", "max"}}}]
 
33
  else:
34
  method_costs[method] = {r_key: coefs["median"]}
35
 
36
+ # ── Debug custom resources ────────────────────────────────────────────────
37
+ print(f"\n{'='*50}")
38
+ print(f"custom_resources received: {len(custom_resources or [])} batch(es)")
39
+ for batch in (custom_resources or []):
40
+ r_key = batch["name"]
41
+ print(f" → '{r_key}' | amount: {resource_caps.get(r_key)} | methods: {list(batch['methods'].keys())}")
42
+ for method in batch["methods"]:
43
+ coef = method_costs.get(method, {}).get(r_key)
44
+ print(f" method_costs['{method}']['{r_key}'] = {coef}")
45
+ print(f"Total resources in matrix: {len(resource_caps)}")
46
+ print(f"Total resources in matrix: {resource_caps}")
47
+ print(f"Resources: {list(resource_caps.keys())}")
48
+ print(f"{'='*50}\n")
49
 
50
  # ── Standard optimisation setup ───────────────────────────────────────────
51
 
 
69
  for i, m in enumerate(methods):
70
  constraint = method_constraints.get(m, {})
71
  cap_type = constraint.get("cap_type", "percent")
72
+ cap_value = constraint.get("cap_value", 100)
73
 
74
  # x_i <= cap_value
75
  if cap_type == "absolute":
 
77
  row[i] = 1
78
  A_share.append(row)
79
  b_share.append(cap_value)
80
+ # x_i <= cap_value/100 * sum(x) → x_i - cap_value/100 * sum(x) <= 0
81
  else:
82
+ cap_ratio = cap_value / 100
83
  row = np.zeros(len(methods))
84
+ row += -cap_ratio
85
  row[i] += 1
86
  A_share.append(row)
87
  b_share.append(0)
 
97
  try:
98
  result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method="highs")
99
  if result.success:
100
+ # x = np.floor(result.x).astype(int)
101
+ x = result.x
102
+ total_removed = x.sum()
103
+ method_usage = {m: x[i] for i, m in enumerate(methods) if x[i] > 0}
104
  return True, {"total_removed": total_removed, "method_usage": method_usage}
105
  else:
106
  return False, {"message": result.message}
tabs/tab0_welcome.py CHANGED
@@ -3,29 +3,33 @@ from config.config import CO2_UNIT
3
  from utils.ui_helpers import show_bar
4
 
5
  def render_tab():
6
- st.markdown('<h1 class="crra-heading">Welcome!</h1>', unsafe_allow_html=True)
7
- show_bar()
8
  st.markdown(f"""
9
  <div class='custom-corps'>
10
- This tool helps you optimize your carbon direct removal portfolio by adjusting resource usage and constraints.
11
-
12
- **Required:**
13
- 1. **Resource inputs**: provide the total available quantities of resources for 1 year.
14
-
15
- **Optional:**
16
-
17
- 2. **Method constraints**: select which carbon removal methods are active and set their **maximum deployment cap** (as % of total potential or absolute value in {CO2_UNIT}/yr).
18
- 3. **Resource efficiency coefficients**: adjust the resource usage for each method.
19
 
20
- **Outputs:**
21
-
22
- 4. **Portfolio generation**: run the optimization to find the best portfolio: how much CO₂ is removed, what methods are used, and in what proportions.
23
-
24
- **Explore:**
25
-
26
- 5. **Sensitivity analysis**: analyze how changes in method caps or resource availability affect the portfolio.
27
- 6. **Scenario comparison**: compare different scenarios based on the optimization results.
28
 
29
- Let's get started! Use the side tabs to navigate through the sections.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  </div>
31
  """, unsafe_allow_html=True)
 
3
  from utils.ui_helpers import show_bar
4
 
5
  def render_tab():
 
 
6
  st.markdown(f"""
7
  <div class='custom-corps'>
 
 
 
 
 
 
 
 
 
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>
31
+ </ol>
32
+
33
+ Let’s get started! Use the side tabs to navigate through the sections.
34
  </div>
35
  """, unsafe_allow_html=True)
tabs/tab1_inputs.py CHANGED
@@ -3,117 +3,258 @@ import streamlit as st
3
 
4
  from config.config import (resource_groups, resource_order, resource_tooltips,
5
  resource_unit_map, method_groups, method_order, CO2_UNIT)
6
- from utils.data_utils import render_resource_input
7
- from utils.ui_helpers import format_resource_summary, show_bar
8
  from io import BytesIO
9
 
10
  # ─── Custom resource helpers ──────────────────────────────────────────────────
11
 
12
  def standard_resource_names():
13
- """Flat set of all standard resource names (without unit suffixes)."""
14
  return {r for rlist in resource_groups.values() for r in rlist}
15
 
16
 
17
  def init_custom_resources():
18
  if "custom_resources" not in st.session_state:
19
  st.session_state.custom_resources = []
 
 
 
 
 
 
 
 
 
 
 
20
 
21
 
22
  # ─── Dialog: add / edit a custom resource batch ───────────────────────────────
23
 
24
- @st.dialog("Add a custom resource", width="large")
25
- def dialog_add_custom_resource(edit_index: int | None = None):
26
- """
27
- Full-screen dialog to create or edit a custom resource batch.
28
- edit_index: index in st.session_state.custom_resources to edit (None = new).
29
- """
 
 
 
 
30
 
31
  standard_names = standard_resource_names()
32
  existing_names = {
33
  b["name"] for i, b in enumerate(st.session_state.custom_resources)
34
  if i != edit_index
35
  }
36
-
37
- # Pre-fill values when editing
38
  existing = st.session_state.custom_resources[edit_index] if edit_index is not None else {}
39
 
40
- st.markdown("### 1 — Name & group")
41
- col_name, col_group = st.columns(2)
 
 
 
 
 
42
 
 
43
  with col_name:
44
  batch_name = st.text_input(
45
  "Resource name *",
46
  value=existing.get("name", ""),
47
  placeholder="e.g. Degraded forest lands – North",
48
  help="It can't match the name of an existing resource.",
 
49
  )
50
-
51
  with col_group:
52
- groups = list(resource_groups.keys())
53
- default_group_idx = groups.index(existing["group"]) if existing.get("group") in groups else 0
54
  batch_group = st.selectbox(
55
  "Resource group *",
56
- options=groups,
57
  index=default_group_idx,
58
  help="Determines the physical unit of this resource.",
 
59
  )
60
 
61
- unit = resource_unit_map.get(batch_group, "?")
62
- st.caption(f"Unit for this group: **{unit}** (per MtCO₂ for coefficients)")
 
 
 
 
63
 
64
- st.markdown("### 2 — Available quantity")
65
  batch_amount = st.number_input(
66
  f"Available amount ({unit})",
67
  min_value=0.0,
68
  format="%.2f",
69
  value=float(existing.get("amount", 0.0)),
70
  help=f"Total quantity available for 1 year, in {unit}.",
 
71
  )
72
 
73
- st.markdown("### 3 — Allowed CDR methods & coefficients")
74
- st.caption(
75
- f"Select which methods can use this resource and enter their resource consumption coefficients in **{unit}/{CO2_UNIT}**."
76
- )
77
 
78
- existing_methods = existing.get("methods", {})
79
  method_configs = {}
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  for group_name, methods in method_groups.items():
 
 
 
 
 
 
82
  with st.expander(f"**{group_name}**", expanded=False):
83
- for m in methods:
84
- prev = existing_methods.get(m)
85
- enabled_default = prev is not None
86
- enabled = st.checkbox(f"**{m}**", value=enabled_default, key=f"_dlg_chk_{m}")
87
-
88
  if enabled:
89
  col_min, col_med, col_max = st.columns(3)
90
  with col_min:
91
  v_min = st.number_input(
92
- "Min", min_value=0.0, format="%.4f",
93
- value=float(prev["min"] if prev else 0.0),
94
- key=f"_dlg_min_{m}",
 
95
  )
96
  with col_med:
97
  v_med = st.number_input(
98
- "Median ★", min_value=0.0, format="%.4f",
99
- value=float(prev["median"] if prev else 0.0),
100
- key=f"_dlg_med_{m}",
 
101
  )
102
  with col_max:
103
  v_max = st.number_input(
104
- "Max", min_value=0.0, format="%.4f",
105
- value=float(prev["max"] if prev else 0.0),
106
- key=f"_dlg_max_{m}",
 
107
  )
108
- method_configs[m] = {"min": v_min, "median": v_med, "max": v_max}
109
-
110
- # ── Save / cancel ──────────────────────────────────────────────────────────
 
 
111
  error_placeholder = st.empty()
112
-
113
  col_save, col_cancel = st.columns([2, 2])
114
 
115
  with col_cancel:
116
- if st.button("Cancel", use_container_width=True):
117
  st.rerun()
118
 
119
  def _validate():
@@ -128,7 +269,6 @@ def dialog_add_custom_resource(edit_index: int | None = None):
128
  errors.append("Available amount must be greater than 0.")
129
  if not method_configs:
130
  errors.append("Select at least one CDR method.")
131
- #consistency check min / median / max
132
  for m, c in method_configs.items():
133
  if c["min"] > c["max"]:
134
  errors.append(f"**{m}**: min ({c['min']}) cannot be greater than max ({c['max']}).")
@@ -137,7 +277,7 @@ def dialog_add_custom_resource(edit_index: int | None = None):
137
  return errors
138
 
139
  with col_save:
140
- if st.button("Add to session", type="primary", use_container_width=True, icon=":material/check:"):
141
  errors = _validate()
142
  if errors:
143
  with error_placeholder.container():
@@ -147,185 +287,234 @@ def dialog_add_custom_resource(edit_index: int | None = None):
147
  batch = {
148
  "name": batch_name.strip(),
149
  "group": batch_group,
150
- "amount": batch_amount,
151
  "methods": method_configs,
152
  }
153
  if edit_index is not None:
154
  st.session_state.custom_resources[edit_index] = batch
155
  else:
156
  st.session_state.custom_resources.append(batch)
157
- # Clear any cached optimization results since inputs changed
158
  st.session_state.pop("latest_result", None)
159
  st.rerun()
160
- return method_configs
161
 
162
- # ─── Load custom resource from a saved Excel ────────────────────────────────────
163
 
164
- def to_excel_bytes(df: pd.DataFrame) -> bytes:
165
- buffer = BytesIO()
166
- with pd.ExcelWriter(buffer, engine="openpyxl") as writer:
167
- df.to_excel(writer, index=False, sheet_name="custom_resources")
168
- return buffer.getvalue()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
- @st.dialog("Load a saved resource batch", width="large")
 
 
171
  def dialog_load_custom_batch():
172
- st.caption("Import a Excel file that you have previously exported from the ‘Custom resource’ section")
173
- uploaded = st.file_uploader("Upload Excel file", type=["xlsx"])
 
174
  if uploaded is None:
175
  return
176
 
177
  try:
178
- df = pd.read_excel(uploaded, sheet_name=0)
179
- required = {"name", "group", "amount", "method", "min", "median", "max"}
180
- if not required.issubset(df.columns):
181
- st.error(f"Missing columns: {required - set(df.columns)}")
182
- return
183
-
184
- standard_names = standard_resource_names()
185
- existing_names = {b["name"] for b in st.session_state.custom_resources}
186
-
187
- batches_in_file = df["name"].unique()
188
- for bname in batches_in_file:
189
- df_b = df[df["name"] == bname]
190
- group = df_b["group"].iloc[0]
191
- amount = float(df_b["amount"].iloc[0])
192
- methods = {
193
- row["method"]: {"min": row["min"], "median": row["median"], "max": row["max"]}
194
- for _, row in df_b.iterrows()
195
- }
196
- batch = {"name": bname, "group": group, "amount": amount, "methods": methods}
197
 
198
- st.markdown(f"**Batch:** {bname} | Group: {group} | Amount: {amount} | Methods: {len(methods)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
199
 
200
- if bname in standard_names:
201
- st.error(f"'{bname}' matches a standard resource. Skipped.")
 
 
202
  continue
203
- if bname in existing_names:
204
- st.warning(f"'{bname}' already exists in session. It will be overwritten.")
205
- idx = next(i for i, b in enumerate(st.session_state.custom_resources) if b["name"] == bname)
206
  st.session_state.custom_resources[idx] = batch
207
  else:
208
  st.session_state.custom_resources.append(batch)
209
-
210
- st.success(f"Loaded {len(batches_in_file)} batch(es) from file.")
211
  st.session_state.pop("latest_result", None)
 
212
  st.rerun()
213
 
214
- except Exception as e:
215
- st.error(f"Error loading file: {e}")
216
-
217
 
218
- # ─── Custom resources management panel ────────────────────────────────────────
219
 
220
- def panel_custom_resources():
221
- """Panel displayed at the top of the inputs tab to manage custom batches."""
222
  init_custom_resources()
223
 
224
- st.markdown("### ➕ Custom resource")
225
- st.caption(
226
- "Add sub-batches for any resource group and manually assign them to specific CDR methods. "
227
- "Standard resources above use the default taxonomy; custom batches give you full control."
228
- )
229
 
230
- col_add, col_load = st.columns([1, 1])
231
- with col_add:
232
- if st.button("New resource", use_container_width=True, type="primary", icon=":material/add:"):
233
- dialog_add_custom_resource()
234
- with col_load:
235
- if st.button("Load from Excel", use_container_width=True, icon=":material/arrow_upload_progress:"):
236
  dialog_load_custom_batch()
237
 
 
 
 
 
 
 
238
  batches = st.session_state.custom_resources
239
  if not batches:
240
- st.info("No custom batches defined yet. Click **+ New resource** to get started.")
241
  return
242
 
243
- st.markdown(f"**{len(batches)} custom batch(es) active**")
244
-
245
  for i, batch in enumerate(batches):
246
- unit = resource_unit_map.get(batch["group"], "?")
 
 
 
 
247
  n_methods = len(batch["methods"])
 
248
 
249
  with st.expander(
250
- f"🆕 {batch['group']} ({unit}) - CDR methods: **{n_methods}**",
251
  expanded=False,
252
  ):
253
- st.markdown(f"**{batch['name']}**")
 
 
 
 
 
254
 
255
  col_amount, _, _, _ = st.columns(4)
256
  with col_amount:
257
  new_amount = st.number_input(
258
  "Available amount",
259
- label_visibility= "collapsed",
260
  min_value=0.0,
261
- value=float(batch["amount"]),
262
  format="%.2f",
263
- key=f"custom_amount_{i}",
264
  )
265
-
266
- if new_amount != batch["amount"]:
267
- batch["amount"] = new_amount
268
  st.session_state.pop("latest_result", None)
269
 
270
- # Summary table
 
 
 
271
  rows = [
272
  {
273
  "Method": m,
274
- f"Min ({unit}/MtCO₂)": f"{c['min']:.2f}",
275
- f"Median ({unit}/MtCO₂)": f"{c['median']:.2f}",
276
- f"Max ({unit}/MtCO₂)": f"{c['max']:.2f}",
277
  }
278
  for m, c in batch["methods"].items()
279
  ]
280
-
281
  st.dataframe(
282
  pd.DataFrame(rows),
283
  use_container_width=True,
284
  hide_index=True,
 
 
 
 
 
 
 
285
  )
286
 
287
  col_edit, col_del = st.columns([1, 1])
288
  with col_edit:
289
  if st.button("Edit", key=f"edit_batch_{i}", use_container_width=True, icon=":material/edit:"):
290
- method_configs = dialog_add_custom_resource(edit_index=i)
291
- print(method_configs)
292
  with col_del:
293
- if st.button("Delete", key=f"del_batch_{i}", use_container_width=True, type="primary", icon=":material/delete:"):
 
294
  st.session_state.custom_resources.pop(i)
295
  st.session_state.pop("latest_result", None)
296
  st.rerun()
297
 
298
- # ── Export all batches as a single Excel file ─────────────────────────────────────
299
- _,_,col_save_custom = st.columns(3)
300
  with col_save_custom:
301
- all_rows = []
302
-
303
- for batch in batches:
304
- for m, coefs in batch["methods"].items():
305
- all_rows.append({
306
- "name": batch["name"],
307
- "group": batch["group"],
308
- "amount": batch["amount"],
309
- "method": m,
310
- "min": coefs["min"],
311
- "median": coefs["median"],
312
- "max": coefs["max"],
313
- })
314
-
315
- df_export = pd.DataFrame(all_rows)
316
-
317
  st.download_button(
318
- "Save all custom resources (Excel)",
319
- data=to_excel_bytes(df_export),
320
- file_name="custom_resources.xlsx",
321
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
322
- icon=":material/save:",
 
 
323
  )
324
 
 
325
  # ─── Main render ──────────────────────────────────────────────────────────────
326
 
327
- def render_tab():
328
- # Safe session init
329
  if "resource_caps" not in st.session_state:
330
  st.session_state.resource_caps = {}
331
  for r in resource_order:
@@ -335,31 +524,21 @@ def render_tab():
335
 
336
  st.markdown('<h1 class="crra-heading">Resource inputs</h1>', unsafe_allow_html=True)
337
  show_bar()
338
-
339
- st.markdown(
340
- """
341
- <p class="objective_title">OBJECTIVE</p>
342
- """,
343
- unsafe_allow_html=True,
344
- )
345
 
346
- st.markdown("""
347
- <p class="text_with_border"> This section lets you define the total available quantities of each resource for the optimization model.</p>
348
- """,
 
349
  unsafe_allow_html=True,
350
  )
351
 
352
  with st.container(key="warning_resources"):
353
  st.warning(
354
  "Resources with zero availability will block portfolio feasibility if required by active methods.",
355
- icon=":material/warning:"
356
  )
357
 
358
- # Reset button
359
- st.markdown("""
360
- <p class="crra-sub-heading">Controls</p>""",
361
- unsafe_allow_html=True,
362
- )
363
 
364
  if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
365
  for r in st.session_state.resource_caps:
@@ -368,37 +547,36 @@ def render_tab():
368
  st.success("All resource values reset to 0.")
369
  st.rerun()
370
 
371
- # Import / download template
372
- st.markdown("##### Import resource values")
373
  col_template, col_upload = st.columns([1, 1])
374
  with col_template:
375
- st.markdown("Use the template below to fill in your resource availability values and import them directly into the tool.")
 
 
376
  with open("data/resource_template.xlsx", "rb") as f:
377
  st.download_button(
378
  label="Download Excel Template",
379
  data=f,
380
  file_name="resource_template.xlsx",
381
- use_container_width=True,
382
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
383
  icon=":material/download:",
 
384
  )
385
  with col_upload:
386
  uploaded_file = st.file_uploader(
387
  "Upload your Excel file", type="xlsx",
388
- key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}"
389
  )
390
-
391
  if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
392
  try:
393
  df = pd.read_excel(uploaded_file)
394
  required_cols = {"Resource", "Available Amount"}
395
  if not required_cols.issubset(df.columns):
396
  raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
397
-
398
  for r in st.session_state.resource_caps:
399
  st.session_state.resource_caps[r] = 0.0
400
  st.session_state[f"cap_{r}"] = 0.0
401
-
402
  for _, row in df.iterrows():
403
  r = row["Resource"]
404
  val = row["Available Amount"]
@@ -410,24 +588,19 @@ def render_tab():
410
  val = 0.0
411
  st.session_state.resource_caps[r] = val
412
  st.session_state[f"cap_{r}"] = val
413
-
414
  st.session_state["resource_last_loaded_file"] = uploaded_file.name
415
  st.success("Resource values imported successfully.", icon="✅")
416
- st.session_state.pop("excel_loaded_file", None)
417
- st.session_state.pop("scenario_name_suggestion", None)
418
- st.session_state.pop("scenario_loaded_file", None)
419
- st.session_state.pop("input_file_name_suggestion", None)
420
- st.session_state.pop("latest_result", None)
421
  st.rerun()
422
-
423
  except Exception as e:
424
  st.error(f"Error importing resource values: {e}")
425
-
426
  elif st.session_state.get("resource_last_loaded_file"):
427
  st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="✅")
428
 
429
- # ── Standard resource inputs ───────────────────────────────────────────────
430
- st.markdown("### Set available resources for one year")
431
  for group, rlist in resource_groups.items():
432
  unit = resource_unit_map.get(group, "")
433
  with st.expander(f"**{group}** ({unit})", expanded=True):
@@ -438,54 +611,54 @@ def render_tab():
438
  with cols[i % 4]:
439
  render_resource_input(name_clean, r, tooltip=tooltip)
440
 
441
- # ── Custom resource batches panel ──────────────────────────────────────────
442
- panel_custom_resources()
443
 
444
- # ── Summary table ──────────────────────────────────────────────────────────
445
- st.markdown("### Summary of available resources")
 
 
 
 
 
 
446
 
447
- # Standard resources
448
  df_caps = pd.DataFrame.from_dict(
449
  {r: st.session_state.resource_caps[r] for r in resource_order
450
  if st.session_state.resource_caps.get(r, 0.0) > 0},
451
- orient="index", columns=["Available Amount"]
452
  ).reindex(resource_order).dropna()
453
-
454
- # Custom resource rows
 
 
 
 
 
 
 
 
 
455
  custom_rows = []
456
  for batch in st.session_state.get("custom_resources", []):
457
- unit = resource_unit_map.get(batch["group"], "?")
 
458
  custom_rows.append({
459
- "Resource": f"{batch['name']}",
460
- "Available Amount": batch["amount"],
461
  })
462
 
463
  if not df_caps.empty or custom_rows:
464
-
465
- # --- Standard resources ---
466
  dfs = []
467
-
468
  if not df_caps.empty:
469
  df_std = df_caps.copy()
470
- df_std = df_std.rename(columns={"Available Amount": "Available Amount"})
471
  df_std["Type"] = "Standard"
472
  dfs.append(df_std)
473
-
474
- # --- Custom resources ---
475
  if custom_rows:
476
  df_custom = pd.DataFrame(custom_rows).set_index("Resource")
477
- df_custom["Type"] = "Custom"
478
  dfs.append(df_custom)
479
-
480
- # --- Merge ---
481
- df_all = pd.concat(dfs)
482
-
483
- # Optionnel : ordre des colonnes
484
- cols = ["Type", "Available Amount"]
485
- df_all = df_all[cols]
486
-
487
- # Styling
488
  st.markdown(format_resource_summary(df_all), unsafe_allow_html=True)
489
-
490
  else:
491
  st.error("No resources defined yet. Set availability above to populate the summary table.")
 
3
 
4
  from config.config import (resource_groups, resource_order, resource_tooltips,
5
  resource_unit_map, method_groups, method_order, CO2_UNIT)
6
+ from utils.data_utils import render_resource_input, strip_unit_suffix
7
+ from utils.ui_helpers import format_resource_summary, format_table_summary, show_bar
8
  from io import BytesIO
9
 
10
  # ─── Custom resource helpers ──────────────────────────────────────────────────
11
 
12
  def standard_resource_names():
 
13
  return {r for rlist in resource_groups.values() for r in rlist}
14
 
15
 
16
  def init_custom_resources():
17
  if "custom_resources" not in st.session_state:
18
  st.session_state.custom_resources = []
19
+ for batch in st.session_state.custom_resources:
20
+ try:
21
+ batch["amount"] = float(batch["amount"])
22
+ except (ValueError, TypeError):
23
+ batch["amount"] = 0.0
24
+ for m, coefs in batch.get("methods", {}).items():
25
+ for key in ("min", "median", "max"):
26
+ try:
27
+ coefs[key] = float(coefs[key])
28
+ except (ValueError, TypeError):
29
+ coefs[key] = 0.0
30
 
31
 
32
  # ─── Dialog: add / edit a custom resource batch ───────────────────────────────
33
 
34
+ @st.dialog("Add a new resource", width="large")
35
+ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None):
36
+
37
+ if edit_index is not None and not st.session_state.get(f"_dlg_initialized_{edit_index}"):
38
+ for key in list(st.session_state.keys()):
39
+ if key.startswith(("_dlg_existing_enable_", "_dlg_edit_existing_",
40
+ "_dlg_min_existing_", "_dlg_med_existing_", "_dlg_max_existing_",
41
+ "_dlg_new_chk_", "_dlg_new_min_", "_dlg_new_med_", "_dlg_new_max_")):
42
+ del st.session_state[key]
43
+ st.session_state[f"_dlg_initialized_{edit_index}"] = True
44
 
45
  standard_names = standard_resource_names()
46
  existing_names = {
47
  b["name"] for i, b in enumerate(st.session_state.custom_resources)
48
  if i != edit_index
49
  }
 
 
50
  existing = st.session_state.custom_resources[edit_index] if edit_index is not None else {}
51
 
52
+ resource_to_group = {
53
+ r: group
54
+ for group, resources in resource_groups.items()
55
+ for r in resources
56
+ }
57
+
58
+ st.markdown('<p class="dialog-title">1 — Name & group</p>', unsafe_allow_html=True)
59
 
60
+ col_name, col_group = st.columns(2)
61
  with col_name:
62
  batch_name = st.text_input(
63
  "Resource name *",
64
  value=existing.get("name", ""),
65
  placeholder="e.g. Degraded forest lands – North",
66
  help="It can't match the name of an existing resource.",
67
+ key="label-input-name"
68
  )
 
69
  with col_group:
70
+ default_group_idx = resource_order.index(existing["group"]) if existing.get("group") in resource_order else 0
 
71
  batch_group = st.selectbox(
72
  "Resource group *",
73
+ options=resource_order,
74
  index=default_group_idx,
75
  help="Determines the physical unit of this resource.",
76
+ key="label-input-group"
77
  )
78
 
79
+ group = resource_to_group.get(batch_group)
80
+ unit = resource_unit_map.get(group, "?")
81
+
82
+ st.markdown(f'<p class="caption">Unit for this group: <strong>{unit}</strong> (per MtCO₂ for coefficients)</p>', unsafe_allow_html=True)
83
+
84
+ st.markdown('<p class="dialog-title">2 — Available quantity</p>', unsafe_allow_html=True)
85
 
 
86
  batch_amount = st.number_input(
87
  f"Available amount ({unit})",
88
  min_value=0.0,
89
  format="%.2f",
90
  value=float(existing.get("amount", 0.0)),
91
  help=f"Total quantity available for 1 year, in {unit}.",
92
+ key="label-input-amount"
93
  )
94
 
95
+ st.markdown('<p class="dialog-title">3 — Existing CDR methods for this resource</p>', unsafe_allow_html=True)
 
 
 
96
 
 
97
  method_configs = {}
98
 
99
+ # Methods linked to this resource grouped by CDR group
100
+ related_methods = {}
101
+ for cdr_method, resource in cost_sliders.items():
102
+ for name, coeff in resource.items():
103
+ if batch_group == strip_unit_suffix(name):
104
+ related_methods[cdr_method] = {
105
+ "min": coeff["min"],
106
+ "median": coeff["median"],
107
+ "max": coeff["max"],
108
+ }
109
+
110
+ # group_name -> methods
111
+ related_by_group = {}
112
+
113
+ for m, coeff in related_methods.items():
114
+ method_group = next(
115
+ (g for g, methods in method_groups.items() if m in methods),
116
+ "Other"
117
+ )
118
+ related_by_group.setdefault(method_group, {})[m] = coeff
119
+
120
+ for method_group, methods in related_by_group.items():
121
+ with st.expander(f"**{method_group}**", expanded=False):
122
+ for m, coeff in methods.items():
123
+
124
+ already_selected = m in existing.get("methods", {})
125
+
126
+ if f"_dlg_existing_enable_{m}" not in st.session_state:
127
+ st.session_state[f"_dlg_existing_enable_{m}"] = already_selected
128
+
129
+ enabled = st.checkbox(
130
+ f"{m}",
131
+ key=f"_dlg_existing_enable_{m}",
132
+ )
133
+
134
+ if not enabled:
135
+ continue
136
+
137
+ if f"_dlg_edit_existing_{m}" not in st.session_state:
138
+ saved = existing.get("methods", {}).get(m, {})
139
+ default = coeff
140
+ coeffs_differ = any(
141
+ abs(float(saved.get(k, default[k])) - float(default[k])) > 1e-9
142
+ for k in ("min", "median", "max")
143
+ ) if saved else False
144
+ st.session_state[f"_dlg_edit_existing_{m}"] = coeffs_differ
145
+
146
+ edit = st.checkbox(
147
+ "Edit coefficients (will overwrite default values)",
148
+ key=f"_dlg_edit_existing_{m}",
149
+ )
150
+
151
+ if edit:
152
+ saved = existing.get("methods", {}).get(m, coeff)
153
+ st.warning(
154
+ "Editing this method will overwrite the default coefficient values.",
155
+ icon="⚠️"
156
+ )
157
+ col_min, col_med, col_max = st.columns(3)
158
+ with col_min:
159
+ v_min = st.number_input(
160
+ "Min",
161
+ min_value=0.0,
162
+ value=float(saved.get("min", coeff["min"])),
163
+ format="%.4f",
164
+ key=f"_dlg_min_existing_{m}",
165
+ )
166
+
167
+ with col_med:
168
+ v_med = st.number_input(
169
+ "Median ★",
170
+ min_value=0.0,
171
+ value=float(saved.get("median", coeff["median"])),
172
+ format="%.4f",
173
+ key=f"_dlg_med_existing_{m}",
174
+ )
175
+
176
+ with col_max:
177
+ v_max = st.number_input(
178
+ "Max",
179
+ min_value=0.0,
180
+ value=float(saved.get("max", coeff["max"])),
181
+ format="%.4f",
182
+ key=f"_dlg_max_existing_{m}",
183
+ )
184
+
185
+ method_configs[m] = {
186
+ "min": v_min,
187
+ "median": v_med,
188
+ "max": v_max,
189
+ }
190
+
191
+ else:
192
+ st.dataframe(
193
+ pd.DataFrame([{
194
+ "Min": coeff["min"],
195
+ "Median": coeff["median"],
196
+ "Max": coeff["max"],
197
+ }]),
198
+ hide_index=True,
199
+ use_container_width=True,
200
+ )
201
+
202
+ method_configs[m] = coeff.copy()
203
+ # -------------------------------------------------
204
+ # Part 4 : new/custom methods only
205
+ # -------------------------------------------------
206
+
207
+ st.markdown('<p class="dialog-title">4 — Add additional CDR methods</p>', unsafe_allow_html=True)
208
+ st.markdown(f'<p class="caption">Select additional methods and enter their coefficients in {unit}/{CO2_UNIT}.</p>', unsafe_allow_html=True)
209
+
210
+ already_used = set(related_methods.keys())
211
+
212
  for group_name, methods in method_groups.items():
213
+ available_methods = [
214
+ m for m in methods
215
+ if m not in already_used
216
+ ]
217
+ if not available_methods:
218
+ continue
219
  with st.expander(f"**{group_name}**", expanded=False):
220
+ for m in available_methods:
221
+ enabled = st.checkbox(
222
+ f"**{m}**",
223
+ key=f"_dlg_new_chk_{m}",
224
+ )
225
  if enabled:
226
  col_min, col_med, col_max = st.columns(3)
227
  with col_min:
228
  v_min = st.number_input(
229
+ "Min",
230
+ min_value=0.0,
231
+ format="%.4f",
232
+ key=f"_dlg_new_min_{m}",
233
  )
234
  with col_med:
235
  v_med = st.number_input(
236
+ "Median ★",
237
+ min_value=0.0,
238
+ format="%.4f",
239
+ key=f"_dlg_new_med_{m}",
240
  )
241
  with col_max:
242
  v_max = st.number_input(
243
+ "Max",
244
+ min_value=0.0,
245
+ format="%.4f",
246
+ key=f"_dlg_new_max_{m}",
247
  )
248
+ method_configs[m] = {
249
+ "min": v_min,
250
+ "median": v_med,
251
+ "max": v_max,
252
+ }
253
  error_placeholder = st.empty()
 
254
  col_save, col_cancel = st.columns([2, 2])
255
 
256
  with col_cancel:
257
+ if st.button("Cancel", use_container_width=True, key="btn-reset-cancel"):
258
  st.rerun()
259
 
260
  def _validate():
 
269
  errors.append("Available amount must be greater than 0.")
270
  if not method_configs:
271
  errors.append("Select at least one CDR method.")
 
272
  for m, c in method_configs.items():
273
  if c["min"] > c["max"]:
274
  errors.append(f"**{m}**: min ({c['min']}) cannot be greater than max ({c['max']}).")
 
277
  return errors
278
 
279
  with col_save:
280
+ if st.button("Add to session", type="tertiary", use_container_width=True, icon=":material/check:", key="btn-new-add"):
281
  errors = _validate()
282
  if errors:
283
  with error_placeholder.container():
 
287
  batch = {
288
  "name": batch_name.strip(),
289
  "group": batch_group,
290
+ "amount": float(batch_amount), # always store as float
291
  "methods": method_configs,
292
  }
293
  if edit_index is not None:
294
  st.session_state.custom_resources[edit_index] = batch
295
  else:
296
  st.session_state.custom_resources.append(batch)
 
297
  st.session_state.pop("latest_result", None)
298
  st.rerun()
 
299
 
 
300
 
301
+ # ─── Excel helpers ────────────────────────────────────────────────────────────
302
+
303
+ def to_excel_bytes(batches: list) -> bytes:
304
+ """Serialize all custom resource batches to an Excel file (one row per batch×method)."""
305
+ rows = []
306
+ for batch in batches:
307
+ for m, coefs in batch["methods"].items():
308
+ rows.append({
309
+ "name": batch["name"],
310
+ "group": batch["group"],
311
+ "amount": float(batch["amount"]),
312
+ "unit": batch["unit"],
313
+ "method": m,
314
+ "min": float(coefs["min"]),
315
+ "median": float(coefs["median"]),
316
+ "max": float(coefs["max"]),
317
+ })
318
+ buf = BytesIO()
319
+ with pd.ExcelWriter(buf, engine="openpyxl") as writer:
320
+ pd.DataFrame(rows).to_excel(writer, index=False, sheet_name="custom_resources")
321
+ return buf.getvalue()
322
+
323
+
324
+ def _parse_custom_excel(file) -> list:
325
+ df = pd.read_excel(file, sheet_name=0)
326
+ required = {"name", "group", "amount", "method", "min", "median", "max"}
327
+ if not required.issubset(df.columns):
328
+ raise ValueError(f"Missing columns: {required - set(df.columns)}")
329
+ batches = []
330
+ for bname in df["name"].unique():
331
+ df_b = df[df["name"] == bname]
332
+ methods = {
333
+ row["method"]: {
334
+ "min": float(row["min"]),
335
+ "median": float(row["median"]),
336
+ "max": float(row["max"]),
337
+ }
338
+ for _, row in df_b.iterrows()
339
+ }
340
+ batches.append({
341
+ "name": str(bname),
342
+ "group": str(df_b["group"].iloc[0]),
343
+ "amount": float(df_b["amount"].iloc[0]),
344
+ "methods": methods,
345
+ })
346
+ return batches
347
+
348
 
349
+ # ─── Load dialog ──────────────────────────────────────────────────────────────
350
+
351
+ @st.dialog("Load custom resource batches", width="large")
352
  def dialog_load_custom_batch():
353
+ st.markdown('<p class="caption">Import an Excel file previously exported from the <strong>Custom resource</strong> section.</p>', unsafe_allow_html=True)
354
+
355
+ uploaded = st.file_uploader("Upload Excel file", type=["xlsx"], key="inputs_loader_custom")
356
  if uploaded is None:
357
  return
358
 
359
  try:
360
+ new_batches = _parse_custom_excel(uploaded)
361
+ except Exception as e:
362
+ st.error(f"Error reading file: {e}")
363
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
 
365
+ standard_names = standard_resource_names()
366
+ existing_names = {b["name"] for b in st.session_state.custom_resources}
367
+
368
+ previews = []
369
+ for batch in new_batches:
370
+ if batch["name"] in standard_names:
371
+ status = "❌ Conflicts with standard resource — will be skipped"
372
+ elif batch["name"] in existing_names:
373
+ status = "⚠️ Already exists — will be overwritten"
374
+ else:
375
+ status = "✅ New"
376
+ previews.append({
377
+ "Resource name": batch["name"],
378
+ "Group": batch["group"],
379
+ "Amount": f"{batch["amount"]} {resource_unit_map.get(batch["group"], "")}",
380
+ "Methods": len(batch["methods"]),
381
+ "Status": status,
382
+ })
383
+ st.dataframe(pd.DataFrame(previews), use_container_width=True, hide_index=True)
384
 
385
+ if st.button("↩️ Import all batches", type="primary", use_container_width=True):
386
+ imported = 0
387
+ for batch in new_batches:
388
+ if batch["name"] in standard_names:
389
  continue
390
+ if batch["name"] in existing_names:
391
+ idx = next(i for i, b in enumerate(st.session_state.custom_resources)
392
+ if b["name"] == batch["name"])
393
  st.session_state.custom_resources[idx] = batch
394
  else:
395
  st.session_state.custom_resources.append(batch)
396
+ imported += 1
 
397
  st.session_state.pop("latest_result", None)
398
+ st.success(f"Imported {imported} batch(es).")
399
  st.rerun()
400
 
 
 
 
401
 
402
+ # ─── Custom resources panel ───────────────────────────────────────────────────
403
 
404
+ def panel_custom_resources(cost_sliders):
 
405
  init_custom_resources()
406
 
407
+ st.markdown('<p class="crra-sub-heading">➕ Custom resource</p>', unsafe_allow_html=True)
408
+ st.markdown('<p class="caption"> Add sub-batches for any resource group and manually assign them to specific CDR methods.</p>', unsafe_allow_html=True)
 
 
 
409
 
410
+ col = st.container(key="container-btn")
411
+
412
+ with col:
413
+ if st.button("New resource", type="tertiary", icon=":material/add:", key="btn-new-resource"):
414
+ dialog_add_custom_resource(cost_sliders)
415
+ if st.button("Load from Excel", type="tertiary", icon=":material/arrow_upload_progress:", key="btn-load"):
416
  dialog_load_custom_batch()
417
 
418
+ resource_to_group = {
419
+ r: group
420
+ for group, resources in resource_groups.items()
421
+ for r in resources
422
+ }
423
+
424
  batches = st.session_state.custom_resources
425
  if not batches:
426
+ st.info("No new resources have been defined yet. Click **+ New resource** to get started.")
427
  return
428
 
429
+ st.markdown(f"**{len(batches)} new resource(s)**")
 
430
  for i, batch in enumerate(batches):
431
+
432
+ group = resource_to_group.get(batch["group"])
433
+ unit = resource_unit_map.get(group, "?")
434
+
435
+ batch["unit"] = unit
436
  n_methods = len(batch["methods"])
437
+ amount_val = float(batch["amount"])
438
 
439
  with st.expander(
440
+ f"🆕 **{batch['name']}** ({batch['group']}) — CDR methods: **{n_methods}**",
441
  expanded=False,
442
  ):
443
+ st.markdown(f"**{batch['name']}** ({unit})")
444
+
445
+ key_amount = f"custom_amount_{i}"
446
+ # Initialise widget state only if not already set (avoids overwrite on rerun)
447
+ if key_amount not in st.session_state:
448
+ st.session_state[key_amount] = amount_val
449
 
450
  col_amount, _, _, _ = st.columns(4)
451
  with col_amount:
452
  new_amount = st.number_input(
453
  "Available amount",
454
+ label_visibility="collapsed",
455
  min_value=0.0,
 
456
  format="%.2f",
457
+ key=key_amount,
458
  )
459
+ # Write back as float — never as a formatted string
460
+ if new_amount != amount_val:
461
+ st.session_state.custom_resources[i]["amount"] = float(new_amount)
462
  st.session_state.pop("latest_result", None)
463
 
464
+ # Coefficients table
465
+ col_min = f"Min ({unit}/MtCO₂)"
466
+ col_med = f"Median ({unit}/MtCO₂)"
467
+ col_max = f"Max ({unit}/MtCO₂)"
468
  rows = [
469
  {
470
  "Method": m,
471
+ col_min: float(c["min"]),
472
+ col_med: float(c["median"]),
473
+ col_max: float(c["max"]),
474
  }
475
  for m, c in batch["methods"].items()
476
  ]
 
477
  st.dataframe(
478
  pd.DataFrame(rows),
479
  use_container_width=True,
480
  hide_index=True,
481
+ key=f"batch_editor_{i}",
482
+ column_config={
483
+ "Method": st.column_config.TextColumn(disabled=True),
484
+ col_min: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
485
+ col_med: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
486
+ col_max: st.column_config.NumberColumn(format="%.4f", min_value=0.0),
487
+ },
488
  )
489
 
490
  col_edit, col_del = st.columns([1, 1])
491
  with col_edit:
492
  if st.button("Edit", key=f"edit_batch_{i}", use_container_width=True, icon=":material/edit:"):
493
+ dialog_add_custom_resource(cost_sliders, edit_index=i)
 
494
  with col_del:
495
+ if st.button("Delete", key=f"del_batch_{i}", use_container_width=True,
496
+ type="primary", icon=":material/delete:"):
497
  st.session_state.custom_resources.pop(i)
498
  st.session_state.pop("latest_result", None)
499
  st.rerun()
500
 
501
+ # Export all batches as a single Excel file
502
+ _, col_save_custom = st.columns(2)
503
  with col_save_custom:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
504
  st.download_button(
505
+ "Download new resources",
506
+ data=to_excel_bytes(batches),
507
+ file_name="new_resources.xlsx",
508
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
509
+ icon=":material/download:",
510
+ # use_container_width=True,
511
+ key="download_button_save_custom"
512
  )
513
 
514
+
515
  # ─── Main render ──────────────────────────────────────────────────────────────
516
 
517
+ def render_tab(cost_sliders):
 
518
  if "resource_caps" not in st.session_state:
519
  st.session_state.resource_caps = {}
520
  for r in resource_order:
 
524
 
525
  st.markdown('<h1 class="crra-heading">Resource inputs</h1>', unsafe_allow_html=True)
526
  show_bar()
 
 
 
 
 
 
 
527
 
528
+ st.markdown('<p class="objective_title">OBJECTIVE</p>', unsafe_allow_html=True)
529
+ st.markdown(
530
+ '<p class="text_with_border">This section lets you define the <strong>total available '
531
+ 'quantities of each resource</strong> for the optimization model.</p>',
532
  unsafe_allow_html=True,
533
  )
534
 
535
  with st.container(key="warning_resources"):
536
  st.warning(
537
  "Resources with zero availability will block portfolio feasibility if required by active methods.",
538
+ icon=":material/warning:",
539
  )
540
 
541
+ st.markdown('<p class="crra-sub-heading">Controls</p>', unsafe_allow_html=True)
 
 
 
 
542
 
543
  if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
544
  for r in st.session_state.resource_caps:
 
547
  st.success("All resource values reset to 0.")
548
  st.rerun()
549
 
550
+ st.markdown('<p class="crra-sub-sub-heading">Import resource values</p>', unsafe_allow_html=True)
 
551
  col_template, col_upload = st.columns([1, 1])
552
  with col_template:
553
+ st.markdown("<div class='caption'><p>Use the template below to fill in your resource "
554
+ "availability values and import them directly into the tool.</p></div>",
555
+ unsafe_allow_html=True)
556
  with open("data/resource_template.xlsx", "rb") as f:
557
  st.download_button(
558
  label="Download Excel Template",
559
  data=f,
560
  file_name="resource_template.xlsx",
561
+ # use_container_width=True,
562
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
563
  icon=":material/download:",
564
+ key="download_button_resource_template"
565
  )
566
  with col_upload:
567
  uploaded_file = st.file_uploader(
568
  "Upload your Excel file", type="xlsx",
569
+ key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}",
570
  )
 
571
  if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
572
  try:
573
  df = pd.read_excel(uploaded_file)
574
  required_cols = {"Resource", "Available Amount"}
575
  if not required_cols.issubset(df.columns):
576
  raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
 
577
  for r in st.session_state.resource_caps:
578
  st.session_state.resource_caps[r] = 0.0
579
  st.session_state[f"cap_{r}"] = 0.0
 
580
  for _, row in df.iterrows():
581
  r = row["Resource"]
582
  val = row["Available Amount"]
 
588
  val = 0.0
589
  st.session_state.resource_caps[r] = val
590
  st.session_state[f"cap_{r}"] = val
 
591
  st.session_state["resource_last_loaded_file"] = uploaded_file.name
592
  st.success("Resource values imported successfully.", icon="✅")
593
+ for key in ["excel_loaded_file", "scenario_name_suggestion",
594
+ "scenario_loaded_file", "input_file_name_suggestion", "latest_result"]:
595
+ st.session_state.pop(key, None)
 
 
596
  st.rerun()
 
597
  except Exception as e:
598
  st.error(f"Error importing resource values: {e}")
 
599
  elif st.session_state.get("resource_last_loaded_file"):
600
  st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="✅")
601
 
602
+ # Standard resource inputs
603
+ st.markdown('<p class="crra-sub-heading">Set available resources for one year</p>', unsafe_allow_html=True)
604
  for group, rlist in resource_groups.items():
605
  unit = resource_unit_map.get(group, "")
606
  with st.expander(f"**{group}** ({unit})", expanded=True):
 
611
  with cols[i % 4]:
612
  render_resource_input(name_clean, r, tooltip=tooltip)
613
 
614
+ # Custom resource batches panel
615
+ panel_custom_resources(cost_sliders)
616
 
617
+ # Summary table
618
+ st.markdown('<p class="crra-sub-heading">Summary of available resources</p>', unsafe_allow_html=True)
619
+
620
+ resource_to_unit = {
621
+ r: resource_unit_map[group]
622
+ for group, resources in resource_groups.items()
623
+ for r in resources
624
+ }
625
 
 
626
  df_caps = pd.DataFrame.from_dict(
627
  {r: st.session_state.resource_caps[r] for r in resource_order
628
  if st.session_state.resource_caps.get(r, 0.0) > 0},
629
+ orient="index", columns=["Available Amount"],
630
  ).reindex(resource_order).dropna()
631
+ df_caps["Available Amount"] = [
632
+ f"{value:,.2f} {resource_to_unit.get(resource, '?')}"
633
+ for resource, value in df_caps["Available Amount"].items()
634
+ ]
635
+
636
+ resource_to_group = {
637
+ r: group
638
+ for group, resources in resource_groups.items()
639
+ for r in resources
640
+ }
641
+
642
  custom_rows = []
643
  for batch in st.session_state.get("custom_resources", []):
644
+ group = resource_to_group.get(batch["group"])
645
+ unit = resource_unit_map.get(group, "?")
646
  custom_rows.append({
647
+ "Resource": batch["name"],
648
+ "Available Amount": f"{float(batch['amount']):,.2f} {unit}",
649
  })
650
 
651
  if not df_caps.empty or custom_rows:
 
 
652
  dfs = []
 
653
  if not df_caps.empty:
654
  df_std = df_caps.copy()
 
655
  df_std["Type"] = "Standard"
656
  dfs.append(df_std)
 
 
657
  if custom_rows:
658
  df_custom = pd.DataFrame(custom_rows).set_index("Resource")
659
+ df_custom["Type"] = "New"
660
  dfs.append(df_custom)
661
+ df_all = pd.concat(dfs)[["Available Amount", "Type"]]
 
 
 
 
 
 
 
 
662
  st.markdown(format_resource_summary(df_all), unsafe_allow_html=True)
 
663
  else:
664
  st.error("No resources defined yet. Set availability above to populate the summary table.")
tabs/tab2_constraints.py CHANGED
@@ -10,77 +10,91 @@ from utils.ui_helpers import format_table_summary, show_bar
10
  # ─── Helpers ─────────────────────────────────────────────────────────────────
11
 
12
  def default_constraint():
13
- return {"active": True, "cap_type": "percent", "cap_value": 1.0}
14
 
15
 
16
  def render_method(m, cols, i):
17
  with cols[i % 3]:
18
  tooltip = method_tooltips.get(m, None)
 
 
 
 
 
 
 
19
  constraint = st.session_state.constraints[m]
20
 
21
- if f"toggle_type_{m}" not in st.session_state:
22
- st.session_state[f"toggle_type_{m}"] = constraint.get("active", True)
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- is_active = st.toggle(m, key=f"toggle_type_{m}", help=tooltip)
 
 
 
 
 
25
 
26
  if not is_active:
27
- st.session_state.constraints[m] = {
28
- "active": False,
29
- "cap_type": constraint.get("cap_type", "percent"),
30
- "cap_value": constraint.get("cap_value", 1.0),
31
- }
32
  return
33
 
34
- cap_type_index = 0 if constraint.get("cap_type", "percent") == "percent" else 1
 
 
35
  cap_type = st.radio(
36
  "Cap value type",
37
- label_visibility="collapsed",
38
- options=["percent", "absolute"],
39
- index=cap_type_index,
40
  horizontal=True,
41
- key=f"cap_type_{m}",
42
- format_func=lambda x: "% of total potential" if x == "percent" else f"Absolute ({CO2_UNIT}/yr)",
 
 
43
  )
44
 
 
 
 
 
 
45
  if cap_type == "percent":
46
- if constraint.get("cap_type") == "percent" and constraint.get("cap_value", 0) > 0:
47
- current_pct = int(constraint["cap_value"] * 100)
48
- else:
49
- current_pct = 100
50
-
51
- share_val = st.slider(
52
- "Max share (%)", min_value=0, max_value=100, step=5,
53
- value=current_pct,
54
- key=f"slider_{m}_widget",
55
  )
56
- st.session_state.constraints[m] = {
57
- "active": True,
58
- "cap_type": "percent",
59
- "cap_value": share_val / 100,
60
- }
61
- if share_val == 0:
62
- st.warning("Cap is 0% but method is active.", icon="⚠️")
63
 
64
- else:
65
- if constraint.get("cap_type") == "absolute":
66
- current_abs = float(constraint.get("cap_value") or 0.0)
67
- else:
68
- current_abs = 0.0
69
 
70
- abs_val = st.number_input(
 
 
 
 
 
 
 
71
  f"Max cap ({CO2_UNIT}/yr)",
72
  min_value=0.0,
73
  format="%.2f",
74
- value=current_abs,
75
- key=f"manual_{m}_widget",
76
  )
77
- st.session_state.constraints[m] = {
78
- "active": True,
79
- "cap_type": "absolute",
80
- "cap_value": abs_val,
81
- }
82
- if abs_val == 0:
83
- st.warning(f"Cap is 0 {CO2_UNIT}/yr but method is active.", icon="⚠️")
84
 
85
  def reset_all_constraints():
86
  for m in method_order:
@@ -99,9 +113,9 @@ def build_summary_df():
99
  if not c["active"]:
100
  continue
101
  if c["cap_type"] == "percent":
102
- rows[m] = {"Cap value": f"{int(c['cap_value'] * 100)}", "Unit": "%"}
103
  else:
104
- rows[m] = {"Cap value": f"{c['cap_value']:,.2f}", "Unit": f"{CO2_UNIT}/yr"}
105
  return pd.DataFrame.from_dict(rows, orient="index") if rows else None
106
 
107
 
@@ -111,28 +125,46 @@ def render_tab():
111
  st.markdown('<h1 class="crra-heading">Method constraints</h1>', unsafe_allow_html=True)
112
  show_bar()
113
 
114
- st.info("""
115
- **Objective:**
116
-
117
- This section lets you control which CDR methods are included in the optimization
118
- and set a **maximum deployment cap** per method.
119
-
120
- - Activate or deactivate individual methods
121
- - Cap each method by **% of its total potential** or by an **absolute value** in {CO2_UNIT}/yr
122
- """.format(CO2_UNIT=CO2_UNIT))
123
-
124
- with st.expander("⚙️ Manage methods", expanded=False):
 
 
 
 
 
 
 
 
 
 
 
 
125
  for group, method_list in method_groups.items():
126
- st.markdown(f"#### {group}")
 
 
 
127
  cols = st.columns(3)
128
  for i, m in enumerate(method_list):
129
  render_method(m, cols, i)
130
 
131
  st.divider()
132
- if st.button("🔄 Reset All Method Constraints"):
133
  reset_all_constraints()
134
 
135
- st.subheader("Summary — Active methods and caps")
 
 
 
136
  df = build_summary_df()
137
  if df is not None:
138
  st.markdown(format_table_summary(df,"cdr-table"), unsafe_allow_html=True)
 
10
  # ─── Helpers ─────────────────────────────────────────────────────────────────
11
 
12
  def default_constraint():
13
+ return {"active": True, "cap_type": "percent", "cap_value": 100}
14
 
15
 
16
  def render_method(m, cols, i):
17
  with cols[i % 3]:
18
  tooltip = method_tooltips.get(m, None)
19
+
20
+ # ─────────────────────────────────────────
21
+ # INIT SAFE
22
+ # ─────────────────────────────────────────
23
+ if m not in st.session_state.constraints:
24
+ st.session_state.constraints[m] = default_constraint()
25
+
26
  constraint = st.session_state.constraints[m]
27
 
28
+ toggle_key = f"toggle_{m}"
29
+ cap_type_key = f"cap_type_{m}"
30
+ slider_key = f"slider_{m}"
31
+ manual_key = f"manual_{m}"
32
+
33
+ # init widgets ONLY from constraint (not reverse)
34
+ st.session_state.setdefault(toggle_key, constraint["active"])
35
+ st.session_state.setdefault(cap_type_key, constraint["cap_type"])
36
+
37
+ if constraint["cap_type"] == "percent":
38
+ st.session_state.setdefault(slider_key, float(constraint["cap_value"]))
39
+ else:
40
+ st.session_state.setdefault(manual_key, float(constraint["cap_value"]))
41
 
42
+ # ─────────────────────────────────────────
43
+ # TOGGLE
44
+ # ─────────────────────────────────────────
45
+ is_active = st.toggle(m, key=toggle_key, help=tooltip)
46
+
47
+ st.session_state.constraints[m]["active"] = is_active
48
 
49
  if not is_active:
 
 
 
 
 
50
  return
51
 
52
+ # ─────────────────────────────────────────
53
+ # CAP TYPE
54
+ # ─────────────────────────────────────────
55
  cap_type = st.radio(
56
  "Cap value type",
57
+ ["percent", "absolute"],
 
 
58
  horizontal=True,
59
+ key=cap_type_key,
60
+ format_func=lambda x: "% of total potential"
61
+ if x == "percent"
62
+ else f"Absolute ({CO2_UNIT}/yr)",
63
  )
64
 
65
+ st.session_state.constraints[m]["cap_type"] = cap_type
66
+
67
+ # ─────────────────────────────────────────
68
+ # PERCENT
69
+ # ─────────────────────────────────────────
70
  if cap_type == "percent":
71
+
72
+ value = st.slider(
73
+ "Max share (%)",
74
+ 0, 100, step=5,
75
+ key=slider_key,
 
 
 
 
76
  )
 
 
 
 
 
 
 
77
 
78
+ st.session_state.constraints[m]["cap_value"] = value
 
 
 
 
79
 
80
+ if value == 0:
81
+ st.warning("Cap is 0% but method is active.", icon=":material/warning:")
82
+
83
+ # ─────────────────────────────────────────
84
+ # ABSOLUTE
85
+ # ─────────────────────────────────────────
86
+ else:
87
+ value = st.number_input(
88
  f"Max cap ({CO2_UNIT}/yr)",
89
  min_value=0.0,
90
  format="%.2f",
91
+ key=manual_key,
 
92
  )
93
+
94
+ st.session_state.constraints[m]["cap_value"] = float(value)
95
+
96
+ if value == 0:
97
+ st.warning(f"Cap is 0 {CO2_UNIT}/yr but method is active.", icon=":material/warning:")
 
 
98
 
99
  def reset_all_constraints():
100
  for m in method_order:
 
113
  if not c["active"]:
114
  continue
115
  if c["cap_type"] == "percent":
116
+ rows[m] = {"Cap value": f"{c['cap_value']}%"}
117
  else:
118
+ rows[m] = {"Cap value": f"{c['cap_value']:,.2f} {CO2_UNIT}/yr"}
119
  return pd.DataFrame.from_dict(rows, orient="index") if rows else None
120
 
121
 
 
125
  st.markdown('<h1 class="crra-heading">Method constraints</h1>', unsafe_allow_html=True)
126
  show_bar()
127
 
128
+ st.markdown(
129
+ """
130
+ <p class="objective_title">OBJECTIVE</p>
131
+ """,
132
+ unsafe_allow_html=True,
133
+ )
134
+
135
+ st.markdown(
136
+ """
137
+ <div class="text_with_border">
138
+ <p>
139
+ This section lets you control which CDR methods are included in the optimization
140
+ and set a <strong>maximum deployment cap</strong> per method.<br>
141
+ <li>Activate or deactivate individual methods</li>
142
+ <li>Cap each method by <strong>% of its total potential</strong> or by an
143
+ <strong>absolute value</strong> in {CO2_UNIT}/yr</li>
144
+ </p>
145
+ </div>
146
+ """.format(CO2_UNIT=CO2_UNIT),
147
+ unsafe_allow_html=True,
148
+ )
149
+
150
+ with st.expander("Manage methods", expanded=False, icon=":material/settings:"):
151
  for group, method_list in method_groups.items():
152
+ st.markdown(f"""
153
+ <p class="crra-group-name">{group}</p>""",
154
+ unsafe_allow_html=True,
155
+ )
156
  cols = st.columns(3)
157
  for i, m in enumerate(method_list):
158
  render_method(m, cols, i)
159
 
160
  st.divider()
161
+ if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
162
  reset_all_constraints()
163
 
164
+ st.markdown("""
165
+ <p class="crra-sub-heading">Summary of Active methods and caps</p>""",
166
+ unsafe_allow_html=True,
167
+ )
168
  df = build_summary_df()
169
  if df is not None:
170
  st.markdown(format_table_summary(df,"cdr-table"), unsafe_allow_html=True)
tabs/tab3_coefficients.py CHANGED
@@ -29,17 +29,32 @@ def render_tab(cost_sliders):
29
  st.markdown('<h1 class="crra-heading">Resource efficiency coefficients</h1>', unsafe_allow_html=True)
30
  show_bar()
31
 
32
- st.info("""
33
- **Objective:**
34
-
35
- This section lets you adjust the resource usage coefficients for each CDR method to best fit your country conditions.
36
-
37
- - Primary resources are always active and initialized with default values.
38
- - Secondary resources are initialized at zero and toggled off by default.
39
- - Toggling on a secondary resource restores its default value.
40
- """)
41
- st.warning("Blue cells in the table highlight coefficients modified from their **default median values**.")
42
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  # Normalize cost_sliders once: strip unit suffixes from resource keys
44
  # cost_sliders_norm[method][resource_name] = {min, median, max}
45
  cost_sliders_norm = {
@@ -63,9 +78,9 @@ def render_tab(cost_sliders):
63
  method_choices.append(label)
64
  method_lookup[label] = method
65
 
66
- selected_label = st.selectbox("Select a method:", method_choices)
67
 
68
- if st.button("🔄 Reset all methods"):
69
  for m, sliders in cost_sliders_norm.items():
70
  for r, conf in sliders.items():
71
  is_secondary = (m, r) in secondary_resources
@@ -86,9 +101,10 @@ def render_tab(cost_sliders):
86
  used = [r for r in rlist if r in method_sliders]
87
  if not used:
88
  continue
89
-
90
- st.markdown(f"### {group_name}")
91
-
 
92
  for r in used:
93
  conf = method_sliders[r]
94
  display_label = resource_display.get(r, r)
@@ -139,7 +155,7 @@ def render_tab(cost_sliders):
139
  st.session_state.costs[method][r] = manual_val if manual_val != slider_val else slider_val
140
 
141
  # ── Reset buttons ────────────────────────────────────────────────────
142
- if st.button("🔄 Reset selected method"):
143
  for r, conf in cost_sliders_norm.get(method, {}).items():
144
  is_secondary = (method, r) in secondary_resources
145
  st.session_state.costs[method][r] = 0.0 if is_secondary else conf["median"]
@@ -149,8 +165,10 @@ def render_tab(cost_sliders):
149
  st.rerun()
150
 
151
  # ── Summary table ─────────────────────────────────────────────────────────
152
- st.subheader("Summary table: resource usage coefficients in the model")
153
-
 
 
154
  df_default = pd.DataFrame(index=method_order, columns=resource_order)
155
  df_current = pd.DataFrame(index=method_order, columns=resource_order)
156
 
 
29
  st.markdown('<h1 class="crra-heading">Resource efficiency coefficients</h1>', unsafe_allow_html=True)
30
  show_bar()
31
 
32
+ st.markdown(
33
+ """
34
+ <p class="objective_title">OBJECTIVE</p>
35
+ """,
36
+ unsafe_allow_html=True,
37
+ )
38
+
39
+ st.markdown(
40
+ """
41
+ <div class="text_with_border">
42
+ <p>
43
+ This section lets you adjust the resource usage coefficients for each CDR method to best fit your country conditions.<br>
44
+ <li>Primary resources are always active and initialized with default values.</li>
45
+ <li>Secondary resources are initialized at zero and toggled off by default.</li>
46
+ <li>Toggling on a secondary resource restores its default value.</li>
47
+ </p>
48
+ </div>
49
+ """.format(CO2_UNIT=CO2_UNIT),
50
+ unsafe_allow_html=True,
51
+ )
52
+
53
+ with st.container(key="warning_resources"):
54
+ st.warning(
55
+ "Blue cells in the table highlight coefficients modified from their **default median values**",
56
+ icon=":material/warning:"
57
+ )
58
  # Normalize cost_sliders once: strip unit suffixes from resource keys
59
  # cost_sliders_norm[method][resource_name] = {min, median, max}
60
  cost_sliders_norm = {
 
78
  method_choices.append(label)
79
  method_lookup[label] = method
80
 
81
+ selected_label = st.selectbox("Select a method:", method_choices, key="crra-select")
82
 
83
+ if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
84
  for m, sliders in cost_sliders_norm.items():
85
  for r, conf in sliders.items():
86
  is_secondary = (m, r) in secondary_resources
 
101
  used = [r for r in rlist if r in method_sliders]
102
  if not used:
103
  continue
104
+ st.markdown(f"""
105
+ <p class="crra-group-name">{group_name}</p>""",
106
+ unsafe_allow_html=True,
107
+ )
108
  for r in used:
109
  conf = method_sliders[r]
110
  display_label = resource_display.get(r, r)
 
155
  st.session_state.costs[method][r] = manual_val if manual_val != slider_val else slider_val
156
 
157
  # ── Reset buttons ────────────────────────────────────────────────────
158
+ if st.button("Reset selected method", key="btn-reset-method", icon=":material/reset_settings:"):
159
  for r, conf in cost_sliders_norm.get(method, {}).items():
160
  is_secondary = (method, r) in secondary_resources
161
  st.session_state.costs[method][r] = 0.0 if is_secondary else conf["median"]
 
165
  st.rerun()
166
 
167
  # ── Summary table ─────────────────────────────────────────────────────────
168
+ st.markdown("""
169
+ <p class="crra-sub-heading">Summary of resource usage coefficients</p>""",
170
+ unsafe_allow_html=True,
171
+ )
172
  df_default = pd.DataFrame(index=method_order, columns=resource_order)
173
  df_current = pd.DataFrame(index=method_order, columns=resource_order)
174
 
tabs/tab4_optimization.py CHANGED
@@ -22,8 +22,8 @@ from utils.data_utils import is_valid_scenario, load_inputs_from_excel, strip_un
22
 
23
  def panel_run_from_tabs():
24
  """Left sub-panel: run optimization from manually filled tabs."""
25
- st.markdown("**Option 1 - From current tab values**")
26
- st.caption("Set resource availability and method constraints in the tabs above, then run.")
27
  st.divider()
28
 
29
 
@@ -43,9 +43,10 @@ def panel_load_from_excel():
43
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
44
  use_container_width=True,
45
  icon=":material/download:",
 
46
  )
47
 
48
- st.caption("Fill in the template with your values, then import and run.")
49
 
50
  if "excel_loaded_file" in st.session_state:
51
  st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="✅")
@@ -60,10 +61,12 @@ def panel_load_from_excel():
60
  if st.button("↩️ Load inputs into all tabs", use_container_width=True):
61
  st.session_state.setdefault("input_file_name_suggestion", default_name)
62
  try:
63
- resource_caps, constraints, costs = load_inputs_from_excel(uploaded)
 
64
  st.session_state.resource_caps = resource_caps
65
  st.session_state.constraints = constraints
66
  st.session_state.costs = costs
 
67
  clear_widget_keys()
68
  st.session_state.pop("latest_result", None)
69
  st.session_state.pop("scenario_loaded_file", None)
@@ -133,20 +136,45 @@ def parse_zip_scenario(uploaded) -> dict | None:
133
 
134
  total = sum(allocations.values())
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  return {
137
  "summary": {"total_co2_removed": total, "total_cost": 0},
138
  "allocations": allocations,
139
  "resource_caps": resource_caps,
140
  "method_constraints": constraints,
141
  "method_costs": costs,
 
142
  }
 
143
  except Exception:
144
  return None
145
 
146
  def panel_load_from_json_or_csv():
147
  """Right column: reload a full scenario from JSON or ZIP."""
148
- st.caption("Accepts `.json` or `.zip` (CSV export)")
149
-
150
  if "scenario_loaded_file" in st.session_state:
151
  st.success(
152
  f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated - Optimization results displayed.",
@@ -188,11 +216,16 @@ def panel_load_from_json_or_csv():
188
  st.session_state.resource_caps = data["resource_caps"].copy()
189
  st.session_state.constraints = data["method_constraints"].copy()
190
  st.session_state.costs = data["method_costs"].copy()
 
 
191
  clear_widget_keys()
192
  st.session_state["scenario_loaded_file"] = uploaded_json.name
193
  st.session_state["scenario_name"] = default_name
194
  st.session_state["latest_result"] = data
195
  st.session_state["excel_uploader_key"] += 1 # force reload in excel uploader
 
 
 
196
  st.rerun()
197
 
198
  def clear_widget_keys():
@@ -204,7 +237,13 @@ def clear_widget_keys():
204
  # ─── Optimization execution ──────────────────────────────────────────────────
205
 
206
  def run_and_store_result():
207
- if sum(st.session_state.resource_caps.values()) <= 0:
 
 
 
 
 
 
208
  st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
209
  return
210
 
@@ -212,6 +251,7 @@ def run_and_store_result():
212
  resource_caps=st.session_state.resource_caps,
213
  method_constraints=st.session_state.constraints,
214
  method_costs=st.session_state.costs,
 
215
  )
216
 
217
  if not success:
@@ -237,20 +277,39 @@ def run_and_store_result():
237
  "resource_caps": st.session_state.resource_caps.copy(),
238
  "method_constraints": st.session_state.constraints,
239
  "method_costs": st.session_state.costs,
 
240
  }
241
- st.success(f"✅ {total_removed:,.0f} {CO2_UNIT} removed")
 
 
242
 
243
 
244
  # ─── Results display ─────────────────────────────────────────────────────────
245
 
246
  def show_metrics(latest):
247
- col1, col2 = st.columns(2)
248
- col1.metric("Total CO₂ Removed", f"{latest['summary']['total_co2_removed']:,.0f} {CO2_UNIT}")
249
- col2.metric("Methods Used", len(latest["allocations"]))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
 
251
 
252
  def build_results_df(allocations, method_constraints=None):
253
  df = pd.DataFrame.from_dict(allocations, orient="index", columns=[f"{CO2_UNIT} removed"])
 
254
  df[f"{CO2_UNIT} removed"] = pd.to_numeric(df[f"{CO2_UNIT} removed"], errors="coerce")
255
  total = df[f"{CO2_UNIT} removed"].sum()
256
  if total > 0:
@@ -264,7 +323,7 @@ def build_results_df(allocations, method_constraints=None):
264
  if c.get("cap_type") == "absolute":
265
  caps[m] = f"{c['cap_value']:,.2f} {CO2_UNIT}/yr"
266
  else:
267
- caps[m] = f"{int(c.get('cap_value', 1.0) * 100)}% of potential"
268
  df["Cap"] = pd.Series(caps)
269
 
270
  return df.sort_values(f"{CO2_UNIT} removed", ascending=False)
@@ -284,24 +343,44 @@ def show_donut(df_result):
284
  st.plotly_chart(fig, use_container_width=True)
285
 
286
 
287
- def show_heatmap(df_result, resource_caps, method_costs):
288
- # Normalize method_costs keys for legacy scenarios
289
  normalized_costs = {
290
  m: {strip_unit_suffix(r): v for r, v in resources.items()}
291
  for m, resources in method_costs.items()
292
  }
293
 
294
- available = [r for r in resource_order if resource_caps.get(r, 0) > 0]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
295
  matrix, y_labels = [], []
296
  for method in df_result.index:
297
  row = [
298
  (normalized_costs.get(method, {}).get(r, 0.0) * df_result.at[method, f"{CO2_UNIT} removed"]
299
- / resource_caps[r] * 100) if resource_caps[r] > 0 else 0
300
  for r in available
301
  ]
302
  matrix.append(row)
303
  y_labels.append(method)
304
 
 
 
305
  fig = go.Figure(data=go.Heatmap(
306
  z=matrix, x=available, y=y_labels,
307
  colorscale="Blues",
@@ -309,22 +388,43 @@ def show_heatmap(df_result, resource_caps, method_costs):
309
  ))
310
  fig.update_layout(
311
  title="Resource Usage vs. Cap",
312
- xaxis_title="Resource", yaxis_title="Method", height=400,
 
313
  )
314
  st.plotly_chart(fig, use_container_width=True)
315
 
316
- def show_absolute_usage(allocations, method_costs):
317
  # Normalize method_costs keys: strip legacy unit suffixes
318
  normalized_costs = {
319
  m: {strip_unit_suffix(r): v for r, v in resources.items()}
320
  for m, resources in method_costs.items()
321
  }
322
 
323
- # Build resource_units locally to avoid the overwrite bug when multiple categories share the same unit
324
  _resource_units = defaultdict(list)
325
  for cat, resources in resource_groups.items():
326
  unit_key = f"{resource_unit_map[cat]}/MtCO2"
327
  _resource_units[unit_key].extend(resources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  local_resource_units = dict(_resource_units)
329
 
330
  rows = []
@@ -338,6 +438,7 @@ def show_absolute_usage(allocations, method_costs):
338
  return
339
 
340
  df = pd.DataFrame(rows)
 
341
  for unit in df["Unit"].unique():
342
  st.markdown(f"#### Resources in {unit}")
343
  df_unit = df[df["Unit"] == unit]
@@ -353,7 +454,7 @@ def show_absolute_usage(allocations, method_costs):
353
 
354
  def save_scenario(latest):
355
  st.markdown("### Save scenario")
356
- st.caption("Save the current scenario with inputs and outputs for comparison and/or export.")
357
 
358
  suggested = st.session_state.get("scenario_loaded_file", "").rsplit(".", 1)[0]
359
 
@@ -386,10 +487,10 @@ def export_scenario():
386
  return
387
 
388
  scenario = st.session_state.scenarios[name]
389
- st.markdown(f"#### Scenario: {name}")
390
- df_saved = build_results_df(scenario["allocations"], method_constraints=scenario.get("method_constraints"))
391
 
392
- st.markdown(format_table_summary(df_saved,"cdr-table-res"), unsafe_allow_html=True)
393
 
394
  safe = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
395
  col_json, col_csv = st.columns(2)
@@ -407,13 +508,38 @@ def export_scenario():
407
  zf.writestr("resource_caps.csv",
408
  pd.DataFrame.from_dict(scenario["resource_caps"], orient="index",
409
  columns=["Available Amount"]).to_csv())
410
- zf.writestr("method_constraints.csv",
411
- pd.DataFrame.from_dict(scenario["method_constraints"], orient="index", columns=["active", "cap_type (percent or absolute)", "cap_value"]).to_csv())
 
 
 
 
 
 
 
 
412
  zf.writestr("method_costs.csv",
413
  pd.DataFrame.from_dict(scenario["method_costs"], orient="index").to_csv())
414
  zf.writestr("allocations.csv",
415
  pd.DataFrame.from_dict(scenario["allocations"], orient="index",
416
  columns=[f"{CO2_UNIT} removed"]).to_csv())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
417
  with col_csv:
418
  st.download_button(
419
  "Export CSV (zip)", data=buf.getvalue(),
@@ -429,7 +555,7 @@ def export_inputs_only():
429
  for r in resources
430
  }
431
  st.markdown("### Export inputs")
432
- st.caption("Export only the resource capabilities, method constraints and coefficients — without results.")
433
 
434
  suggested = st.session_state.get("excel_loaded_file", "").rsplit(".", 1)[0]
435
  # Update when a new file is loaded
@@ -461,7 +587,7 @@ def export_inputs_only():
461
  ws2 = wb.create_sheet("constraints")
462
  ws2.append(["method", "active", "cap_type (percent or absolute)", "cap_value"])
463
  for m, c in st.session_state.constraints.items():
464
- ws2.append([m, c.get("active", True), c.get("cap_type", "percent"), c.get("cap_value", 1.0)])
465
 
466
  ws3 = wb.create_sheet("coefficients")
467
  ws3.append(["method", "resource", "value", "secondary"])
@@ -469,6 +595,21 @@ def export_inputs_only():
469
  for r, v in resources.items():
470
  ws3.append([m, r, v, (m, r) in secondary_resources])
471
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
472
  buf = io.BytesIO()
473
  wb.save(buf)
474
 
@@ -513,7 +654,8 @@ def confirm_clear_all():
513
  def reset_scenario():
514
  st.divider()
515
  st.markdown("### Reset")
516
- st.caption("Clear all current inputs, results and saved scenarios from the session.")
 
517
  if st.button("Clear all data", key="reset_scenario", use_container_width=False, type="primary", icon=":material/delete:"):
518
  confirm_clear_all()
519
 
@@ -523,8 +665,20 @@ def render_tab():
523
  st.markdown('<h1 class="crra-heading">Portfolio generation</h1>', unsafe_allow_html=True)
524
  show_bar()
525
 
526
- st.info(
527
- "Run the optimization tool, explore results and save scenarios for comparison or export."
 
 
 
 
 
 
 
 
 
 
 
 
528
  )
529
 
530
  # Session state defaults
@@ -537,17 +691,26 @@ def render_tab():
537
  st.session_state.setdefault(key, default)
538
 
539
  # ── Two-column input area ─────────────────────���────────────────────────
540
- st.markdown("### Run optimization")
 
 
 
541
  col_inputs, col_scenario = st.columns(2, gap="large")
542
 
543
  with col_inputs:
544
- st.markdown("#### From inputs")
 
 
 
545
  run_clicked = st.button("▶️ Run Optimization", use_container_width=True)
546
  panel_run_from_tabs()
547
  panel_load_from_excel()
548
 
549
  with col_scenario:
550
- st.markdown("#### From a saved scenario")
 
 
 
551
  panel_load_from_json_or_csv()
552
 
553
  # ── Execute if button pressed ──────────────────────────────────────────
@@ -567,20 +730,31 @@ def render_tab():
567
 
568
  # ── Results ────────────────────────────────────────────────────────────
569
  show_metrics(latest)
570
- df_result = build_results_df(allocations, method_constraints=latest.get("method_constraints"))
 
 
 
 
 
 
 
 
 
571
 
572
  st.subheader("Optimization Summary")
573
- # st.table(format_results_summary(df_result))
574
- st.markdown(format_table_summary(df_result,"cdr-table-res"), unsafe_allow_html=True)
 
 
575
 
576
  st.markdown("### Method allocation breakdown")
577
  show_donut(df_result)
578
 
579
  st.markdown("### Resource usage heatmap (% of cap)")
580
- show_heatmap(df_result, resource_caps, method_costs)
581
 
582
  st.markdown("### Absolute resource usage by unit")
583
- show_absolute_usage(allocations, method_costs)
584
 
585
  # ── Save & export & reset ───────────────────────────────────────────────────────
586
  save_scenario(latest)
 
22
 
23
  def panel_run_from_tabs():
24
  """Left sub-panel: run optimization from manually filled tabs."""
25
+ st.markdown("**Option 1 - From current tab values**", unsafe_allow_html=True)
26
+ st.markdown('<p class="caption">Set resource availability and method constraints in the tabs above, then run.</p>', unsafe_allow_html=True)
27
  st.divider()
28
 
29
 
 
43
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
44
  use_container_width=True,
45
  icon=":material/download:",
46
+ key="download_button_input_template"
47
  )
48
 
49
+ st.markdown('<p class="caption">Fill in the template with your values, then import and run.</p>', unsafe_allow_html=True)
50
 
51
  if "excel_loaded_file" in st.session_state:
52
  st.success(f"Loaded: **{st.session_state['excel_loaded_file']}** - All tabs updated.", icon="✅")
 
61
  if st.button("↩️ Load inputs into all tabs", use_container_width=True):
62
  st.session_state.setdefault("input_file_name_suggestion", default_name)
63
  try:
64
+ resource_caps, constraints, costs, custom_resources = load_inputs_from_excel(uploaded)
65
+ print(custom_resources)
66
  st.session_state.resource_caps = resource_caps
67
  st.session_state.constraints = constraints
68
  st.session_state.costs = costs
69
+ st.session_state.custom_resources = custom_resources
70
  clear_widget_keys()
71
  st.session_state.pop("latest_result", None)
72
  st.session_state.pop("scenario_loaded_file", None)
 
136
 
137
  total = sum(allocations.values())
138
 
139
+ # Load optional custom resource batches
140
+ custom_resources = []
141
+ if "custom_resources.csv" in names:
142
+ df_custom = pd.read_csv(io.BytesIO(zf.read("custom_resources.csv")))
143
+ required_custom = {"name", "group", "amount", "method", "min", "median", "max"}
144
+ if required_custom.issubset(df_custom.columns):
145
+ for bname in df_custom["name"].unique():
146
+ df_b = df_custom[df_custom["name"] == bname]
147
+ methods = {
148
+ row["method"]: {
149
+ "min": float(row["min"]),
150
+ "median": float(row["median"]),
151
+ "max": float(row["max"]),
152
+ }
153
+ for _, row in df_b.iterrows()
154
+ }
155
+ custom_resources.append({
156
+ "name": bname,
157
+ "group": str(df_b["group"].iloc[0]),
158
+ "amount": float(df_b["amount"].iloc[0]),
159
+ "methods": methods,
160
+ })
161
+
162
  return {
163
  "summary": {"total_co2_removed": total, "total_cost": 0},
164
  "allocations": allocations,
165
  "resource_caps": resource_caps,
166
  "method_constraints": constraints,
167
  "method_costs": costs,
168
+ "custom_resources": custom_resources,
169
  }
170
+
171
  except Exception:
172
  return None
173
 
174
  def panel_load_from_json_or_csv():
175
  """Right column: reload a full scenario from JSON or ZIP."""
176
+ st.markdown('<p class="caption">Accepts `.json` or `.zip` (CSV export)</p>', unsafe_allow_html=True)
177
+
178
  if "scenario_loaded_file" in st.session_state:
179
  st.success(
180
  f"Scenario **{st.session_state['scenario_loaded_file']}** loaded :\n\nAll tabs updated - Optimization results displayed.",
 
216
  st.session_state.resource_caps = data["resource_caps"].copy()
217
  st.session_state.constraints = data["method_constraints"].copy()
218
  st.session_state.costs = data["method_costs"].copy()
219
+ # Restore custom resource batches if present in the scenario
220
+ st.session_state.custom_resources = data.get("custom_resources", [])
221
  clear_widget_keys()
222
  st.session_state["scenario_loaded_file"] = uploaded_json.name
223
  st.session_state["scenario_name"] = default_name
224
  st.session_state["latest_result"] = data
225
  st.session_state["excel_uploader_key"] += 1 # force reload in excel uploader
226
+ if st.session_state.custom_resources:
227
+ n = len(st.session_state.custom_resources)
228
+ st.info(f"ℹ️ {n} new resource(s) restored from scenario.")
229
  st.rerun()
230
 
231
  def clear_widget_keys():
 
237
  # ─── Optimization execution ──────────────────────────────────────────────────
238
 
239
  def run_and_store_result():
240
+ custom_resources = st.session_state.get("custom_resources", [])
241
+
242
+ # Check that at least some resource is non-zero (standard OR custom)
243
+ std_total = sum(st.session_state.resource_caps.values())
244
+ print(custom_resources)
245
+ custom_total = sum(float(b["amount"]) for b in custom_resources)
246
+ if std_total <= 0 and custom_total <= 0:
247
  st.warning("⚠️ All resources are set to 0. Please set resource availability or load a scenario / input data.")
248
  return
249
 
 
251
  resource_caps=st.session_state.resource_caps,
252
  method_constraints=st.session_state.constraints,
253
  method_costs=st.session_state.costs,
254
+ custom_resources=custom_resources,
255
  )
256
 
257
  if not success:
 
277
  "resource_caps": st.session_state.resource_caps.copy(),
278
  "method_constraints": st.session_state.constraints,
279
  "method_costs": st.session_state.costs,
280
+ "custom_resources": custom_resources,
281
  }
282
+ if custom_resources:
283
+ st.info(f"ℹ️ {len(custom_resources)} new resource(s) included in optimisation.")
284
+ st.success(f"✅ {total_removed:,.4f} {CO2_UNIT} removed")
285
 
286
 
287
  # ─── Results display ─────────────────────────────────────────────────────────
288
 
289
  def show_metrics(latest):
290
+ custom_resources = latest.get("custom_resources", [])
291
+ cols = st.columns(3) if custom_resources else st.columns(2)
292
+ cols[0].metric("Total CO₂ Removed", f"{latest['summary']['total_co2_removed']:,.2f} {CO2_UNIT}")
293
+ cols[1].metric("Methods Used", len(latest["allocations"]))
294
+
295
+ # Show which custom batches were included and how much of each was available
296
+ if custom_resources:
297
+ with st.expander(f"✅ {len(custom_resources)} new resource(s) included in this optimisation", expanded=False):
298
+ rows = []
299
+ for batch in custom_resources:
300
+ unit = batch.get("group", "?")
301
+ rows.append({
302
+ "Batch name": batch["name"],
303
+ "Group": batch["group"],
304
+ "Amount available": f"{float(batch['amount']):,.2f}",
305
+ "Methods linked": ", ".join(batch["methods"].keys())
306
+ })
307
+ st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
308
 
309
 
310
  def build_results_df(allocations, method_constraints=None):
311
  df = pd.DataFrame.from_dict(allocations, orient="index", columns=[f"{CO2_UNIT} removed"])
312
+ print(df)
313
  df[f"{CO2_UNIT} removed"] = pd.to_numeric(df[f"{CO2_UNIT} removed"], errors="coerce")
314
  total = df[f"{CO2_UNIT} removed"].sum()
315
  if total > 0:
 
323
  if c.get("cap_type") == "absolute":
324
  caps[m] = f"{c['cap_value']:,.2f} {CO2_UNIT}/yr"
325
  else:
326
+ caps[m] = f"{int(c.get('cap_value', 100))}% of potential"
327
  df["Cap"] = pd.Series(caps)
328
 
329
  return df.sort_values(f"{CO2_UNIT} removed", ascending=False)
 
343
  st.plotly_chart(fig, use_container_width=True)
344
 
345
 
346
+ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None):
347
+ # Normalize standard method_costs keys (strip legacy unit suffixes)
348
  normalized_costs = {
349
  m: {strip_unit_suffix(r): v for r, v in resources.items()}
350
  for m, resources in method_costs.items()
351
  }
352
 
353
+ # Inject custom resource coefficients (median)
354
+ for batch in (custom_resources or []):
355
+ for method, coefs in batch["methods"].items():
356
+ normalized_costs.setdefault(method, {})[batch["name"]] = coefs["median"]
357
+
358
+ # Build complete caps — resource_caps may not contain custom batches
359
+ # if latest_result was generated before the enriched_caps fix
360
+ full_caps = dict(resource_caps)
361
+ for batch in (custom_resources or []):
362
+ full_caps.setdefault(batch["name"], float(batch["amount"]))
363
+
364
+ standard_available = [r for r in resource_order if full_caps.get(r, 0) > 0]
365
+ extra_available = [b["name"] for b in (custom_resources or [])]
366
+ available = standard_available + extra_available
367
+
368
+ if not available:
369
+ st.info("No resources with non-zero availability to display.")
370
+ return
371
+
372
  matrix, y_labels = [], []
373
  for method in df_result.index:
374
  row = [
375
  (normalized_costs.get(method, {}).get(r, 0.0) * df_result.at[method, f"{CO2_UNIT} removed"]
376
+ / full_caps[r] * 100) if full_caps.get(r, 0) > 0 else 0
377
  for r in available
378
  ]
379
  matrix.append(row)
380
  y_labels.append(method)
381
 
382
+ height = max(400, 60 + 30 * len(y_labels))
383
+
384
  fig = go.Figure(data=go.Heatmap(
385
  z=matrix, x=available, y=y_labels,
386
  colorscale="Blues",
 
388
  ))
389
  fig.update_layout(
390
  title="Resource Usage vs. Cap",
391
+ xaxis_title="Resource", yaxis_title="Method", height=height,
392
+ xaxis=dict(tickangle=-35),
393
  )
394
  st.plotly_chart(fig, use_container_width=True)
395
 
396
+ def show_absolute_usage(allocations, method_costs, custom_resources=None):
397
  # Normalize method_costs keys: strip legacy unit suffixes
398
  normalized_costs = {
399
  m: {strip_unit_suffix(r): v for r, v in resources.items()}
400
  for m, resources in method_costs.items()
401
  }
402
 
403
+ # Build resource_units from standard groups
404
  _resource_units = defaultdict(list)
405
  for cat, resources in resource_groups.items():
406
  unit_key = f"{resource_unit_map[cat]}/MtCO2"
407
  _resource_units[unit_key].extend(resources)
408
+
409
+ resource_to_group = {
410
+ r: group
411
+ for group, resources in resource_groups.items()
412
+ for r in resources
413
+ }
414
+
415
+ # Inject custom resource batches into the unit map
416
+ for batch in (custom_resources or []):
417
+ group = resource_to_group.get(batch["group"])
418
+ unit_key = f"{resource_unit_map.get(group, '?')}/MtCO2"
419
+ if batch["name"] not in _resource_units[unit_key]:
420
+ _resource_units[unit_key].append(batch["name"])
421
+ # Make sure the median coefficient is in normalized_costs
422
+ for method, coefs in batch["methods"].items():
423
+ if method in normalized_costs:
424
+ normalized_costs[method].setdefault(batch["name"], coefs["median"])
425
+ else:
426
+ normalized_costs[method] = {batch["name"]: coefs["median"]}
427
+
428
  local_resource_units = dict(_resource_units)
429
 
430
  rows = []
 
438
  return
439
 
440
  df = pd.DataFrame(rows)
441
+ print(df)
442
  for unit in df["Unit"].unique():
443
  st.markdown(f"#### Resources in {unit}")
444
  df_unit = df[df["Unit"] == unit]
 
454
 
455
  def save_scenario(latest):
456
  st.markdown("### Save scenario")
457
+ st.markdown('<p class="caption">Save the current scenario with inputs and outputs for comparison and/or export.</p>', unsafe_allow_html=True)
458
 
459
  suggested = st.session_state.get("scenario_loaded_file", "").rsplit(".", 1)[0]
460
 
 
487
  return
488
 
489
  scenario = st.session_state.scenarios[name]
490
+ # st.markdown(f"#### Scenario: {name}")
491
+ # df_saved = build_results_df(scenario["allocations"], method_constraints=scenario.get("method_constraints"))
492
 
493
+ # st.markdown(format_table_summary(df_saved,"cdr-table-res"), unsafe_allow_html=True)
494
 
495
  safe = unicodedata.normalize("NFKD", name).encode("ascii", "ignore").decode("ascii")
496
  col_json, col_csv = st.columns(2)
 
508
  zf.writestr("resource_caps.csv",
509
  pd.DataFrame.from_dict(scenario["resource_caps"], orient="index",
510
  columns=["Available Amount"]).to_csv())
511
+ zf.writestr(
512
+ "method_constraints.csv",
513
+ pd.DataFrame.from_dict(
514
+ scenario["method_constraints"],
515
+ orient="index",
516
+ columns=["active", "cap_type", "cap_value"]
517
+ ).rename(columns={
518
+ "cap_type": "cap_type (percent or absolute)"
519
+ }).to_csv()
520
+ )
521
  zf.writestr("method_costs.csv",
522
  pd.DataFrame.from_dict(scenario["method_costs"], orient="index").to_csv())
523
  zf.writestr("allocations.csv",
524
  pd.DataFrame.from_dict(scenario["allocations"], orient="index",
525
  columns=[f"{CO2_UNIT} removed"]).to_csv())
526
+ # Custom resource batches — one row per (batch × method)
527
+ custom_batches = scenario.get("custom_resources", [])
528
+ if custom_batches:
529
+ custom_rows = []
530
+ for batch in custom_batches:
531
+ for method, coefs in batch["methods"].items():
532
+ custom_rows.append({
533
+ "name": batch["name"],
534
+ "group": batch["group"],
535
+ "amount": batch["amount"],
536
+ "method": method,
537
+ "min": coefs["min"],
538
+ "median": coefs["median"],
539
+ "max": coefs["max"],
540
+ })
541
+ zf.writestr("custom_resources.csv", pd.DataFrame(custom_rows).to_csv(index=False))
542
+
543
  with col_csv:
544
  st.download_button(
545
  "Export CSV (zip)", data=buf.getvalue(),
 
555
  for r in resources
556
  }
557
  st.markdown("### Export inputs")
558
+ st.markdown('<p class="caption">Export only the resource capabilities, method constraints and coefficients — without results.</p>', unsafe_allow_html=True)
559
 
560
  suggested = st.session_state.get("excel_loaded_file", "").rsplit(".", 1)[0]
561
  # Update when a new file is loaded
 
587
  ws2 = wb.create_sheet("constraints")
588
  ws2.append(["method", "active", "cap_type (percent or absolute)", "cap_value"])
589
  for m, c in st.session_state.constraints.items():
590
+ ws2.append([m, c.get("active", True), c.get("cap_type", "percent"), c.get("cap_value", 100)])
591
 
592
  ws3 = wb.create_sheet("coefficients")
593
  ws3.append(["method", "resource", "value", "secondary"])
 
595
  for r, v in resources.items():
596
  ws3.append([m, r, v, (m, r) in secondary_resources])
597
 
598
+ ws4 = wb.create_sheet("custom_resources")
599
+ ws4.append(["name", "group", "amount", "unit", "method", "min", "median", "max"])
600
+ for batch in st.session_state.get("custom_resources", []):
601
+ for method, c in batch["methods"].items():
602
+ ws4.append([
603
+ batch["name"],
604
+ batch["group"],
605
+ batch["amount"],
606
+ resource_unit_map.get(batch["group"]),
607
+ method,
608
+ c["min"],
609
+ c["median"],
610
+ c["max"],
611
+ ])
612
+
613
  buf = io.BytesIO()
614
  wb.save(buf)
615
 
 
654
  def reset_scenario():
655
  st.divider()
656
  st.markdown("### Reset")
657
+ st.markdown('<p class="caption">Clear all current inputs, results and saved scenarios from the session.</p>', unsafe_allow_html=True)
658
+
659
  if st.button("Clear all data", key="reset_scenario", use_container_width=False, type="primary", icon=":material/delete:"):
660
  confirm_clear_all()
661
 
 
665
  st.markdown('<h1 class="crra-heading">Portfolio generation</h1>', unsafe_allow_html=True)
666
  show_bar()
667
 
668
+ st.markdown(
669
+ """
670
+ <p class="objective_title">OBJECTIVE</p>
671
+ """,
672
+ unsafe_allow_html=True,
673
+ )
674
+
675
+ st.markdown(
676
+ """
677
+ <div class="text_with_border">
678
+ <p>Run the optimization tool, explore results and save scenarios for comparison or export.</p>
679
+ </div>
680
+ """,
681
+ unsafe_allow_html=True
682
  )
683
 
684
  # Session state defaults
 
691
  st.session_state.setdefault(key, default)
692
 
693
  # ── Two-column input area ─────────────────────���────────────────────────
694
+ st.markdown("""
695
+ <p class="crra-sub-heading">Run optimization</p>""",
696
+ unsafe_allow_html=True,
697
+ )
698
  col_inputs, col_scenario = st.columns(2, gap="large")
699
 
700
  with col_inputs:
701
+ st.markdown("""
702
+ <p class="crra-sub-sub-heading">From inputs</p>""",
703
+ unsafe_allow_html=True,
704
+ )
705
  run_clicked = st.button("▶️ Run Optimization", use_container_width=True)
706
  panel_run_from_tabs()
707
  panel_load_from_excel()
708
 
709
  with col_scenario:
710
+ st.markdown("""
711
+ <p class="crra-sub-sub-heading">From a saved scenario</p>""",
712
+ unsafe_allow_html=True,
713
+ )
714
  panel_load_from_json_or_csv()
715
 
716
  # ── Execute if button pressed ──────────────────────────────────────────
 
730
 
731
  # ── Results ────────────────────────────────────────────────────────────
732
  show_metrics(latest)
733
+ df_result = build_results_df(
734
+ allocations,
735
+ method_constraints=latest.get("method_constraints")
736
+ )
737
+
738
+ df_display = df_result.copy()
739
+
740
+ df_display[f"{CO2_UNIT} removed"] = df_display[f"{CO2_UNIT} removed"].map(
741
+ lambda x: f"{x:,.2f}" if pd.notna(x) else x
742
+ )
743
 
744
  st.subheader("Optimization Summary")
745
+ st.markdown(
746
+ format_table_summary(df_display, "cdr-table-res"),
747
+ unsafe_allow_html=True
748
+ )
749
 
750
  st.markdown("### Method allocation breakdown")
751
  show_donut(df_result)
752
 
753
  st.markdown("### Resource usage heatmap (% of cap)")
754
+ show_heatmap(df_result, resource_caps, method_costs, custom_resources=latest.get("custom_resources", []))
755
 
756
  st.markdown("### Absolute resource usage by unit")
757
+ show_absolute_usage(allocations, method_costs, custom_resources=latest.get("custom_resources", []))
758
 
759
  # ── Save & export & reset ───────────────────────────────────────────────────────
760
  save_scenario(latest)
tabs/tab5_sensitivity.py CHANGED
@@ -12,11 +12,21 @@ def render_tab():
12
  st.markdown('<h1 class="crra-heading">Sensitivity analysis</h1>', unsafe_allow_html=True)
13
  show_bar()
14
 
15
- st.info("""
16
- **Objective**:
17
-
18
- - Explore how varying **one or multiple resources' availability** or **methods' maximum allocation** affects total CO₂ removed.
19
- """)
 
 
 
 
 
 
 
 
 
 
20
 
21
  if "resource_caps" not in st.session_state or "constraints" not in st.session_state or "costs" not in st.session_state:
22
  st.warning("Please complete the setup in previous tabs before running sensitivity analysis.")
@@ -27,14 +37,18 @@ def render_tab():
27
  resource_caps=st.session_state.resource_caps,
28
  method_constraints=st.session_state.constraints,
29
  method_costs=st.session_state.costs,
 
30
  )
31
  baseline_removed = baseline_result["total_removed"] if baseline_success else 0
32
 
33
  tab1, tab2 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity"])
34
 
35
  with tab1:
36
- st.subheader("Impact of resource availability")
37
- selected_resources = st.multiselect("Select one or more resources:", resource_order)
 
 
 
38
 
39
  if not selected_resources:
40
  st.warning("Please select at least one resource to run the sensitivity analysis.")
@@ -52,6 +66,7 @@ def render_tab():
52
  resource_caps=temp_caps,
53
  method_constraints=st.session_state.constraints,
54
  method_costs=st.session_state.costs,
 
55
  )
56
 
57
  co2_removed = result["total_removed"] if success else 0
@@ -71,7 +86,7 @@ def render_tab():
71
 
72
  with tab2:
73
  st.subheader("Impact of max allocation per method")
74
- selected_methods = st.multiselect("Select one or more methods:", method_order)
75
 
76
  if not selected_methods:
77
  st.warning("Please select at least one method to run the sensitivity analysis.")
@@ -88,12 +103,13 @@ def render_tab():
88
  for pct in percentages:
89
  temp_constraints = st.session_state.constraints.copy()
90
  temp_constraints[method]["cap_type"] = "percent"
91
- temp_constraints[method]["cap_value"] = pct / 100
92
 
93
  success, result = run_optimization(
94
  resource_caps=st.session_state.resource_caps,
95
  method_constraints=temp_constraints,
96
  method_costs=st.session_state.costs,
 
97
  )
98
  co2_removed = result["total_removed"] if success else 0
99
  all_data.append({
@@ -131,6 +147,7 @@ def render_tab():
131
  resource_caps=st.session_state.resource_caps,
132
  method_constraints=temp_constraints,
133
  method_costs=st.session_state.costs,
 
134
  )
135
  co2_removed = result["total_removed"] if success else 0
136
  all_data.append({
@@ -145,4 +162,4 @@ def render_tab():
145
  sns.lineplot(data=df, x=f"Cap ({CO2_UNIT}/yr)", y=f"CO₂ removed ({CO2_UNIT})", hue="Input", ax=ax)
146
  ax.axhline(y=baseline_removed, linestyle="--", color="gray", label=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
147
  ax.legend()
148
- st.pyplot(fig)
 
12
  st.markdown('<h1 class="crra-heading">Sensitivity analysis</h1>', unsafe_allow_html=True)
13
  show_bar()
14
 
15
+ st.markdown(
16
+ """
17
+ <p class="objective_title">OBJECTIVE</p>
18
+ """,
19
+ unsafe_allow_html=True,
20
+ )
21
+
22
+ st.markdown(
23
+ """
24
+ <div class="text_with_border">
25
+ <p>Explore how varying <strong>one or multiple resources' availability</strong> or <strong>methods' maximum allocation</strong> affects total CO₂ removed.</p>
26
+ </div>
27
+ """,
28
+ unsafe_allow_html=True
29
+ )
30
 
31
  if "resource_caps" not in st.session_state or "constraints" not in st.session_state or "costs" not in st.session_state:
32
  st.warning("Please complete the setup in previous tabs before running sensitivity analysis.")
 
37
  resource_caps=st.session_state.resource_caps,
38
  method_constraints=st.session_state.constraints,
39
  method_costs=st.session_state.costs,
40
+ custom_resources=st.session_state.get("custom_resources", []),
41
  )
42
  baseline_removed = baseline_result["total_removed"] if baseline_success else 0
43
 
44
  tab1, tab2 = st.tabs(["🔁 Resource sensitivity", "🔁 Method sensitivity"])
45
 
46
  with tab1:
47
+ st.markdown("""
48
+ <p class="crra-sub-heading">Impact of resource availability</p>""",
49
+ unsafe_allow_html=True,
50
+ )
51
+ selected_resources = st.multiselect("Select one or more resources:", resource_order,key="crra-select-resource")
52
 
53
  if not selected_resources:
54
  st.warning("Please select at least one resource to run the sensitivity analysis.")
 
66
  resource_caps=temp_caps,
67
  method_constraints=st.session_state.constraints,
68
  method_costs=st.session_state.costs,
69
+ custom_resources=st.session_state.get("custom_resources", []),
70
  )
71
 
72
  co2_removed = result["total_removed"] if success else 0
 
86
 
87
  with tab2:
88
  st.subheader("Impact of max allocation per method")
89
+ selected_methods = st.multiselect("Select one or more methods:", method_order, key="crra-select-method")
90
 
91
  if not selected_methods:
92
  st.warning("Please select at least one method to run the sensitivity analysis.")
 
103
  for pct in percentages:
104
  temp_constraints = st.session_state.constraints.copy()
105
  temp_constraints[method]["cap_type"] = "percent"
106
+ temp_constraints[method]["cap_value"] = pct
107
 
108
  success, result = run_optimization(
109
  resource_caps=st.session_state.resource_caps,
110
  method_constraints=temp_constraints,
111
  method_costs=st.session_state.costs,
112
+ custom_resources=st.session_state.get("custom_resources", []),
113
  )
114
  co2_removed = result["total_removed"] if success else 0
115
  all_data.append({
 
147
  resource_caps=st.session_state.resource_caps,
148
  method_constraints=temp_constraints,
149
  method_costs=st.session_state.costs,
150
+ custom_resources=st.session_state.get("custom_resources", []),
151
  )
152
  co2_removed = result["total_removed"] if success else 0
153
  all_data.append({
 
162
  sns.lineplot(data=df, x=f"Cap ({CO2_UNIT}/yr)", y=f"CO₂ removed ({CO2_UNIT})", hue="Input", ax=ax)
163
  ax.axhline(y=baseline_removed, linestyle="--", color="gray", label=f"Baseline: {baseline_removed:,} {CO2_UNIT}")
164
  ax.legend()
165
+ st.pyplot(fig)
tabs/tab6_scenarios.py CHANGED
@@ -13,8 +13,11 @@ from utils.ui_helpers import show_bar
13
  # ─── Load from file ───────────────────────────────────────────────────────────
14
 
15
  def panel_load_scenario():
16
- st.markdown("### Load a scenario from file")
17
- st.caption("Accepts `.json` or `.zip` (CSV export)")
 
 
 
18
 
19
  uploaded = st.file_uploader("Upload scenario", type=["json", "zip"], key="comparison_loader", accept_multiple_files=True)
20
  if uploaded is None:
 
13
  # ─── Load from file ───────────────────────────────────────────────────────────
14
 
15
  def panel_load_scenario():
16
+ st.markdown("""
17
+ <p class="crra-sub-heading">Load a scenario from file</p>""",
18
+ unsafe_allow_html=True,
19
+ )
20
+ st.markdown('<p class="caption">Accepts `.json` or `.zip` (CSV export)</p>', unsafe_allow_html=True)
21
 
22
  uploaded = st.file_uploader("Upload scenario", type=["json", "zip"], key="comparison_loader", accept_multiple_files=True)
23
  if uploaded is None:
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_order, method_order, secondary_resources
9
 
10
  @st.cache_data
11
  def load_slider_data():
@@ -53,18 +53,18 @@ def load_inputs_from_excel(file):
53
  if "constraints" in xl:
54
  for _, row in xl["constraints"].iterrows():
55
  # Compatibility with older files that used `max_share`
56
- if "cap_type (percent or absolute)" in xl["constraints"].columns:
57
  constraints[row["method"]] = {
58
  "active": bool(row["active"]),
59
  "cap_type": str(row["cap_type (percent or absolute)"]),
60
  "cap_value": float(row["cap_value"])
61
  }
62
- else:
63
- constraints[row["method"]] = {
64
- "active": bool(row["active"]),
65
- "cap_type": "percent",
66
- "cap_value": float(row["max_share"])
67
- }
68
 
69
  costs = {}
70
  if "coefficients" in xl:
@@ -72,7 +72,33 @@ def load_inputs_from_excel(file):
72
  m, r, v = row["method"], row["resource"], float(row["value"])
73
  costs.setdefault(m, {})[r] = v
74
 
75
- return resource_caps, constraints, costs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
 
77
  def strip_unit_suffix(resource_name: str) -> str:
78
  """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
9
 
10
  @st.cache_data
11
  def load_slider_data():
 
53
  if "constraints" in xl:
54
  for _, row in xl["constraints"].iterrows():
55
  # Compatibility with older files that used `max_share`
56
+ # if "cap_type (percent or absolute)" in xl["constraints"].columns:
57
  constraints[row["method"]] = {
58
  "active": bool(row["active"]),
59
  "cap_type": str(row["cap_type (percent or absolute)"]),
60
  "cap_value": float(row["cap_value"])
61
  }
62
+ # else:
63
+ # constraints[row["method"]] = {
64
+ # "active": bool(row["active"]),
65
+ # "cap_type": "percent",
66
+ # "cap_value": float(row["max_share"])
67
+ # }
68
 
69
  costs = {}
70
  if "coefficients" in xl:
 
72
  m, r, v = row["method"], row["resource"], float(row["value"])
73
  costs.setdefault(m, {})[r] = v
74
 
75
+ custom_resources = []
76
+ print("--------------------------------")
77
+ if "custom_resources" in xl:
78
+ df_custom = xl["custom_resources"]
79
+ if not df_custom.empty:
80
+ for name in df_custom["name"].unique():
81
+ df_b = df_custom[df_custom["name"] == name]
82
+ batch = {
83
+ "name": name,
84
+ "group": df_b["group"].iloc[0],
85
+ "amount": float(df_b["amount"].iloc[0]),
86
+ "unit": resource_unit_map.get(
87
+ df_b["group"].iloc[0],
88
+ ""
89
+ ),
90
+ "methods": {}
91
+ }
92
+
93
+ for _, row in df_b.iterrows():
94
+ batch["methods"][row["method"]] = {
95
+ "min": float(row["min"]),
96
+ "median": float(row["median"]),
97
+ "max": float(row["max"]),
98
+ }
99
+ custom_resources.append(batch)
100
+
101
+ return resource_caps, constraints, costs, custom_resources
102
 
103
  def strip_unit_suffix(resource_name: str) -> str:
104
  """Remove legacy unit suffix from resource name, e.g. 'Unspecified land (ha/tCO2)' -> 'Unspecified land'"""
utils/ui_helpers.py CHANGED
@@ -183,15 +183,22 @@ def format_results_table(df, highlight_changes=False, precision=2):
183
  return styled
184
 
185
  def format_resource_summary(df: pd.DataFrame):
186
- """
187
- Format a resource availability summary table with styling and column width control.
188
- """
189
  df = df.copy()
190
  df.index.name = "Resource"
191
  df = df.reset_index()
192
-
193
- html = df.to_html(index=True, classes="resources-summary", border=0)
194
- return html
 
 
 
 
 
 
 
 
 
 
195
 
196
  def smart_display_format(val):
197
  if abs(val) < 1e-8:
 
183
  return styled
184
 
185
  def format_resource_summary(df: pd.DataFrame):
 
 
 
186
  df = df.copy()
187
  df.index.name = "Resource"
188
  df = df.reset_index()
189
+
190
+ def highlight_new(row):
191
+ if row.get("Type") == "New":
192
+ return ["color: #ff5f4d; font-weight: bold"] * len(row)
193
+ return [""] * len(row)
194
+
195
+ styled = (
196
+ df.style
197
+ .apply(highlight_new, axis=1)
198
+ .set_table_attributes('class="resources-summary"')
199
+ )
200
+
201
+ return styled.to_html()
202
 
203
  def smart_display_format(val):
204
  if abs(val) < 1e-8: