CarlyneB commited on
Commit
4885a3b
·
1 Parent(s): fa58ff0

[FIX] Try fixing the position of the sidebar

Browse files
app.py CHANGED
@@ -10,30 +10,30 @@ from streamlit_option_menu import option_menu
10
  # ── Auto-update: regenerate JSON files if inputs_aggregated.csv has changed ───
11
  # Uses a hash file to detect changes — works both locally and on Hugging Face.
12
  # Must run before importing config/tabs, which read the generated JSON files.
13
- _csv = "data/inputs_aggregated.csv"
14
- _hash_file = "data/.csv_hash"
15
- _generated = [
16
  "data/cost_sliders.json",
17
  "data/method_groups.json",
18
  "data/secondary_resources.json",
19
  ]
20
 
21
- def _md5(path):
22
  hasher = hashlib.md5()
23
  with open(path, "rb") as f:
24
  hasher.update(f.read())
25
  return hasher.hexdigest()
26
 
27
- if os.path.exists(_csv):
28
- _current_hash = _md5(_csv)
29
- _stored_hash = open(_hash_file).read().strip() if os.path.exists(_hash_file) else ""
30
- _missing = any(not os.path.exists(f) for f in _generated)
31
 
32
- if _current_hash != _stored_hash or _missing:
33
  print("inputs_aggregated.csv has changed — running data/update.py ...")
34
  subprocess.run([sys.executable, "data/update.py"], check=True)
35
- with open(_hash_file, "w") as _f:
36
- _f.write(_current_hash)
37
  print("Data files updated successfully.")
38
 
39
  from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
 
10
  # ── Auto-update: regenerate JSON files if inputs_aggregated.csv has changed ───
11
  # Uses a hash file to detect changes — works both locally and on Hugging Face.
12
  # Must run before importing config/tabs, which read the generated JSON files.
13
+ csv = "data/inputs_aggregated.csv"
14
+ hash_file = "data/.csv_hash"
15
+ generated = [
16
  "data/cost_sliders.json",
17
  "data/method_groups.json",
18
  "data/secondary_resources.json",
19
  ]
20
 
21
+ def md5(path):
22
  hasher = hashlib.md5()
23
  with open(path, "rb") as f:
24
  hasher.update(f.read())
25
  return hasher.hexdigest()
26
 
27
+ if os.path.exists(csv):
28
+ current_hash = md5(csv)
29
+ stored_hash = open(hash_file).read().strip() if os.path.exists(hash_file) else ""
30
+ missing = any(not os.path.exists(f) for f in generated)
31
 
32
+ if current_hash != stored_hash or missing:
33
  print("inputs_aggregated.csv has changed — running data/update.py ...")
34
  subprocess.run([sys.executable, "data/update.py"], check=True)
35
+ with open(hash_file, "w") as file:
36
+ file.write(current_hash)
37
  print("Data files updated successfully.")
38
 
39
  from tabs import (tab0_welcome, tab1_inputs, tab2_constraints,
assets/styles.css CHANGED
@@ -53,6 +53,18 @@ input, textarea, select,
53
  background: var(--off-white) !important;
54
  }
55
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  /* ── 4. Markdown headings (st.markdown "### …") ────────────── */
57
  .stMarkdown h1 {
58
  font-weight: 700;
 
53
  background: var(--off-white) !important;
54
  }
55
 
56
+ /* Default width — no !important so Streamlit's drag-to-resize JS can override it */
57
+ section[data-testid="stSidebar"] {
58
+ width: 350px;
59
+ }
60
+
61
+ /* Keep the sidebar fixed on screen while the main content scrolls */
62
+ section[data-testid="stSidebar"] > div:first-child {
63
+ position: fixed;
64
+ overflow-y: auto;
65
+ width: inherit;
66
+ }
67
+
68
  /* ── 4. Markdown headings (st.markdown "### …") ────────────── */
