CarlyneB commited on
Commit
549ff1c
Β·
1 Parent(s): 64cc758

[FEAT] Objective 2: Modifying the graphical user interface to add custom resources + Test CSS

Browse files
app.py CHANGED
@@ -19,7 +19,7 @@ def load_css(file_path):
19
  css_path = pathlib.Path("assets/styles.css")
20
  load_css(css_path)
21
 
22
- st.markdown('<h1 class="custom-title">CRRA Toolkit: CDR Portfolio Optimization</h1>', unsafe_allow_html=True)
23
 
24
  with st.sidebar:
25
  selected = option_menu(
 
19
  css_path = pathlib.Path("assets/styles.css")
20
  load_css(css_path)
21
 
22
+ st.markdown('<h1 class="custom-title">CRRA <span class="crra-hl">Toolkit</span>: CDR Portfolio Optimization</h1>', unsafe_allow_html=True)
23
 
24
  with st.sidebar:
25
  selected = option_menu(
assets/styles.css CHANGED
@@ -1,4 +1,4 @@
1
- .cdr-table, .cdr-table-res{
2
  width: 100%;
3
  border-collapse: separate;
4
  border-spacing: 0;
@@ -6,7 +6,7 @@
6
  overflow: hidden;
7
  }
8
 
9
- .cdr-table thead th, .cdr-table-res thead th{
10
  background-color: #3d86e9;
11
  color: white;
12
  font-weight: bold;
@@ -14,7 +14,7 @@
14
  text-align: center;
15
  }
16
 
17
- .cdr-table tbody th, .cdr-table-res tbody th{
18
  text-align: center;
19
  color: #949393;
20
  font-weight: 100;
@@ -28,7 +28,7 @@
28
  text-align: center;
29
  }
30
 
31
- .cdr-table tbody tr:hover, .cdr-table-res tbody tr:hover {
32
  background-color: #f5f5f5;
33
  }
34
 
@@ -37,21 +37,116 @@
37
  font-weight: 600;
38
  }
39
 
40
- h1.custom-header {
41
  font-family: "Segoe UI", sans-serif;
42
- font-weight: bold;
43
  font-style: italic;
44
- font-size: 35px;
45
  }
46
 
47
- h1.custom-title {
48
- font-family: "Segoe UI", sans-serif;
49
- font-weight: bold;
50
- font-size: 50px;
51
- font-style: italic;
52
  }
53
 
54
  div.custom-corps {
55
  font-family: "Segoe UI", sans-serif;
56
  font-size: 18px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  }
 
1
+ .cdr-table, .cdr-table-res, .resources-summary {
2
  width: 100%;
3
  border-collapse: separate;
4
  border-spacing: 0;
 
6
  overflow: hidden;
7
  }
8
 
9
+ .cdr-table thead th, .cdr-table-res thead th, .resources-summary thead th{
10
  background-color: #3d86e9;
11
  color: white;
12
  font-weight: bold;
 
14
  text-align: center;
15
  }
16
 
17
+ .cdr-table tbody th, .cdr-table-res tbody th, .resources-summary tbody th{
18
  text-align: center;
19
  color: #949393;
20
  font-weight: 100;
 
28
  text-align: center;
29
  }
30
 
31
+ .cdr-table tbody tr:hover, .cdr-table-res tbody tr:hover, .resources-summary tbody tr:hover {
32
  background-color: #f5f5f5;
33
  }
34
 
 
37
  font-weight: 600;
38
  }
39
 
40
+ h1.custom-title {
41
  font-family: "Segoe UI", sans-serif;
42
+ font-size: 60px;
43
  font-style: italic;
44
+ line-height: 0.95em;
45
  }
46
 
