Spaces:
Running
Running
[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- utils/data_utils.py +153 -0
- 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="%.
|
| 29 |
value=None,
|
| 30 |
-
placeholder="0.
|
| 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 |
)
|