Spaces:
Sleeping
Sleeping
[FEAT] objective 2: More flexible resources input
Browse files- app.py +29 -3
- assets/styles.css +295 -30
- data/generate_input_template.py +10 -7
- data/inputs_template.xlsx +0 -0
- optimization/optimization.py +22 -7
- tabs/tab0_welcome.py +24 -20
- tabs/tab1_inputs.py +381 -208
- tabs/tab2_constraints.py +94 -62
- tabs/tab3_coefficients.py +37 -19
- tabs/tab4_optimization.py +212 -38
- tabs/tab5_sensitivity.py +27 -10
- tabs/tab6_scenarios.py +5 -2
- utils/data_utils.py +35 -9
- utils/ui_helpers.py +13 -6
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":
|
| 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:
|
| 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:
|
| 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:
|
| 42 |
font-size: 60px;
|
| 43 |
font-style: italic;
|
| 44 |
line-height: 0.95em;
|
| 45 |
}
|
| 46 |
|
| 47 |
.crra-hl {
|
| 48 |
-
background: linear-gradient(90deg,
|
| 49 |
-webkit-text-fill-color: transparent;
|
| 50 |
background-clip: text;
|
| 51 |
padding-right: 6px;
|
| 52 |
}
|
| 53 |
|
| 54 |
div.custom-corps {
|
| 55 |
-
font-family:
|
| 56 |
font-size: 18px;
|
| 57 |
}
|
| 58 |
|
|
@@ -61,17 +92,15 @@ div.custom-corps {
|
|
| 61 |
}
|
| 62 |
|
| 63 |
.text_with_border {
|
| 64 |
-
border-left: 3px solid
|
| 65 |
padding-left: 16px;
|
| 66 |
-
font-family:
|
| 67 |
-
/* font-style: italic; */
|
| 68 |
-
font-weight: 600;
|
| 69 |
font-size: 18px !important;
|
| 70 |
}
|
| 71 |
|
| 72 |
.objective_title {
|
| 73 |
-
color:
|
| 74 |
-
font-family:
|
| 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:
|
| 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:
|
| 89 |
font-style: italic;
|
| 90 |
font-size: 16px;
|
| 91 |
-
|
| 92 |
-
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
|
| 95 |
.st-key-warning_resources span{
|
| 96 |
font-style: italic;
|
| 97 |
-
color: #
|
| 98 |
}
|
| 99 |
|
| 100 |
.crra-heading {
|
| 101 |
-
font-family:
|
| 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:
|
| 116 |
font-weight: 700;
|
| 117 |
font-style: italic;
|
| 118 |
font-size: clamp(25px, 2vw, 32px) !important;
|
| 119 |
line-height: 1.25em;
|
| 120 |
-
color:
|
| 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:
|
| 131 |
-
background:
|
| 132 |
-
display: inline-flex;
|
| 133 |
-
align-items: center;
|
| 134 |
height: clamp(52px, 3.125vw, 74px);
|
| 135 |
-
padding: 0 clamp(20px,
|
| 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:
|
| 143 |
-
font-size: clamp(
|
| 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",
|
| 45 |
|
| 46 |
ws3 = wb.create_sheet("coefficients")
|
| 47 |
ws3.append(["method", "resource", "value", "secondary"])
|
| 48 |
-
ws3[
|
| 49 |
-
|
| 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
|
| 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",
|
| 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 += -
|
| 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 |
-
|
| 88 |
-
|
|
|
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 25 |
-
def dialog_add_custom_resource(edit_index: int | None = None):
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 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 |
-
|
| 41 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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=
|
| 57 |
index=default_group_idx,
|
| 58 |
help="Determines the physical unit of this resource.",
|
|
|
|
| 59 |
)
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 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",
|
| 93 |
-
|
| 94 |
-
|
|
|
|
| 95 |
)
|
| 96 |
with col_med:
|
| 97 |
v_med = st.number_input(
|
| 98 |
-
"Median ★",
|
| 99 |
-
|
| 100 |
-
|
|
|
|
| 101 |
)
|
| 102 |
with col_max:
|
| 103 |
v_max = st.number_input(
|
| 104 |
-
"Max",
|
| 105 |
-
|
| 106 |
-
|
|
|
|
| 107 |
)
|
| 108 |
-
method_configs[m] = {
|
| 109 |
-
|
| 110 |
-
|
|
|
|
|
|
|
| 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="
|
| 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 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
-
|
|
|
|
|
|
|
| 171 |
def dialog_load_custom_batch():
|
| 172 |
-
st.
|
| 173 |
-
|
|
|
|
| 174 |
if uploaded is None:
|
| 175 |
return
|
| 176 |
|
| 177 |
try:
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
|
| 200 |
-
|
| 201 |
-
|
|
|
|
|
|
|
| 202 |
continue
|
| 203 |
-
if
|
| 204 |
-
|
| 205 |
-
|
| 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
|
| 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(
|
| 225 |
-
st.
|
| 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 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
if st.button("Load from Excel",
|
| 236 |
dialog_load_custom_batch()
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
batches = st.session_state.custom_resources
|
| 239 |
if not batches:
|
| 240 |
-
st.info("No
|
| 241 |
return
|
| 242 |
|
| 243 |
-
st.markdown(f"**{len(batches)}
|
| 244 |
-
|
| 245 |
for i, batch in enumerate(batches):
|
| 246 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
n_methods = len(batch["methods"])
|
|
|
|
| 248 |
|
| 249 |
with st.expander(
|
| 250 |
-
f"🆕 {batch['
|
| 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=
|
| 260 |
min_value=0.0,
|
| 261 |
-
value=float(batch["amount"]),
|
| 262 |
format="%.2f",
|
| 263 |
-
key=
|
| 264 |
)
|
| 265 |
-
|
| 266 |
-
if new_amount !=
|
| 267 |
-
|
| 268 |
st.session_state.pop("latest_result", None)
|
| 269 |
|
| 270 |
-
#
|
|
|
|
|
|
|
|
|
|
| 271 |
rows = [
|
| 272 |
{
|
| 273 |
"Method": m,
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 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 |
-
|
| 291 |
-
print(method_configs)
|
| 292 |
with col_del:
|
| 293 |
-
if st.button("Delete", key=f"del_batch_{i}", use_container_width=True,
|
|
|
|
| 294 |
st.session_state.custom_resources.pop(i)
|
| 295 |
st.session_state.pop("latest_result", None)
|
| 296 |
st.rerun()
|
| 297 |
|
| 298 |
-
#
|
| 299 |
-
_,
|
| 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 |
-
"
|
| 319 |
-
data=to_excel_bytes(
|
| 320 |
-
file_name="
|
| 321 |
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
| 322 |
-
icon=":material/
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
|
|
|
|
|
|
| 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 |
-
|
| 417 |
-
|
| 418 |
-
|
| 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 |
-
#
|
| 430 |
-
st.markdown(
|
| 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 |
-
#
|
| 442 |
-
panel_custom_resources()
|
| 443 |
|
| 444 |
-
#
|
| 445 |
-
st.markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
custom_rows = []
|
| 456 |
for batch in st.session_state.get("custom_resources", []):
|
| 457 |
-
|
|
|
|
| 458 |
custom_rows.append({
|
| 459 |
-
"Resource":
|
| 460 |
-
"Available Amount": batch[
|
| 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"] = "
|
| 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":
|
| 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 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 35 |
cap_type = st.radio(
|
| 36 |
"Cap value type",
|
| 37 |
-
|
| 38 |
-
options=["percent", "absolute"],
|
| 39 |
-
index=cap_type_index,
|
| 40 |
horizontal=True,
|
| 41 |
-
key=
|
| 42 |
-
format_func=lambda x: "% of total potential"
|
|
|
|
|
|
|
| 43 |
)
|
| 44 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
if cap_type == "percent":
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
f"Max cap ({CO2_UNIT}/yr)",
|
| 72 |
min_value=0.0,
|
| 73 |
format="%.2f",
|
| 74 |
-
|
| 75 |
-
key=f"manual_{m}_widget",
|
| 76 |
)
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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"{
|
| 103 |
else:
|
| 104 |
-
rows[m] = {"Cap value": f"{c['cap_value']:,.2f}
|
| 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.
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
for group, method_list in method_groups.items():
|
| 126 |
-
st.markdown(f"
|
|
|
|
|
|
|
|
|
|
| 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("
|
| 133 |
reset_all_constraints()
|
| 134 |
|
| 135 |
-
st.
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 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("
|
| 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 |
-
|
| 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("
|
| 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.
|
| 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.
|
| 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.
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 242 |
|
| 243 |
|
| 244 |
# ─── Results display ─────────────────────────────────────────────────────────
|
| 245 |
|
| 246 |
def show_metrics(latest):
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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',
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
/
|
| 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=
|
|
|
|
| 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
|
| 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.
|
| 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(
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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",
|
| 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.
|
|
|
|
| 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.
|
| 527 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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("
|
|
|
|
|
|
|
|
|
|
| 541 |
col_inputs, col_scenario = st.columns(2, gap="large")
|
| 542 |
|
| 543 |
with col_inputs:
|
| 544 |
-
st.markdown("
|
|
|
|
|
|
|
|
|
|
| 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("
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 571 |
|
| 572 |
st.subheader("Optimization Summary")
|
| 573 |
-
|
| 574 |
-
|
|
|
|
|
|
|
| 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.
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 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.
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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("
|
| 17 |
-
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 194 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|