47
+ .crra-hl {
48
+ background: linear-gradient(90deg, #D9E210 0%, #92EF9B 99%);
49
+ -webkit-text-fill-color: transparent;
50
+ background-clip: text;
51
+ padding-right: 6px;
52
  }
53
 
54
  div.custom-corps {
55
  font-family: "Segoe UI", sans-serif;
56
  font-size: 18px;
57
+ }
58
+
59
+ .resources-summary tbody {
60
+ text-align: center
61
+ }
62
+
63
+ .text_with_border {
64
+ border-left: 3px solid #FF5F4D;
65
+ padding-left: 16px;
66
+ font-family: "Segoe UI", sans-serif;
67
+ /* font-style: italic; */
68
+ font-weight: 600;
69
+ font-size: 18px !important;
70
+ }
71
+
72
+ .objective_title {
73
+ color: #FF5F4D;
74
+ font-family: "Segoe UI", sans-serif;
75
+ font-size: 14px;
76
+ font-weight: 700;
77
+ text-transform: uppercase;
78
+ letter-spacing: 0.094444em;
79
+ margin: 0 0 8px 0;
80
+ }
81
+
82
+ .st-key-warning_resources [data-testid="stAlert"] {
83
+ background-color: #EAE9D8!important;
84
+ clip-path: polygon(1.5% 0, 100% 0, 98.5% 100%, 0 100%);
85
+ }
86
+
87
+ .st-key-warning_resources p{
88
+ font-family: 'Segoe UI', sans-serif;
89
+ font-style: italic;
90
+ font-size: 16px;
91
+ font-weight: 600;
92
+ color: #665d5b;
93
+ }
94
+
95
+ .st-key-warning_resources span{
96
+ font-style: italic;
97
+ color: #665d5b;
98
+ }
99
+
100
+ .crra-heading {
101
+ font-family: 'Segoe UI', sans-serif !important;
102
+ font-weight: 700;
103
+ font-style: italic;
104
+ font-size: clamp(32px, 2.5vw, 55px) !important;
105
+ line-height: 1.25em;
106
+ margin: 0;
107
+ padding: 0;
108
+ }
109
+
110
+ .et_pb_module {
111
+ margin-bottom: 30px
112
+ }
113
+
114
+ .crra-sub-heading {
115
+ font-family: 'Segoe UI', sans-serif !important;
116
+ font-weight: 700;
117
+ font-style: italic;
118
+ font-size: clamp(25px, 2vw, 32px) !important;
119
+ line-height: 1.25em;
120
+ color: #665D5B !important;
121
+ margin-bottom: 15px;
122
+ margin-top: 30px !important;
123
+ }
124
+
125
+
126
+ /* buttons */
127
+ .st-key-btn-reset button {
128
+ border-width: 0px !important;
129
+ border-radius: 0px;
130
+ color: #FFFFFF;
131
+ background: #FF5F4D;
132
+ display: inline-flex;
133
+ align-items: center;
134
+ height: clamp(52px, 3.125vw, 74px);
135
+ padding: 0 clamp(20px, 2.625vw, 42px);
136
+ clip-path: polygon(16px 0, 100% 0, calc(100% - 16px) 100%, 0 100%);
137
+ transition: background 0.25s ease;
138
+ }
139
+
140
+ .st-key-btn-reset button p{
141
+ font-weight: 700 !important;
142
+ font-family: "Segoe UI", sans-serif;
143
+ font-size: clamp(14px, 0.900vw, 18px);
144
+ letter-spacing: 0.094444em;
145
+ text-decoration: none;
146
+ text-transform: uppercase;
147
+ }
148
+
149
+ .st-key-btn-reset button:hover{
150
+ background: #E5503F;
151
+ color: #FFFFFF;
152
  }
optimization/optimization.py CHANGED
@@ -2,7 +2,7 @@ import numpy as np
2
  from scipy.optimize import linprog
3
 
4
 
5
- def run_optimization(resource_caps, method_constraints, method_costs):
6
  """
7
  Run linear optimization to maximize COβ‚‚ removal while respecting resource limits and method constraints.
8
 
@@ -12,11 +12,30 @@ def run_optimization(resource_caps, method_constraints, method_costs):
12
  cap_type: "percent" β†’ cap_value is a fraction of total (0 to 1)
13
  cap_type: "absolute" β†’ cap_value is a hard upper bound in COβ‚‚ units
14
  method_costs (dict): {method: {resource: usage_per_tCO2}}
 
 
15
 
16
  Returns:
17
  (bool, dict): (success_flag, result_dict)
18
  result_dict includes "total_removed", "method_usage"
19
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # Only keep active methods
21
  methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
22
  resources = list(resource_caps.keys())
 
2
  from scipy.optimize import linprog
3
 
4
 
5
+ def run_optimization(resource_caps, method_constraints, method_costs, custom_resources=None):
6
  """
7
  Run linear optimization to maximize COβ‚‚ removal while respecting resource limits and method constraints.
8
 
 
12
  cap_type: "percent" β†’ cap_value is a fraction of total (0 to 1)
13
  cap_type: "absolute" β†’ cap_value is a hard upper bound in COβ‚‚ units
14
  method_costs (dict): {method: {resource: usage_per_tCO2}}
15
+ custom_resources (list | None): list of custom batch dicts: [{"name": str, "group": str, "amount": float, "methods": {method: {"min", "median", "max"}}}]
16
+ If provided, each batch is injected as an extra resource row.
17
 
18
  Returns:
19
  (bool, dict): (success_flag, result_dict)
20
  result_dict includes "total_removed", "method_usage"
21
  """
22
+ # ── Inject custom resource batches ────────────────────────────────────────
23
+ # Work on shallow copies so callers are not mutated.
24
+ resource_caps = dict(resource_caps)
25
+ method_costs = {m: dict(v) for m, v in method_costs.items()}
26
+
27
+ for batch in (custom_resources or []):
28
+ r_key = batch["name"]
29
+ resource_caps[r_key] = batch["amount"]
30
+ for method, coefs in batch["methods"].items():
31
+ if method in method_costs:
32
+ method_costs[method][r_key] = coefs["median"]
33
+ else:
34
+ method_costs[method] = {r_key: coefs["median"]}
35
+
36
+
37
+ # ── Standard optimisation setup ───────────────────────────────────────────
38
+
39
  # Only keep active methods
40
  methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
41
  resources = list(resource_caps.keys())
tabs/tab0_welcome.py CHANGED
@@ -1,8 +1,10 @@
1
  import streamlit as st
2
  from config.config import CO2_UNIT
 
3
 
4
  def render_tab():
5
- st.markdown('<h1 class="custom-header">Welcome!</h1>', unsafe_allow_html=True)
 
6
  st.markdown(f"""
7
  <div class='custom-corps'>
8
  This tool helps you optimize your carbon direct removal portfolio by adjusting resource usage and constraints.
 
1
  import streamlit as st
2
  from config.config import CO2_UNIT
3
+ from utils.ui_helpers import show_bar
4
 
5
  def render_tab():
6
+ st.markdown('<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.
tabs/tab1_inputs.py CHANGED
@@ -2,10 +2,327 @@ import pandas as pd
2
  import streamlit as st
3
 
4
  from config.config import (resource_groups, resource_order, resource_tooltips,
5
- resource_unit_map)
6
  from utils.data_utils import render_resource_input
7
- from utils.ui_helpers import format_resource_summary
 
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
  def render_tab():
11
  # Safe session init
@@ -14,56 +331,70 @@ def render_tab():
14
  for r in resource_order:
15
  if r not in st.session_state.resource_caps:
16
  st.session_state.resource_caps[r] = 0.0
17
- st.markdown('<h1 class="custom-header">Resource inputs</h1>', unsafe_allow_html=True)
18
 
19
- st.info("""
20
- **Objective:**
 
 
 
 
 
 
 
21
 
22
- This section lets you define the **total available quantities of each resource** for the optimization model.
23
- """)
 
 
 
24
 
25
- st.warning("Resources with zero availability will block portfolio feasibility if required by active methods.", icon=":material/warning:")
 
 
 
 
26
 
27
  # Reset button
28
- st.markdown("### Controls")
29
- col_reset, _ = st.columns([1, 5])
30
-
31
- with col_reset:
32
- if st.button("πŸ”„ Reset All", type="secondary"):
33
- for r in st.session_state.resource_caps:
34
- st.session_state.resource_caps[r] = 0.0
35
- st.success("All resource values reset to 0.")
36
- st.rerun()
 
 
37
 
38
- # Dowload template
39
- st.markdown("**Import resource values**")
40
  col_template, col_upload = st.columns([1, 1])
41
  with col_template:
42
  st.markdown("Use the template below to fill in your resource availability values and import them directly into the tool.")
43
- st.warning("The change of unit in the template will not be taken into account.", icon=":material/warning:")
44
  with open("data/resource_template.xlsx", "rb") as f:
45
  st.download_button(
46
  label="Download Excel Template",
47
  data=f,
48
  file_name="resource_template.xlsx",
49
- use_container_width= True,
50
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
51
- icon=":material/download:"
52
  )
53
  with col_upload:
54
  uploaded_file = st.file_uploader(
55
  "Upload your Excel file", type="xlsx",
56
  key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}"
57
  )
58
-
59
  if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
60
  try:
61
  df = pd.read_excel(uploaded_file)
62
- required_cols = {"Resource", "Unit", "Available Amount"}
63
  if not required_cols.issubset(df.columns):
64
  raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
65
-
66
- # Reset tout Γ  0 avant d'appliquer le fichier
67
  for r in st.session_state.resource_caps:
68
  st.session_state.resource_caps[r] = 0.0
69
  st.session_state[f"cap_{r}"] = 0.0
@@ -94,10 +425,8 @@ def render_tab():
94
 
95
  elif st.session_state.get("resource_last_loaded_file"):
96
  st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="βœ…")
