Spaces:
Sleeping
Sleeping
[FEAT] optimization: add tiebreak pass to prefer the more efficient resource in standard/custom pooling pairs
Browse files- 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,
|
| 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,
|