Spaces:
Running
Running
[FIX] Optimization: harden LP robustness + clip floating-point residuals
Browse files- Clip resource_used_total to 10 decimal places and resource_remaining to 0
when within 1e-9 of zero, eliminating LP solver noise in exported values
- Guard against None resource_caps input
- Clamp negative absolute method caps to 0 to avoid infeasible LP
- Deactivate methods whose effective coefficients are all zero to prevent
unbounded LP variables
- Return zero result early when no resource caps are defined at all
- optimization/optimization.py +53 -3
optimization/optimization.py
CHANGED
|
@@ -333,14 +333,20 @@ def extract_resource_usage(
|
|
| 333 |
if used > 1e-9:
|
| 334 |
resource_usage[batch_name][m] = float(used)
|
| 335 |
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
| 337 |
all_caps = {
|
| 338 |
**resource_caps,
|
| 339 |
**{batch_name: specs["amount"] for batch_name, specs in custom_batch_specs.items()},
|
| 340 |
}
|
| 341 |
resource_remaining = {
|
| 342 |
-
r:
|
| 343 |
-
for r in
|
|
|
|
|
|
|
|
|
|
| 344 |
}
|
| 345 |
return resource_usage, resource_used_total, resource_remaining
|
| 346 |
|
|
@@ -418,19 +424,63 @@ def run_optimization(
|
|
| 418 |
resource_actual_gain (dict).
|
| 419 |
(False, {"message": str}) on failure.
|
| 420 |
"""
|
|
|
|
|
|
|
|
|
|
| 421 |
resource_caps = dict(resource_caps)
|
| 422 |
method_costs = apply_enabled_methods(method_costs, enabled_methods)
|
| 423 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
custom_batch_specs, group_to_custom_batches, custom_batch_names = build_custom_batch_structures(
|
| 425 |
method_costs, resource_caps, custom_resources
|
| 426 |
)
|
| 427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
active_methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
|
| 429 |
num_methods = len(active_methods)
|
| 430 |
method_to_index = {m: j for j, m in enumerate(active_methods)}
|
| 431 |
if not num_methods:
|
| 432 |
return False, {"message": "No active methods."}
|
| 433 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 434 |
custom_usage_pairs, custom_pair_to_index, num_lp_vars = build_lp_indices(
|
| 435 |
active_methods, num_methods, custom_batch_specs, resource_caps, method_costs
|
| 436 |
)
|
|
|
|
| 333 |
if used > 1e-9:
|
| 334 |
resource_usage[batch_name][m] = float(used)
|
| 335 |
|
| 336 |
+
_EPS = 1e-9
|
| 337 |
+
resource_used_total = {
|
| 338 |
+
r: round(sum(v.values()), 10) for r, v in resource_usage.items()
|
| 339 |
+
}
|
| 340 |
all_caps = {
|
| 341 |
**resource_caps,
|
| 342 |
**{batch_name: specs["amount"] for batch_name, specs in custom_batch_specs.items()},
|
| 343 |
}
|
| 344 |
resource_remaining = {
|
| 345 |
+
r: max(0.0, v) if abs(v) >= _EPS else 0.0
|
| 346 |
+
for r, v in {
|
| 347 |
+
r: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
|
| 348 |
+
for r in all_caps
|
| 349 |
+
}.items()
|
| 350 |
}
|
| 351 |
return resource_usage, resource_used_total, resource_remaining
|
| 352 |
|
|
|
|
| 424 |
resource_actual_gain (dict).
|
| 425 |
(False, {"message": str}) on failure.
|
| 426 |
"""
|
| 427 |
+
if resource_caps is None:
|
| 428 |
+
return False, {"message": "resource_caps cannot be None."}
|
| 429 |
+
|
| 430 |
resource_caps = dict(resource_caps)
|
| 431 |
method_costs = apply_enabled_methods(method_costs, enabled_methods)
|
| 432 |
|
| 433 |
+
# Clamp negative absolute caps to 0 to avoid infeasible LP.
|
| 434 |
+
for m, c in method_constraints.items():
|
| 435 |
+
if c.get("cap_type") == "absolute" and c.get("cap_value", 0) < 0:
|
| 436 |
+
method_constraints = {
|
| 437 |
+
**method_constraints,
|
| 438 |
+
m: {**c, "cap_value": 0.0},
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
custom_batch_specs, group_to_custom_batches, custom_batch_names = build_custom_batch_structures(
|
| 442 |
method_costs, resource_caps, custom_resources
|
| 443 |
)
|
| 444 |
|
| 445 |
+
# Deactivate methods whose effective coefficients are all zero — they would
|
| 446 |
+
# otherwise create unconstrained LP variables and cause an unbounded problem.
|
| 447 |
+
# Runs AFTER build_custom_batch_structures so that batch coefficients injected
|
| 448 |
+
# into method_costs are visible here. Without this ordering, a method that relies
|
| 449 |
+
# solely on a custom batch (standard pool blocked via enabled_methods) would be
|
| 450 |
+
# wrongly deactivated because its standard coefficients all appear zero.
|
| 451 |
+
# Iterate over method_costs (not just method_constraints) to catch methods
|
| 452 |
+
# that are active by default (not present in method_constraints at all).
|
| 453 |
+
safe = {m: c.copy() for m, c in method_constraints.items()}
|
| 454 |
+
for m in method_costs:
|
| 455 |
+
c = safe.get(m, {"active": True, "cap_type": "percent", "cap_value": 100})
|
| 456 |
+
if not c.get("active", True):
|
| 457 |
+
continue
|
| 458 |
+
coeffs = method_costs.get(m, {})
|
| 459 |
+
if all(v == 0 for v in coeffs.values()) if coeffs else True:
|
| 460 |
+
safe[m] = {**c, "active": False}
|
| 461 |
+
method_constraints = safe
|
| 462 |
+
|
| 463 |
active_methods = [m for m in method_costs if method_constraints.get(m, {}).get("active", True)]
|
| 464 |
num_methods = len(active_methods)
|
| 465 |
method_to_index = {m: j for j, m in enumerate(active_methods)}
|
| 466 |
if not num_methods:
|
| 467 |
return False, {"message": "No active methods."}
|
| 468 |
|
| 469 |
+
# If there are no resource caps at all (not even custom batches), the LP has no
|
| 470 |
+
# upper bound on y_m — only percent-type method caps apply, which are relative and
|
| 471 |
+
# don't fix the scale. Return zero rather than letting HiGHS report "unbounded".
|
| 472 |
+
all_caps = {**resource_caps, **{b["name"]: float(b.get("amount", 0)) for b in (custom_resources or [])}}
|
| 473 |
+
if not all_caps:
|
| 474 |
+
empty_result = {
|
| 475 |
+
"total_removed": 0.0,
|
| 476 |
+
"method_usage": {},
|
| 477 |
+
"resource_usage": {},
|
| 478 |
+
"resource_used_total": {},
|
| 479 |
+
"resource_remaining": {},
|
| 480 |
+
"resource_actual_gain": {},
|
| 481 |
+
}
|
| 482 |
+
return True, empty_result
|
| 483 |
+
|
| 484 |
custom_usage_pairs, custom_pair_to_index, num_lp_vars = build_lp_indices(
|
| 485 |
active_methods, num_methods, custom_batch_specs, resource_caps, method_costs
|
| 486 |
)
|