97
-
98
- col_reset, _ = st.columns([1, 5])
99
 
100
- # Inputs by resource group
101
  st.markdown("### Set available resources for one year")
102
  for group, rlist in resource_groups.items():
103
  unit = resource_unit_map.get(group, "")
@@ -109,16 +438,54 @@ def render_tab():
109
  with cols[i % 4]:
110
  render_resource_input(name_clean, r, tooltip=tooltip)
111
 
112
- # Summary table
 
 
 
113
  st.markdown("### Summary of available resources")
114
- df_caps = pd.DataFrame.from_dict({
115
- r: st.session_state.resource_caps[r]
116
- for r in resource_order if st.session_state.resource_caps.get(r, 0.0) > 0
117
- }, orient="index", columns=["Available Amount"]).reindex(resource_order).dropna()
118
-
119
- if not df_caps.empty:
120
- df_caps.columns = ["Available Amount"]
121
- styled = format_resource_summary(df_caps)
122
- st.table(styled)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  else:
124
- st.error("No resources defined yet. Set availability above to populate the summary table.")
 
2
  import streamlit as st
3
 
4
  from config.config import (resource_groups, resource_order, resource_tooltips,
5
+ resource_unit_map, method_groups, method_order, CO2_UNIT)
6
  from utils.data_utils import render_resource_input
7
+ from utils.ui_helpers import format_resource_summary, show_bar
8
+ from io import BytesIO
9
 
