CarlyneB commited on
Commit
e391f61
·
1 Parent(s): fd0b601

[FEAT] utils: add run_expanded_optimization (LP pipeline for tab5), add resource_caps_to_excel_bytes, increase number precision in render_resource_input

Browse files
Files changed (2) hide show
  1. utils/data_utils.py +153 -0
  2. utils/ui_helpers.py +2 -2
utils/data_utils.py CHANGED
@@ -7,7 +7,9 @@ import streamlit as st
7
  from config.config import (
8
  resource_unit_map, resource_order, method_order,
9
  resource_groups, method_groups, secondary_resources, CO2_UNIT,
 
10
  )
 
11
 
12
  def get_resource_to_group() -> dict[str, str]:
13
  """Map each resource name to its resource group.
@@ -243,6 +245,129 @@ def make_variant_name(method_name: str, batch_name: str) -> str:
243
  return f"{method_name}: {batch_name}"
244
 
245
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
246
  # ---------------------------------------------------------------------------
247
  # Resource / Excel helpers (moved from tab1)
248
 
@@ -251,6 +376,34 @@ def standard_resource_names() -> set:
251
  return {r for rlist in resource_groups.values() for r in rlist}
252
 
253
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  def to_excel_bytes(batches: list) -> bytes:
255
  """Serialise all custom resource batches to an Excel file (one row per batch × method).
256
 
 
7
  from config.config import (
8
  resource_unit_map, resource_order, method_order,
9
  resource_groups, method_groups, secondary_resources, CO2_UNIT,
10
+ hidden_resource_names,
11
  )
12
+ from optimization.optimization import run_optimization
13
 
14
  def get_resource_to_group() -> dict[str, str]:
15
  """Map each resource name to its resource group.
 
245
  return f"{method_name}: {batch_name}"
246
 
247
 
248
+ def run_expanded_optimization(resource_caps, method_constraints, costs, custom_resources, enabled_methods):
249
+ """Apply the full Tab 4 preprocessing pipeline, then run the LP.
250
+
251
+ This is the same pipeline as tab4_optimization.run_and_store_result: apply
252
+ enabled_methods overrides, inject custom-batch coefficients, neutralise
253
+ all-zero methods, deactivate methods missing a required resource, and —
254
+ crucially — expand any "Other"-tagged method into one independent method
255
+ variant per assigned custom batch (each variant gets the batch as its own
256
+ standard-style resource, with its own cap, instead of going through the
257
+ z-variable pooling/substitution mechanism).
258
+
259
+ Any caller that needs LP results consistent with what Tab 4 actually shows
260
+ (e.g. Tab 5's sensitivity sweeps) should use this instead of calling
261
+ run_optimization() directly with raw session-state costs — otherwise a
262
+ custom resource attached to an "Other" method would be silently computed
263
+ through the wrong mechanism and never show up as its own entry.
264
+
265
+ Args:
266
+ resource_caps (dict): {resource: available amount} — may be a perturbed
267
+ copy of st.session_state.resource_caps.
268
+ method_constraints (dict): {method: {active, cap_type, cap_value}} — may
269
+ be a perturbed copy of st.session_state.constraints.
270
+ costs (dict): {method: {resource: coefficient}}, raw/unexpanded —
271
+ normally st.session_state.costs.
272
+ custom_resources (list): custom batch dicts.
273
+ enabled_methods (dict): {resource: {method: bool}}.
274
+
275
+ Returns:
276
+ tuple[bool, dict]: same shape as optimization.run_optimization()'s
277
+ return value — (True, result_dict) or (False, {"message": str}).
278
+ """
279
+ applied_costs = {m: dict(v) for m, v in costs.items()}
280
+ for method, method_resources in applied_costs.items():
281
+ for resource in list(method_resources):
282
+ if not enabled_methods.get(resource, {}).get(method, True):
283
+ applied_costs[method][resource] = 0.0
284
+
285
+ # Inject custom batch coefficients so neutralize does not wrongly deactivate
286
+ # a method that only has custom-batch access.
287
+ for batch in custom_resources:
288
+ batch_name = batch["name"]
289
+ for method_name, coefficients in batch.get("methods", {}).items():
290
+ if method_name in applied_costs and applied_costs[method_name].get(batch_name, 0.0) == 0.0:
291
+ applied_costs[method_name][batch_name] = coefficients.get("median", 0.0)
292
+
293
+ standard_constraints = {
294
+ m: c for m, c in method_constraints.items()
295
+ if m in applied_costs
296
+ }
297
+ safe_constraints = neutralize_zero_coefficient_methods(applied_costs, standard_constraints)
298
+
299
+ # Deactivate any method that requires a resource not provided (standard cap
300
+ # nor custom batch covers it).
301
+ custom_batch_names_pre = {b["name"] for b in custom_resources}
302
+ available_resources = set(resource_caps) | custom_batch_names_pre
303
+ for m, m_costs in applied_costs.items():
304
+ if not safe_constraints.get(m, {}).get("active", True):
305
+ continue
306
+ missing = [
307
+ r for r, c in m_costs.items()
308
+ if c > 0 and r not in available_resources
309
+ ]
310
+ if missing:
311
+ safe_constraints.setdefault(m, default_constraint())
312
+ safe_constraints[m] = {**safe_constraints[m], "active": False}
313
+
314
+ # Expand "Other" methods: replace each with one variant per assigned batch.
315
+ resource_caps_for_lp = dict(resource_caps)
316
+ expanded_batch_names: set[str] = set()
317
+
318
+ for m in list(applied_costs.keys()):
319
+ if not is_other_method(m):
320
+ continue
321
+ batches_for_m = get_other_method_variants(m, custom_resources)
322
+ if not batches_for_m:
323
+ continue
324
+ all_batch_names_for_m = {b["name"] for b in batches_for_m}
325
+
326
+ for batch in batches_for_m:
327
+ vname = make_variant_name(m, batch["name"])
328
+ variant_costs = {
329
+ r: coeff for r, coeff in applied_costs[m].items()
330
+ if r not in hidden_resource_names and r not in all_batch_names_for_m
331
+ }
332
+ variant_costs[batch["name"]] = applied_costs[m].get(
333
+ batch["name"],
334
+ batch["methods"][m]["median"],
335
+ )
336
+ applied_costs[vname] = variant_costs
337
+ safe_constraints[vname] = (
338
+ method_constraints.get(vname) or default_constraint()
339
+ )
340
+ resource_caps_for_lp[batch["name"]] = float(batch["amount"])
341
+ expanded_batch_names.add(batch["name"])
342
+ del applied_costs[m]
343
+ del safe_constraints[m]
344
+
345
+ custom_resources_for_lp = [b for b in custom_resources if b["name"] not in expanded_batch_names]
346
+
347
+ # Non-"Other" methods that share an expanded batch lose their z-variable
348
+ # once the batch is removed from custom_resources_for_lp — inject it as a
349
+ # standard resource coefficient so the constraint still applies to them.
350
+ for batch in custom_resources:
351
+ if batch["name"] not in expanded_batch_names:
352
+ continue
353
+ batch_group = batch.get("group", "")
354
+ group_cap_is_zero = resource_caps_for_lp.get(batch_group, 0.0) == 0.0
355
+ for method_name, coeff_dict in batch.get("methods", {}).items():
356
+ if method_name not in applied_costs:
357
+ continue
358
+ applied_costs[method_name][batch["name"]] = coeff_dict.get("median", 0.0)
359
+ if group_cap_is_zero and batch_group in applied_costs[method_name]:
360
+ applied_costs[method_name][batch_group] = 0.0
361
+
362
+ return run_optimization(
363
+ resource_caps=resource_caps_for_lp,
364
+ method_constraints=safe_constraints,
365
+ method_costs=applied_costs,
366
+ custom_resources=custom_resources_for_lp,
367
+ enabled_methods=enabled_methods,
368
+ )
369
+
370
+
371
  # ---------------------------------------------------------------------------
372
  # Resource / Excel helpers (moved from tab1)
373
 
 
376
  return {r for rlist in resource_groups.values() for r in rlist}
377
 
378
 
379
+ def resource_caps_to_excel_bytes(resource_caps: dict) -> bytes:
380
+ """Serialise the current standard resource availabilities to an Excel file.
381
+
382
+ Same "Resource" / "Unit" / "Available Amount" columns as the blank template
383
+ (data/resource_template.xlsx), so the exported file can be re-uploaded as-is.
384
+
385
+ Args:
386
+ resource_caps (dict): {resource: available amount}.
387
+
388
+ Returns:
389
+ bytes: raw bytes of the .xlsx workbook.
390
+ """
391
+ resource_to_unit = get_resource_to_unit()
392
+ rows = [
393
+ {
394
+ "Resource": r,
395
+ "Unit": resource_to_unit.get(r, ""),
396
+ "Available Amount": float(amount),
397
+ }
398
+ for r, amount in resource_caps.items()
399
+ if r in resource_order
400
+ ]
401
+ excel_buffer = BytesIO()
402
+ with pd.ExcelWriter(excel_buffer, engine="openpyxl") as writer:
403
+ pd.DataFrame(rows).to_excel(writer, index=False, sheet_name="resource_caps")
404
+ return excel_buffer.getvalue()
405
+
406
+
407
  def to_excel_bytes(batches: list) -> bytes:
408
  """Serialise all custom resource batches to an Excel file (one row per batch × method).
409
 
utils/ui_helpers.py CHANGED
@@ -25,9 +25,9 @@ def render_resource_input(name_clean, r, tooltip=None):
25
  cap = st.number_input(
26
  label=name_clean,
27
  min_value=0.0,
28
- format="%.2f",
29
  value=None,
30
- placeholder="0.00",
31
  key=widget_key,
32
  help=tooltip,
33
  )
 
25
  cap = st.number_input(
26
  label=name_clean,
27
  min_value=0.0,
28
+ format="%.10g",
29
  value=None,
30
+ placeholder="0.0",
31
  key=widget_key,
32
  help=tooltip,
33
  )