69
  .stMarkdown h1 {
70
  font-weight: 700;
config/config.py CHANGED
@@ -7,20 +7,20 @@ CO2_UNIT = "MtCO₂"
7
  with open("data/method_groups.json") as _f:
8
  method_groups: dict[str, list[str]] = json.load(_f)
9
 
10
- _csv = pd.read_csv("data/inputs_aggregated.csv")
11
 
12
  # resource_groups: group → [resources], order follows first appearance in CSV
13
  resource_groups: dict[str, list[str]] = {}
14
- for _, _row in _csv.drop_duplicates(subset=["Resource group", "Resource"]).iterrows():
15
- _g, _r = _row["Resource group"], _row["Resource"]
16
- resource_groups.setdefault(_g, [])
17
- if _r not in resource_groups[_g]:
18
- resource_groups[_g].append(_r)
19
 
20
  # resource_unit_map: group → base unit (e.g. "Mha/MtCO2" → "Mha")
21
  resource_unit_map: dict[str, str] = {}
22
- for _, _row in _csv.drop_duplicates(subset=["Resource group"]).iterrows():
23
- resource_unit_map[_row["Resource group"]] = str(_row["Unit"]).split("/")[0]
24
 
25
  resource_order = sum(resource_groups.values(), [])
26
  method_order = sum(method_groups.values(), [])
@@ -35,17 +35,17 @@ except FileNotFoundError:
35
  # To add or edit a tooltip, open that file — no code change needed.
36
  # If the file is missing, tooltips are silently disabled (no hover text shown).
37
  try:
38
- _tooltips_xl = pd.read_excel("data/tooltips.xlsx", sheet_name=None)
39
- _df_res = _tooltips_xl.get("Resources", pd.DataFrame(columns=["Resource", "Tooltip"]))
40
- _df_met = _tooltips_xl.get("Methods", pd.DataFrame(columns=["Method", "Tooltip"]))
41
  resource_tooltips: dict[str, str] = {
42
  row["Resource"]: row["Tooltip"]
43
- for _, row in _df_res.iterrows()
44
  if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
45
  }
46
  method_tooltips: dict[str, str] = {
47
  row["Method"]: row["Tooltip"]
48
- for _, row in _df_met.iterrows()
49
  if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
50
  }
51
  except FileNotFoundError:
 
7
  with open("data/method_groups.json") as _f:
8
  method_groups: dict[str, list[str]] = json.load(_f)
9
 
10
+ csv_taxonomy = pd.read_csv("data/inputs_aggregated.csv")
11
 
12
  # resource_groups: group → [resources], order follows first appearance in CSV
13
  resource_groups: dict[str, list[str]] = {}
14
+ for _, row in csv_taxonomy.drop_duplicates(subset=["Resource group", "Resource"]).iterrows():
15
+ group, resource = row["Resource group"], row["Resource"]
16
+ resource_groups.setdefault(group, [])
17
+ if resource not in resource_groups[group]:
18
+ resource_groups[group].append(resource)
19
 
20
  # resource_unit_map: group → base unit (e.g. "Mha/MtCO2" → "Mha")
21
  resource_unit_map: dict[str, str] = {}
22
+ for _, row in csv_taxonomy.drop_duplicates(subset=["Resource group"]).iterrows():
23
+ resource_unit_map[row["Resource group"]] = str(row["Unit"]).split("/")[0]
24
 
25
  resource_order = sum(resource_groups.values(), [])
26
  method_order = sum(method_groups.values(), [])
 
35
  # To add or edit a tooltip, open that file — no code change needed.
36
  # If the file is missing, tooltips are silently disabled (no hover text shown).
37
  try:
38
+ tooltips_xl = pd.read_excel("data/tooltips.xlsx", sheet_name=None)
39
+ df_res = tooltips_xl.get("Resources", pd.DataFrame(columns=["Resource", "Tooltip"]))
40
+ df_met = tooltips_xl.get("Methods", pd.DataFrame(columns=["Method", "Tooltip"]))
41
  resource_tooltips: dict[str, str] = {
42
  row["Resource"]: row["Tooltip"]
43
+ for _, row in df_res.iterrows()
44
  if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
45
  }
46
  method_tooltips: dict[str, str] = {
47
  row["Method"]: row["Tooltip"]
48
+ for _, row in df_met.iterrows()
49
  if pd.notna(row.get("Tooltip")) and str(row.get("Tooltip", "")).strip()
50
  }
51
  except FileNotFoundError:
tabs/tab1_inputs.py CHANGED
@@ -279,7 +279,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
279
  if st.button("Cancel", width='stretch', key="btn-reset-cancel"):
280
  st.rerun()
281
 
282
- def _validate():
283
  errors = []
284
  if not batch_name.strip():
285
  errors.append("Resource name is required.")
@@ -303,7 +303,7 @@ def dialog_add_custom_resource(cost_sliders, edit_index: int | None = None, pres
303
 
304
  with col_save:
305
  if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key="btn-new-add"):
306
- errors = _validate()
307
  if errors:
308
  with error_placeholder.container():
309
  for e in errors:
@@ -356,7 +356,7 @@ def to_excel_bytes(batches: list) -> bytes:
356
  return excel_buffer.getvalue()
357
 
358
 
359
- def _parse_custom_excel(file) -> list:
360
  """Parse a custom resource Excel file into a list of batch dicts.
361
 
362
  Expects columns: name, group, amount, unit, method, min, median, max.
@@ -417,7 +417,7 @@ def dialog_load_custom_batch():
417
  return
418
 
419
  try:
420
- new_batches = _parse_custom_excel(uploaded)
421
  except Exception as e:
422
  st.error(f"Error reading file: {e}")
423
  return
@@ -460,7 +460,7 @@ def dialog_load_custom_batch():
460
 
461
 
462
 
463
- def _render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
464
  """Render a single custom batch inside its resource-group expander."""
465
  n_methods = len(batch["methods"])
466
  amount_val = float(batch["amount"])
@@ -733,7 +733,7 @@ def render_tab(cost_sliders):
733
  if resource_to_group.get(b["group"]) == group
734
  ]
735
  for global_idx, batch in group_batches:
736
- _render_custom_batch_inline(global_idx, batch, cost_sliders, group_unit)
737
 
738
  _, col_button = st.columns([3,1])
739
  with col_button:
 
279
  if st.button("Cancel", width='stretch', key="btn-reset-cancel"):
280
  st.rerun()
281
 
282
+ def validate():
283
  errors = []
284
  if not batch_name.strip():
285
  errors.append("Resource name is required.")
 
303
 
304
  with col_save:
305
  if st.button("Add to session", type="tertiary", width='stretch', icon=":material/check:", key="btn-new-add"):
306
+ errors = validate()
307
  if errors:
308
  with error_placeholder.container():
309
  for e in errors:
 
356
  return excel_buffer.getvalue()
357
 
358
 
359
+ def parse_custom_excel(file) -> list:
360
  """Parse a custom resource Excel file into a list of batch dicts.
361
 
362
  Expects columns: name, group, amount, unit, method, min, median, max.
 
417
  return
418
 
419
  try:
420
+ new_batches = parse_custom_excel(uploaded)
421
  except Exception as e:
422
  st.error(f"Error reading file: {e}")
423
  return
 
460
 
461
 
462
 
463
+ def render_custom_batch_inline(global_idx, batch, cost_sliders, unit):
464
  """Render a single custom batch inside its resource-group expander."""
465
  n_methods = len(batch["methods"])
466
  amount_val = float(batch["amount"])
 
733
  if resource_to_group.get(b["group"]) == group
734
  ]