10
+ # ─── Custom resource helpers ──────────────────────────────────────────────────
11
+
12
+ def standard_resource_names():
13
+ """Flat set of all standard resource names (without unit suffixes)."""
14
+ return {r for rlist in resource_groups.values() for r in rlist}
15
+
16
+
17
+ def init_custom_resources():
18
+ if "custom_resources" not in st.session_state:
19
+ st.session_state.custom_resources = []
20
+
21
+
22
+ # ─── Dialog: add / edit a custom resource batch ───────────────────────────────
23
+
24
+ @st.dialog("Add a custom resource", width="large")
25
+ def dialog_add_custom_resource(edit_index: int | None = None):
26
+ """
27
+ Full-screen dialog to create or edit a custom resource batch.
28
+ edit_index: index in st.session_state.custom_resources to edit (None = new).
29
+ """
30
+
31
+ standard_names = standard_resource_names()
32
+ existing_names = {
33
+ b["name"] for i, b in enumerate(st.session_state.custom_resources)
34
+ if i != edit_index
35
+ }
36
+
37
+ # Pre-fill values when editing
38
+ existing = st.session_state.custom_resources[edit_index] if edit_index is not None else {}
39
+
40
+ st.markdown("### 1 β€” Name & group")
41
+ col_name, col_group = st.columns(2)
42
+
43
+ with col_name:
44
+ batch_name = st.text_input(
45
+ "Resource name *",
46
+ value=existing.get("name", ""),
47
+ placeholder="e.g. Degraded forest lands – North",
48
+ help="It can't match the name of an existing resource.",
49
+ )
50
+
51
+ with col_group:
52
+ groups = list(resource_groups.keys())
53
+ default_group_idx = groups.index(existing["group"]) if existing.get("group") in groups else 0
54
+ batch_group = st.selectbox(
55
+ "Resource group *",
56
+ options=groups,
57
+ index=default_group_idx,
58
+ help="Determines the physical unit of this resource.",
59
+ )
60
+
61
+ unit = resource_unit_map.get(batch_group, "?")
62
+ st.caption(f"Unit for this group: **{unit}** (per MtCOβ‚‚ for coefficients)")
63
+
64
+ st.markdown("### 2 β€” Available quantity")
65
+ batch_amount = st.number_input(
66
+ f"Available amount ({unit})",
67
+ min_value=0.0,
68
+ format="%.2f",
69
+ value=float(existing.get("amount", 0.0)),
70
+ help=f"Total quantity available for 1 year, in {unit}.",
71
+ )
72
+
73
+ st.markdown("### 3 β€” Allowed CDR methods & coefficients")
74
+ st.caption(
75
+ f"Select which methods can use this resource and enter their resource consumption coefficients in **{unit}/{CO2_UNIT}**."
76
+ )
77
+
78
+ existing_methods = existing.get("methods", {})
79
+ method_configs = {}
80
+
81
+ for group_name, methods in method_groups.items():
82
+ with st.expander(f"**{group_name}**", expanded=False):
83
+ for m in methods:
84
+ prev = existing_methods.get(m)
85
+ enabled_default = prev is not None
86
+ enabled = st.checkbox(f"**{m}**", value=enabled_default, key=f"_dlg_chk_{m}")
87
+
88
+ if enabled:
89
+ col_min, col_med, col_max = st.columns(3)
90
+ with col_min:
91
+ v_min = st.number_input(
92
+ "Min", min_value=0.0, format="%.4f",
93
+ value=float(prev["min"] if prev else 0.0),
94
+ key=f"_dlg_min_{m}",
95
+ )
96
+ with col_med:
97
+ v_med = st.number_input(
98
+ "Median β˜…", min_value=0.0, format="%.4f",
99
+ value=float(prev["median"] if prev else 0.0),
100
+ key=f"_dlg_med_{m}",
101
+ )
102
+ with col_max:
103
+ v_max = st.number_input(
104
+ "Max", min_value=0.0, format="%.4f",
105
+ value=float(prev["max"] if prev else 0.0),
106
+ key=f"_dlg_max_{m}",
107
+ )
108
+ method_configs[m] = {"min": v_min, "median": v_med, "max": v_max}
109
+
110
+ # ── Save / cancel ──────────────────────────────────────────────────────────
111
+ error_placeholder = st.empty()
112
+
113
+ col_save, col_cancel = st.columns([2, 2])
114
+
115
+ with col_cancel:
116
+ if st.button("Cancel", use_container_width=True):
117
+ st.rerun()
118
+
119
+ def _validate():
120
+ errors = []
121
+ if not batch_name.strip():
122
+ errors.append("Resource name is required.")
123
+ elif batch_name.strip() in standard_names:
124
+ errors.append(f"**{batch_name}** matches a standard resource name. Choose a different name.")
125
+ elif batch_name.strip() in existing_names:
126
+ errors.append(f"**{batch_name}** already exists. Choose a different name.")
127
+ if batch_amount <= 0:
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']}).")
135
+ if not (c["min"] <= c["median"] <= c["max"]):
136
+ errors.append(f"**{m}**: median must be between min and max.")
137
+ return errors
138
+
139
+ with col_save:
140
+ if st.button("Add to session", type="primary", use_container_width=True, icon=":material/check:"):
141
+ errors = _validate()
142
+ if errors:
143
+ with error_placeholder.container():
144
+ for e in errors:
145
+ st.error(e, icon=":material/error:")
146
+ else:
147
+ batch = {
148
+ "name": batch_name.strip(),
149
+ "group": batch_group,
150
+ "amount": batch_amount,
151
+ "methods": method_configs,
152
+ }
153
+ if edit_index is not None:
154
+ st.session_state.custom_resources[edit_index] = batch
155
+ else:
156
+ st.session_state.custom_resources.append(batch)
157
+ # Clear any cached optimization results since inputs changed
158
+ st.session_state.pop("latest_result", None)
159
+ st.rerun()
160
+ return method_configs
161
+
162
+ # ─── Load custom resource from a saved Excel ────────────────────────────────────
163
+
164
+ def to_excel_bytes(df: pd.DataFrame) -> bytes:
165
+ buffer = BytesIO()
166
+ with pd.ExcelWriter(buffer, engine="openpyxl") as writer:
167
+ df.to_excel(writer, index=False, sheet_name="custom_resources")
168
+ return buffer.getvalue()
169
+
170
+ @st.dialog("Load a saved resource batch", width="large")
171
+ def dialog_load_custom_batch():
172
+ st.caption("Import a Excel file that you have previously exported from the β€˜Custom resource’ section")
173
+ uploaded = st.file_uploader("Upload Excel file", type=["xlsx"])
174
+ if uploaded is None:
175
+ return
176
+
177
+ try:
178
+ df = pd.read_excel(uploaded, sheet_name=0)
179
+ required = {"name", "group", "amount", "method", "min", "median", "max"}
180
+ if not required.issubset(df.columns):
181
+ st.error(f"Missing columns: {required - set(df.columns)}")
182
+ return
183
+
184
+ standard_names = standard_resource_names()
185
+ existing_names = {b["name"] for b in st.session_state.custom_resources}
186
+
187
+ batches_in_file = df["name"].unique()
188
+ for bname in batches_in_file:
189
+ df_b = df[df["name"] == bname]
190
+ group = df_b["group"].iloc[0]
191
+ amount = float(df_b["amount"].iloc[0])
192
+ methods = {
193
+ row["method"]: {"min": row["min"], "median": row["median"], "max": row["max"]}
194
+ for _, row in df_b.iterrows()
195
+ }
196
+ batch = {"name": bname, "group": group, "amount": amount, "methods": methods}
197
+
198
+ st.markdown(f"**Batch:** {bname} | Group: {group} | Amount: {amount} | Methods: {len(methods)}")
199
+
200
+ if bname in standard_names:
201
+ st.error(f"'{bname}' matches a standard resource. Skipped.")
202
+ continue
203
+ if bname in existing_names:
204
+ st.warning(f"'{bname}' already exists in session. It will be overwritten.")
205
+ idx = next(i for i, b in enumerate(st.session_state.custom_resources) if b["name"] == bname)
206
+ st.session_state.custom_resources[idx] = batch
207
+ else:
208
+ st.session_state.custom_resources.append(batch)
209
+
210
+ st.success(f"Loaded {len(batches_in_file)} batch(es) from file.")
211
+ st.session_state.pop("latest_result", None)
212
+ st.rerun()
213
+
214
+ except Exception as e:
215
+ st.error(f"Error loading file: {e}")
216
+
217
+
218
+ # ─── Custom resources management panel ────────────────────────────────────────
219
+
220
+ def panel_custom_resources():
221
+ """Panel displayed at the top of the inputs tab to manage custom batches."""
222
+ init_custom_resources()
223
+
224
+ st.markdown("### βž• Custom resource")
225
+ st.caption(
226
+ "Add sub-batches for any resource group and manually assign them to specific CDR methods. "
227
+ "Standard resources above use the default taxonomy; custom batches give you full control."
228
+ )
229
+
230
+ col_add, col_load = st.columns([1, 1])
231
+ with col_add:
232
+ if st.button("New resource", use_container_width=True, type="primary", icon=":material/add:"):
233
+ dialog_add_custom_resource()
234
+ with col_load:
235
+ if st.button("Load from Excel", use_container_width=True, icon=":material/arrow_upload_progress:"):
236
+ dialog_load_custom_batch()
237
+
238
+ batches = st.session_state.custom_resources
239
+ if not batches:
240
+ st.info("No custom batches defined yet. Click **+ New resource** to get started.")
241
+ return
242
+
243
+ st.markdown(f"**{len(batches)} custom batch(es) active**")
244
+
245
+ for i, batch in enumerate(batches):
246
+ unit = resource_unit_map.get(batch["group"], "?")
247
+ n_methods = len(batch["methods"])
248
+
249
+ with st.expander(
250
+ f"πŸ†• {batch['group']} ({unit}) - CDR methods: **{n_methods}**",
251
+ expanded=False,
252
+ ):
253
+ st.markdown(f"**{batch['name']}**")
254
+
255
+ col_amount, _, _, _ = st.columns(4)
256
+ with col_amount:
257
+ new_amount = st.number_input(
258
+ "Available amount",
259
+ label_visibility= "collapsed",
260
+ min_value=0.0,
261
+ value=float(batch["amount"]),
262
+ format="%.2f",
263
+ key=f"custom_amount_{i}",
264
+ )
265
+
266
+ if new_amount != batch["amount"]:
267
+ batch["amount"] = new_amount
268
+ st.session_state.pop("latest_result", None)
269
+
270
+ # Summary table
271
+ rows = [
272
+ {
273
+ "Method": m,
274
+ f"Min ({unit}/MtCOβ‚‚)": f"{c['min']:.2f}",
275
+ f"Median ({unit}/MtCOβ‚‚)": f"{c['median']:.2f}",
276
+ f"Max ({unit}/MtCOβ‚‚)": f"{c['max']:.2f}",
277
+ }
278
+ for m, c in batch["methods"].items()
279
+ ]
280
+
281
+ st.dataframe(
282
+ pd.DataFrame(rows),
283
+ use_container_width=True,
284
+ hide_index=True,
285
+ )
286
+
287
+ col_edit, col_del = st.columns([1, 1])
288
+ with col_edit:
289
+ if st.button("Edit", key=f"edit_batch_{i}", use_container_width=True, icon=":material/edit:"):
290
+ method_configs = dialog_add_custom_resource(edit_index=i)
291
+ print(method_configs)
292
+ with col_del:
293
+ if st.button("Delete", key=f"del_batch_{i}", use_container_width=True, type="primary", icon=":material/delete:"):
294
+ st.session_state.custom_resources.pop(i)
295
+ st.session_state.pop("latest_result", None)
296
+ st.rerun()
297
+
298
+ # ── Export all batches as a single Excel file ─────────────────────────────────────
299
+ _,_,col_save_custom = st.columns(3)
300
+ with col_save_custom:
301
+ all_rows = []
302
+
303
+ for batch in batches:
304
+ for m, coefs in batch["methods"].items():
305
+ all_rows.append({
306
+ "name": batch["name"],
307
+ "group": batch["group"],
308
+ "amount": batch["amount"],
309
+ "method": m,
310
+ "min": coefs["min"],
311
+ "median": coefs["median"],
312
+ "max": coefs["max"],
313
+ })
314
+
315
+ df_export = pd.DataFrame(all_rows)
316
+
317
+ st.download_button(
318
+ "Save all custom resources (Excel)",
319
+ data=to_excel_bytes(df_export),
320
+ file_name="custom_resources.xlsx",
321
+ mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
322
+ icon=":material/save:",
323
+ )
324
+
325
+ # ─── Main render ──────────────────────────────────────────────────────────────
326
 
