CarlyneB commited on
Commit
43c01c6
·
1 Parent(s): 3b61151

[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

Files changed (1) hide show
  1. 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
- resource_used_total = {r: sum(v.values()) for r, v in resource_usage.items()}
 
 
 
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: all_caps.get(r, 0.0) - resource_used_total.get(r, 0.0)
343
- for r in all_caps
 
 
 
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
  )