735
  for global_idx, batch in group_batches:
736
+ render_custom_batch_inline(global_idx, batch, cost_sliders, group_unit)
737
 
738
  _, col_button = st.columns([3,1])
739
  with col_button:
tabs/tab4_optimization.py CHANGED
@@ -16,12 +16,12 @@ from utils.ui_helpers import apply_chart_style, register_chart_data, show_bar
16
  from utils.data_utils import is_valid_scenario, is_complete_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix, get_resource_to_group, get_resource_to_unit
17
 
18
 
19
- def _to_json_safe(obj):
20
  """Recursively convert numpy/pandas types to plain Python so json.dumps works."""
21
  if isinstance(obj, dict):
22
- return {k: _to_json_safe(v) for k, v in obj.items()}
23
  if isinstance(obj, list):
24
- return [_to_json_safe(v) for v in obj]
25
  if isinstance(obj, pd.Series):
26
  return obj.iloc[0] if len(obj) == 1 else obj.tolist()
27
  if isinstance(obj, pd.DataFrame):
@@ -114,7 +114,7 @@ def parse_zip_scenario(uploaded) -> dict | None:
114
  with zipfile.ZipFile(io.BytesIO(uploaded.read())) as zf:
115
  names = zf.namelist()
116
 
117
- def _read(fname):
118
  return pd.read_csv(io.BytesIO(zf.read(fname)), index_col=0)