327
  def render_tab():
328
  # Safe session init
 
331
  for r in resource_order:
332
  if r not in st.session_state.resource_caps:
333
  st.session_state.resource_caps[r] = 0.0
334
+ init_custom_resources()
335
 
336
+ st.markdown('<h1 class="crra-heading">Resource inputs</h1>', unsafe_allow_html=True)
337
+ show_bar()
338
+
339
+ st.markdown(
340
+ """
341
+ <p class="objective_title">OBJECTIVE</p>
342
+ """,
343
+ unsafe_allow_html=True,
344
+ )
345
 
346
+ st.markdown("""
347
+ <p class="text_with_border"> This section lets you define the total available quantities of each resource for the optimization model.</p>
348
+ """,
349
+ unsafe_allow_html=True,
350
+ )
351
 
352
+ with st.container(key="warning_resources"):
353
+ st.warning(
354
+ "Resources with zero availability will block portfolio feasibility if required by active methods.",
355
+ icon=":material/warning:"
356
+ )
357
 
358
  # Reset button
359
+ st.markdown("""
360
+ <p class="crra-sub-heading">Controls</p>""",
361
+ unsafe_allow_html=True,
362
+ )
363
+
364
+ if st.button("RESET ALL", key="btn-reset", icon=":material/reset_settings:"):
365
+ for r in st.session_state.resource_caps:
366
+ st.session_state.resource_caps[r] = 0.0
367
+ st.session_state.custom_resources = []
368
+ st.success("All resource values reset to 0.")
369
+ st.rerun()
370
 
