CarlyneB commited on
Commit
f666cf8
·
1 Parent(s): af9a933

[FEAT] optimization: add tiebreak pass to prefer the more efficient resource in standard/custom pooling pairs

Browse files
Files changed (1) hide show
  1. optimization/optimization.py +66 -1
optimization/optimization.py CHANGED
@@ -357,6 +357,65 @@ def extract_resource_usage(
357
  return resource_usage, resource_used_total, resource_remaining
358
 
359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  def run_perturbation(constraint_labels, A_ub, b_ub, objective_coefficients, bounds, num_methods, total_removed):
361
  """Compute marginal CDR gain per unit for each binding resource constraint.
362
 
@@ -524,8 +583,14 @@ def run_optimization(
524
  for j in range(num_methods) if method_outputs[j] > 1e-9
525
  }
526
 
 
 
 
 
 
 
527
  resource_usage, resource_used_total, resource_remaining = extract_resource_usage(
528
- active_methods, num_methods, method_outputs, lp_solution, method_costs,
529
  resource_caps, custom_batch_specs, group_to_custom_batches,
530
  custom_pair_to_index, standard_resources,
531
  resources_no_batches, resources_with_batches, custom_batch_names,
 
357
  return resource_usage, resource_used_total, resource_remaining
358
 
359
 
360
+ def resolve_pooling_tiebreak(
361
+ num_methods, num_lp_vars, method_outputs, custom_usage_pairs,
362
+ custom_pair_to_index, custom_batch_specs, method_costs, A_ub, b_ub, bounds,
363
+ ):
364
+ """Re-solve the LP with each method's output pinned, preferring the more
365
+ efficient resource within each standard/custom pooling pair.
366
+
367
+ The main LP (run_optimization) maximises total CO2 removed only — nothing
368
+ in its objective distinguishes a standard resource from a custom batch
369
+ that substitutes for it, so when both could satisfy the same demand the
370
+ solver picks an arbitrary split. This second pass fixes that: it pins
371
+ every active method's output y_m to its pass-1 value (so total_removed and
372
+ method_usage never change) and minimises Σ (c_n - c_s) · z_{m,n} across all
373
+ (method, batch) pooling pairs — i.e. it prefers fully using whichever
374
+ resource (standard or custom) has the lower coefficient (more MtCO2
375
+ removed per unit of resource) before drawing on the less efficient one.
376
+
377
+ Args:
378
+ num_methods (int): number of y_m variables.
379
+ num_lp_vars (int): total LP variable count (pass-1 sizing).
380
+ method_outputs (np.ndarray): y_m values from the pass-1 solution.
381
+ custom_usage_pairs (list): [(method, batch_name)] with non-zero coefficient.
382
+ custom_pair_to_index (dict): {(method, batch_name): lp_column_index}.
383
+ custom_batch_specs (dict): {batch_name: {group, amount, primary_methods}}.
384
+ method_costs (dict): {method: {resource: coefficient}}.
385
+ A_ub, b_ub: pass-1 constraint matrix/vector (reused unchanged — pass-1's
386
+ own solution already satisfies them, so this second LP is always feasible).
387
+ bounds: pass-1 variable bounds (reused unchanged).
388
+
389
+ Returns:
390
+ np.ndarray | None: the tie-broken solution vector, or None if there are
391
+ no pooling pairs to break ties on, or if the second solve fails
392
+ (in which case the caller should fall back to the pass-1 solution).
393
+ """
394
+ if not custom_usage_pairs:
395
+ return None
396
+
397
+ A_eq = np.zeros((num_methods, num_lp_vars))
398
+ for j in range(num_methods):
399
+ A_eq[j, j] = 1.0
400
+ b_eq = np.array(method_outputs, dtype=float)
401
+
402
+ objective2 = np.zeros(num_lp_vars)
403
+ for (m, batch_name) in custom_usage_pairs:
404
+ ref = custom_batch_specs[batch_name]["group"]
405
+ c_s = method_costs.get(m, {}).get(ref, 0.0)
406
+ c_n = method_costs.get(m, {}).get(batch_name, 0.0)
407
+ objective2[custom_pair_to_index[(m, batch_name)]] = c_n - c_s
408
+
409
+ try:
410
+ result = linprog(
411
+ objective2, A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq,
412
+ bounds=bounds, method="highs",
413
+ )
414
+ except Exception:
415
+ return None
416
+ return result.x if result.success else None
417
+
418
+
419
  def run_perturbation(constraint_labels, A_ub, b_ub, objective_coefficients, bounds, num_methods, total_removed):
420
  """Compute marginal CDR gain per unit for each binding resource constraint.
421
 
 
583
  for j in range(num_methods) if method_outputs[j] > 1e-9
584
  }
585
 
586
+ tiebreak_solution = resolve_pooling_tiebreak(
587
+ num_methods, num_lp_vars, method_outputs, custom_usage_pairs,
588
+ custom_pair_to_index, custom_batch_specs, method_costs, A_ub, b_ub, bounds,
589
+ )
590
+ lp_solution_for_usage = tiebreak_solution if tiebreak_solution is not None else lp_solution
591
+
592
  resource_usage, resource_used_total, resource_remaining = extract_resource_usage(
593
+ active_methods, num_methods, method_outputs, lp_solution_for_usage, method_costs,
594
  resource_caps, custom_batch_specs, group_to_custom_batches,
595
  custom_pair_to_index, standard_resources,
596
  resources_no_batches, resources_with_batches, custom_batch_names,