119
 
120
  required = {"resource_caps.csv", "method_constraints.csv",
@@ -122,10 +122,10 @@ def parse_zip_scenario(uploaded) -> dict | None:
122
  if not required.issubset(names):
123
  return None
124
 
125
- resource_caps_raw = _read("resource_caps.csv")["Available Amount"].to_dict()
126
  resource_caps = {strip_unit_suffix(k): v for k, v in resource_caps_raw.items()}
127
 
128
- df_c = _read("method_constraints.csv")
129
 
130
  # Compatibility with older files that used `max_share`
131
  if "cap_type (percent or absolute)" in df_c.columns:
@@ -147,11 +147,11 @@ def parse_zip_scenario(uploaded) -> dict | None:
147
  for m, row in df_c.iterrows()
148
  }
149
 
150
- df_costs = _read("method_costs.csv") # index=method, columns=resources
151
 
152
  costs = {m: row.dropna().to_dict() for m, row in df_costs.iterrows()}
153
 
154
- df = _read("allocations.csv")
155
  col = f"{CO2_UNIT} removed"
156
  if col not in df.columns:
157
  old_col = "tCO₂ removed"
@@ -550,7 +550,6 @@ def show_heatmap(df_result, resource_caps, method_costs, custom_resources=None,
550
  full_caps.setdefault(batch["name"], float(batch["amount"]))
551
 
552
  standard_available = [r for r in resource_order if full_caps.get(r, 0) > 0]
553
- custom_names = {b["name"] for b in (custom_resources or [])}
554
  extra_available = [b["name"] for b in (custom_resources or []) if float(b["amount"]) > 0]
555
  available = standard_available + extra_available
556
 
@@ -632,10 +631,10 @@ def show_absolute_usage(allocations, method_costs, custom_resources=None, resour
632
  for m, resources in method_costs.items()
633
  }
634
  # Build resource_units from standard groups
635
- _resource_units = defaultdict(list)
636
  resource_to_group = get_resource_to_group()
637
  for cat, resources in resource_groups.items():
638
- _resource_units[resource_unit_map[cat]].extend(resources)
639
 
640
  # Inject custom resource batches using the correct unit from their reference group
641
  custom_batch_names = set()
@@ -643,10 +642,10 @@ def show_absolute_usage(allocations, method_costs, custom_resources=None, resour
643
  custom_batch_names.add(batch["name"])
644
  group_name = resource_to_group.get(batch["group"])
645
  correct_unit = resource_unit_map.get(group_name, "?")
646
- if batch["name"] not in _resource_units[correct_unit]:
647
- _resource_units[correct_unit].append(batch["name"])
648
 
649
- local_resource_units = dict(_resource_units)
650
 
651
  rows = []
652
  for m, co2 in allocations.items():
@@ -838,7 +837,7 @@ def export_scenario():
838
  col_json, col_csv = st.columns(2)
839
  with col_json:
840
  st.download_button(
841
- "Export JSON", data=json.dumps(_to_json_safe(scenario), indent=2),
842
  file_name=f"{safe}.json", mime="application/json",
843
  width='stretch',
844
  icon=":material/download:"
@@ -1013,7 +1012,7 @@ def export_inputs_only():
1013
 
1014
  enabled_methods = st.session_state.get("enabled_methods", {})
1015
 
1016
- def _is_allowed(method, resource):
1017
  return enabled_methods.get(resource, {}).get(method, True)
1018
 
1019
  wb = openpyxl.Workbook()
@@ -1035,7 +1034,7 @@ def export_inputs_only():
1035
  ws3.append(["method", "resource", "value", "secondary"])
1036
  for m, resources in st.session_state.costs.items():
1037
  for r, v in resources.items():
1038
- value = 0.0 if not _is_allowed(m, r) else v
1039
  ws3.append([m, r, value, (m, r) in secondary_resources])
1040
 
1041
  ws4 = wb.create_sheet("custom_resources")
 
16
  from utils.data_utils import is_valid_scenario, is_complete_scenario, load_inputs_from_excel, neutralize_zero_coefficient_methods, strip_unit_suffix, get_resource_to_group, get_resource_to_unit
17
 
18
 
19
+ def to_json_safe(obj):
20
  """Recursively convert numpy/pandas types to plain Python so json.dumps works."""
21
  if isinstance(obj, dict):
22
+ return {k: to_json_safe(v) for k, v in obj.items()}
23
  if isinstance(obj, list):
24
+ return [to_json_safe(v) for v in obj]
25
  if isinstance(obj, pd.Series):
26
  return obj.iloc[0] if len(obj) == 1 else obj.tolist()
27
  if isinstance(obj, pd.DataFrame):
 
114
  with zipfile.ZipFile(io.BytesIO(uploaded.read())) as zf:
115
  names = zf.namelist()
116
 
117
+ def read(fname):
118
  return pd.read_csv(io.BytesIO(zf.read(fname)), index_col=0)
119
 
120
  required = {"resource_caps.csv", "method_constraints.csv",
 
122
  if not required.issubset(names):
123
  return None
124
 
125
+ resource_caps_raw = read("resource_caps.csv")["Available Amount"].to_dict()
126
  resource_caps = {strip_unit_suffix(k): v for k, v in resource_caps_raw.items()}
127
 
128
+ df_c = read("method_constraints.csv")
129
 
130
  # Compatibility with older files that used `max_share`
131
  if "cap_type (percent or absolute)" in df_c.columns:
 
147
  for m, row in df_c.iterrows()
148
  }
149
 
150
+ df_costs = read("method_costs.csv") # index=method, columns=resources
151
 
152
  costs = {m: row.dropna().to_dict() for m, row in df_costs.iterrows()}
153
 
154
+ df = read("allocations.csv")
155
  col = f"{CO2_UNIT} removed"
156
  if col not in df.columns:
157
  old_col = "tCO₂ removed"
 
550
  full_caps.setdefault(batch["name"], float(batch["amount"]))
551
 
552
  standard_available = [r for r in resource_order if full_caps.get(r, 0) > 0]
 
553
  extra_available = [b["name"] for b in (custom_resources or []) if float(b["amount"]) > 0]
554
  available = standard_available + extra_available
555
 
 
631
  for m, resources in method_costs.items()
632
  }
633
  # Build resource_units from standard groups
634
+ resource_units = defaultdict(list)
635
  resource_to_group = get_resource_to_group()
636
  for cat, resources in resource_groups.items():
637
+ resource_units[resource_unit_map[cat]].extend(resources)
638
 
639
  # Inject custom resource batches using the correct unit from their reference group
640
  custom_batch_names = set()
 
642
  custom_batch_names.add(batch["name"])
643
  group_name = resource_to_group.get(batch["group"])
644
  correct_unit = resource_unit_map.get(group_name, "?")
645
+ if batch["name"] not in resource_units[correct_unit]:
646
+ resource_units[correct_unit].append(batch["name"])
647
 
648
+ local_resource_units = dict(resource_units)
649
 
650
  rows = []
651
  for m, co2 in allocations.items():
 
837
  col_json, col_csv = st.columns(2)
838
  with col_json:
839
  st.download_button(
840
+ "Export JSON", data=json.dumps(to_json_safe(scenario), indent=2),
841
  file_name=f"{safe}.json", mime="application/json",
842
  width='stretch',
843
  icon=":material/download:"
 
1012
 
1013
  enabled_methods = st.session_state.get("enabled_methods", {})
1014
 
1015
+ def is_allowed(method, resource):
1016
  return enabled_methods.get(resource, {}).get(method, True)
1017
 
1018
  wb = openpyxl.Workbook()
 
1034
  ws3.append(["method", "resource", "value", "secondary"])
1035
  for m, resources in st.session_state.costs.items():
1036
  for r, v in resources.items():
1037
+ value = 0.0 if not is_allowed(m, r) else v
1038
  ws3.append([m, r, value, (m, r) in secondary_resources])
1039
 
1040
  ws4 = wb.create_sheet("custom_resources")
tabs/tab6_scenarios.py CHANGED
@@ -42,7 +42,7 @@ def panel_load_scenario():
42
  except Exception as e:
43
  st.error(f"Error loading {uploaded_file.name}: {e}")
44
 
45
- def _prepare_comparison_data(selected):
46
  """Build comparison DataFrames from the currently selected scenarios.
47
 
48
  Args:
@@ -101,7 +101,7 @@ def _prepare_comparison_data(selected):
101
  )
102
 
103
 
104
- def _show_method_allocations(df):
105
  """Render a grouped bar chart of method allocations across scenarios.
106
 
107
  Args:
@@ -120,7 +120,7 @@ def _show_method_allocations(df):
120
 
121
 
122
 
123
- def _show_resource_usage(df):
124
  """Render grouped bar charts of resource usage per method and total per scenario.
125
 
126
  Args:
@@ -184,7 +184,7 @@ def render_tab():
184
  return
185
 
186
  # Comparison
187
- df_summary, df_methods, df_resources = _prepare_comparison_data(selected)
188
 
189
  st.markdown("""<p class="crra-sub-heading">Summary Table</p>""", unsafe_allow_html=True)
190
  st.dataframe(df_summary.style.format(na_rep="-", thousands=","), width='stretch')
@@ -195,5 +195,5 @@ def render_tab():
195
  icon=":material/download:"
196
  )
197
 
198
- _show_method_allocations(df_methods)
199
- _show_resource_usage(df_resources)
 
42
  except Exception as e:
43
  st.error(f"Error loading {uploaded_file.name}: {e}")
44
 
45
+ def prepare_comparison_data(selected):
46
  """Build comparison DataFrames from the currently selected scenarios.
47
 
48
  Args:
 
101
  )
102
 
103
 
104
+ def show_method_allocations(df):
105
  """Render a grouped bar chart of method allocations across scenarios.
106
 
107
  Args:
 
120
 
121
 
122
 
123
+ def show_resource_usage(df):
124
  """Render grouped bar charts of resource usage per method and total per scenario.
125
 
126
  Args:
 
184
  return
185
 
186
  # Comparison
187
+ df_summary, df_methods, df_resources = prepare_comparison_data(selected)
188
 
189
  st.markdown("""<p class="crra-sub-heading">Summary Table</p>""", unsafe_allow_html=True)
190
  st.dataframe(df_summary.style.format(na_rep="-", thousands=","), width='stretch')
 
195
  icon=":material/download:"
196
  )
197
 
198
+ show_method_allocations(df_methods)
199
+ show_resource_usage(df_resources)