371
+ # Import / download template
372
+ st.markdown("##### Import resource values")
373
  col_template, col_upload = st.columns([1, 1])
374
  with col_template:
375
  st.markdown("Use the template below to fill in your resource availability values and import them directly into the tool.")
 
376
  with open("data/resource_template.xlsx", "rb") as f:
377
  st.download_button(
378
  label="Download Excel Template",
379
  data=f,
380
  file_name="resource_template.xlsx",
381
+ use_container_width=True,
382
  mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
383
+ icon=":material/download:",
384
  )
385
  with col_upload:
386
  uploaded_file = st.file_uploader(
387
  "Upload your Excel file", type="xlsx",
388
  key=f"resource_uploader_{st.session_state.get('resource_uploader_key', 0)}"
389
  )
390
+
391
  if uploaded_file is not None and uploaded_file.name != st.session_state.get("resource_last_loaded_file"):
392
  try:
393
  df = pd.read_excel(uploaded_file)
394
+ required_cols = {"Resource", "Available Amount"}
395
  if not required_cols.issubset(df.columns):
396
  raise ValueError(f"Missing columns: {required_cols - set(df.columns)}")
397
+
 
398
  for r in st.session_state.resource_caps:
399
  st.session_state.resource_caps[r] = 0.0
400
  st.session_state[f"cap_{r}"] = 0.0
 
425
 
426
  elif st.session_state.get("resource_last_loaded_file"):
427
  st.success(f"Loaded: **{st.session_state['resource_last_loaded_file']}**", icon="βœ…")
 
 
428
 
429
+ # ── Standard resource inputs ───────────────────────────────────────────────
430
  st.markdown("### Set available resources for one year")
431
  for group, rlist in resource_groups.items():
432
  unit = resource_unit_map.get(group, "")
 
438
  with cols[i % 4]:
439
  render_resource_input(name_clean, r, tooltip=tooltip)
440
 
441
+ # ── Custom resource batches panel ──────────────────────────────────────────
442
+ panel_custom_resources()
443
+
444
+ # ── Summary table ──────────────────────────────────────────────────────────
445
  st.markdown("### Summary of available resources")
446
+
447
+ # Standard resources
448
+ df_caps = pd.DataFrame.from_dict(
449
+ {r: st.session_state.resource_caps[r] for r in resource_order
450
+ if st.session_state.resource_caps.get(r, 0.0) > 0},
451
+ orient="index", columns=["Available Amount"]
452
+ ).reindex(resource_order).dropna()
453
+
454
+ # Custom resource rows
455
+ custom_rows = []
456
+ for batch in st.session_state.get("custom_resources", []):
457
+ unit = resource_unit_map.get(batch["group"], "?")
458
+ custom_rows.append({
459
+ "Resource": f"{batch['name']}",
460
+ "Available Amount": batch["amount"],
461
+ })
462
+
463
+ if not df_caps.empty or custom_rows:
464
+
465
+ # --- Standard resources ---
466
+ dfs = []
467
+
468
+ if not df_caps.empty:
469
+ df_std = df_caps.copy()
470
+ df_std = df_std.rename(columns={"Available Amount": "Available Amount"})
471
+ df_std["Type"] = "Standard"
472
+ dfs.append(df_std)
473
+
474
+ # --- Custom resources ---
475
+ if custom_rows:
476
+ df_custom = pd.DataFrame(custom_rows).set_index("Resource")
477
+ df_custom["Type"] = "Custom"
478
+ dfs.append(df_custom)
479
+
480
+ # --- Merge ---
481
+ df_all = pd.concat(dfs)
482
+
483
+ # Optionnel : ordre des colonnes
484
+ cols = ["Type", "Available Amount"]
485
+ df_all = df_all[cols]
486
+
487
+ # Styling
488
+ st.markdown(format_resource_summary(df_all), unsafe_allow_html=True)
489
+
490
  else:
491
+ st.error("No resources defined yet. Set availability above to populate the summary table.")
tabs/tab2_constraints.py CHANGED
@@ -4,7 +4,7 @@ import pandas as pd
4
  import streamlit as st
5
 
6
  from config.config import CO2_UNIT, method_groups, method_order, method_tooltips
7
- from utils.ui_helpers import format_table_summary
8
 
9
 
10
  # ─── Helpers ─────────────────────────────────────────────────────────────────
@@ -108,7 +108,9 @@ def build_summary_df():
108
  # ─── Main render ─────────────────────────────────────────────────────────────
109
 
110
  def render_tab():
111
- st.markdown('<h1 class="custom-header">Method constraints</h1>', unsafe_allow_html=True)
 
 
112
  st.info("""
113
  **Objective:**
114
 
 
4
  import streamlit as st
5
 
6
  from config.config import CO2_UNIT, method_groups, method_order, method_tooltips
7
+ from utils.ui_helpers import format_table_summary, show_bar
8
 
9
 
10
  # ─── Helpers ─────────────────────────────────────────────────────────────────
 
108
  # ─── Main render ─────────────────────────────────────────────────────────────
109
 
110
  def render_tab():
111
+ st.markdown('<h1 class="crra-heading">Method constraints</h1>', unsafe_allow_html=True)
112
+ show_bar()
113
+
114
  st.info("""
115
  **Objective:**
116
 
tabs/tab3_coefficients.py CHANGED
@@ -3,7 +3,7 @@ import streamlit as st
3
 
4
  from config.config import (method_groups, method_order, resource_groups,
5
  resource_order, secondary_resources, resource_unit_map, CO2_UNIT)
6
- from utils.ui_helpers import format_efficiency_summary
7
  from utils.data_utils import strip_unit_suffix
8
 
9
 
@@ -26,8 +26,9 @@ def _get_slider_params(conf: dict) -> tuple:
26
 
27
 
28
  def render_tab(cost_sliders):
29
- st.header("Resource efficiency coefficients")
30
-
 
31
  st.info("""
32
  **Objective:**
33
 
 
3
 
4
  from config.config import (method_groups, method_order, resource_groups,
5
  resource_order, secondary_resources, resource_unit_map, CO2_UNIT)
6
+ from utils.ui_helpers import format_efficiency_summary, show_bar
7
  from utils.data_utils import strip_unit_suffix
8
 
9
 
 
26
 
27
 
28
  def render_tab(cost_sliders):
29
+ st.markdown('<h1 class="crra-heading">Resource efficiency coefficients</h1>', unsafe_allow_html=True)
30
+ show_bar()
31
+
32
  st.info("""
33
  **Objective:**
34
 
tabs/tab4_optimization.py CHANGED
@@ -14,7 +14,7 @@ import plotly.graph_objects as go
14
 
15
  from config.config import method_order, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
16
  from optimization.optimization import run_optimization
17
- from utils.ui_helpers import format_table_summary
18
  from utils.data_utils import is_valid_scenario, load_inputs_from_excel, strip_unit_suffix
19
 
20
 
@@ -520,7 +520,9 @@ def reset_scenario():
520
  # ─── Main tab entry point ─────────────────────────────────────────────────────
521
 
522
  def render_tab():
523
- st.header("Portfolio generation")
 
 
524
  st.info(
525
  "Run the optimization tool, explore results and save scenarios for comparison or export."
526
  )
 
14
 
15
  from config.config import method_order, resource_order, resource_unit_map, resource_groups, secondary_resources, CO2_UNIT
16
  from optimization.optimization import run_optimization
17
+ from utils.ui_helpers import format_table_summary, show_bar
18
  from utils.data_utils import is_valid_scenario, load_inputs_from_excel, strip_unit_suffix
19
 
20
 
 
520
  # ─── Main tab entry point ─────────────────────────────────────────────────────
521
 
522
  def render_tab():
523
+ st.markdown('<h1 class="crra-heading">Portfolio generation</h1>', unsafe_allow_html=True)
524
+ show_bar()
525
+
526
  st.info(
527
  "Run the optimization tool, explore results and save scenarios for comparison or export."
528
  )
tabs/tab5_sensitivity.py CHANGED
@@ -5,10 +5,13 @@ import streamlit as st
5
 
6
  from config.config import CO2_UNIT, method_order, resource_order
7
  from optimization.optimization import run_optimization
 
8
 
9
 
10
  def render_tab():
11
- st.header("Sensitivity analysis")
 
 
12
  st.info("""
13
  **Objective**:
14
 
 
5
 
6
  from config.config import CO2_UNIT, method_order, resource_order
7
  from optimization.optimization import run_optimization
8
+ from utils.ui_helpers import show_bar
9
 
10
 
11
  def render_tab():
12
+ st.markdown('<h1 class="crra-heading">Sensitivity analysis</h1>', unsafe_allow_html=True)
13
+ show_bar()
14
+
15
  st.info("""
16
  **Objective**:
17
 
tabs/tab6_scenarios.py CHANGED
@@ -7,6 +7,7 @@ import streamlit as st
7
  from config.config import CO2_UNIT
8
  from utils.data_utils import is_valid_scenario
9
  from tabs.tab4_optimization import parse_zip_scenario
 
10
 
11
 
12
  # ─── Load from file ───────────────────────────────────────────────────────────
@@ -128,7 +129,8 @@ def _show_resource_usage(df):
128
  # ─── Main tab entry point ─────────────────────────────────────────────────────
129
 
130
  def render_tab():
131
- st.header("Scenario Comparison")
 
132
 
133
  st.session_state.setdefault("scenarios", {})
134
 
 
7
  from config.config import CO2_UNIT
8
  from utils.data_utils import is_valid_scenario
9
  from tabs.tab4_optimization import parse_zip_scenario
10
+ from utils.ui_helpers import show_bar
11
 
12
 
13
  # ─── Load from file ───────────────────────────────────────────────────────────
 
129
  # ─── Main tab entry point ─────────────────────────────────────────────────────
130
 
131
  def render_tab():
132
+ st.markdown('<h1 class="crra-heading">Scenario Comparison</h1>', unsafe_allow_html=True)
133
+ show_bar()
134
 
135
  st.session_state.setdefault("scenarios", {})
136
 
utils/ui_helpers.py CHANGED
@@ -6,6 +6,21 @@ from config.config import CO2_UNIT
6
  # params
7
  background_color = "#3d86e9"
8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  def styled_table(df):
10
  return (
11
  df.style
@@ -41,23 +56,6 @@ def format_table_summary(df: pd.DataFrame, class_name: str):
41
 
42
  html = df.to_html(index=True, classes=class_name, border=0)
43
  return html
44
-
45
- # df = df.copy()
46
- # df.index.name = "CDR Method"
47
- # df.reset_index(inplace=True)
48
-
49
- # return (
50
- # df.style
51
- # .format({
52
- # f"{CO2_UNIT} removed": "{:,.0f}",
53
- # "Share (%)": "{:.2f}"
54
- # })
55
- # .set_table_styles([
56
- # {"selector": "thead th", "props": [("background-color", background_color), ("color", "white"), ("font-weight", "bold")]},
57
- # {"selector": "td.col0", "props": [("text-align", "left"), ("font-weight", "bold")]},
58
- # {"selector": "td.col1", "props": [("text-align", "center")]}
59
- # ])
60
- # )
61
 
62
  def format_efficiency_summary(df: pd.DataFrame, df_default: pd.DataFrame):
63
  df_numeric = df.copy()
@@ -189,31 +187,11 @@ def format_resource_summary(df: pd.DataFrame):
189
  Format a resource availability summary table with styling and column width control.
190
  """
191
  df = df.copy()
192
- col = df.columns[0]
193
- df[col] = df[col].map(lambda x: f"{x:,.2f}")
194
- df.index.name = "Resource" # label the index for clarity
195
-
196
- return (
197
- df.style
198
- .set_table_styles([
199
- {
200
- "selector": "thead th:nth-child(1)", # First column header (after index)
201
- "props": [
202
- ("background-color", background_color),
203
- ("color", "white"),
204
- ("font-weight", "bold")
205
- ]
206
- },
207
- {
208
- "selector": "thead th:nth-child(2)", # Second column header
209
- "props": [
210
- ("background-color", background_color),
211
- ("color", "white"),
212
- ("font-weight", "bold")
213
- ]
214
- }
215
- ])
216
- )
217
 
218
  def smart_display_format(val):
219
  if abs(val) < 1e-8:
 
6
  # params
7
  background_color = "#3d86e9"
8
 
9
+ def show_bar():
10
+ st.markdown(
11
+ """
12
+ <div class="et_pb_module">
13
+ <div style="display:flex;width:100%;height:12px;margin:0 auto 0 0;">
14
+ <span style="flex:1;background:#FF5F4D;"></span>
15
+ <span style="flex:1;background:#A0A0A0;"></span>
16
+ <span style="flex:1;background:#D9E210;"></span>
17
+ <span style="flex:1;background:#55C95B;"></span>
18
+ </div>
19
+ </div>
20
+ """,
21
+ unsafe_allow_html=True,
22
+ )
23
+
24
  def styled_table(df):
25
  return (
26
  df.style
 
56
 
57
  html = df.to_html(index=True, classes=class_name, border=0)
58
  return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
  def format_efficiency_summary(df: pd.DataFrame, df_default: pd.DataFrame):
61
  df_numeric = df.copy()
 
187
  Format a resource availability summary table with styling and column width control.
188
  """
189
  df = df.copy()
190
+ df.index.name = "Resource"
191
+ df = df.reset_index()
192
+
193
+ html = df.to_html(index=True, classes="resources-summary", border=0)
194
+ return html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
195
 
196
  def smart_display_format(val):
197
  if abs(val) < 1e-8: