mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
HiGHS Optimization: - qubo_highs.py: QUBO→MIP reformulation via highspy (exact, not approximate) - solve_route_lp: TSP/VRP assignment relaxation for RouteCost 39-node graph - scale_space_solver.py: multi-scale optimization (coarse LP → fine MIP) - Gaussian kernels in Q16_16, voltage↔scale mapping - alphaproof_loop.py: Ollama → lake build → feedback proof search Lean Formalization: - AdjugateMatrix.lean: division-free matrix inversion (291 lines, 3300 jobs, 0 errors) - det2/det4/det8 via cofactor expansion, all Q16_16 - adjugate, matrixInverse, cayleyTransform - 7 #eval witnesses all pass FPGA (Tang Nano 9K): - voltage_mode_controller.v: 4-mode BRAM (STORE/COMPUTE/APPROX/MORPHIC) - scale_space_bram.v: 4 Gaussian kernel banks (σ=0.25/0.50/0.75/1.00) - highs_pivot_accelerator.v: 3-stage pipeline, Q16_16 division, 64-element columns - blitter_memory_map.v: 8-bit CPU ↔ 32-bit Q16 bridge, full I/O map at $8000
420 lines
13 KiB
Python
420 lines
13 KiB
Python
"""
|
|
QUBO-to-MIP bridge using HiGHS solver.
|
|
|
|
Converts Quadratic Unconstrained Binary Optimization problems into
|
|
Mixed Integer Programming formulations solvable by HiGHS.
|
|
|
|
Falls back to simulated annealing if highspy is not installed.
|
|
"""
|
|
|
|
import math
|
|
import random
|
|
import time
|
|
from typing import Any
|
|
|
|
try:
|
|
import highspy
|
|
import numpy as np
|
|
HAS_HIGHS = True
|
|
except ImportError:
|
|
HAS_HIGHS = False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# QUBO → MIP reformulation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def qubo_to_mip(Q: dict[tuple[int, int], float]):
|
|
"""Convert QUBO matrix Q to MIP formulation.
|
|
|
|
QUBO: min x^T Q x where x in {0,1}^n
|
|
Since x_i^2 = x_i for binary variables:
|
|
min sum_i Q[i,i]*x[i] + sum_{i<j} 2*Q[i,j]*x[i]*x[j]
|
|
|
|
Linearization of bilinear terms y[i,j] = x[i]*x[j]:
|
|
y[i,j] <= x[i]
|
|
y[i,j] <= x[j]
|
|
y[i,j] >= x[i] + x[j] - 1
|
|
|
|
Returns (H, c, sense, rhs) where:
|
|
H: objective coefficients dict {var_index: coeff}
|
|
c: constraint matrix as list of (var_coeffs, sense, rhs)
|
|
sense: list of constraint senses ('<=' or '>=')
|
|
rhs: list of constraint RHS values
|
|
"""
|
|
# Collect all variable indices
|
|
indices = set()
|
|
for i, j in Q:
|
|
indices.add(i)
|
|
indices.add(j)
|
|
if not indices:
|
|
return {}, [], [], []
|
|
|
|
n = max(indices) + 1 # number of x variables
|
|
|
|
# Separate diagonal (linear) and off-diagonal (bilinear) terms
|
|
linear_coeffs: dict[int, float] = {}
|
|
pair_coeffs: dict[tuple[int, int], float] = {}
|
|
|
|
for (i, j), coeff in Q.items():
|
|
if i == j:
|
|
linear_coeffs[i] = linear_coeffs.get(i, 0.0) + coeff
|
|
else:
|
|
key = (min(i, j), max(i, j))
|
|
pair_coeffs[key] = pair_coeffs.get(key, 0.0) + coeff
|
|
|
|
# Build objective: linear part in x, bilinear part in y variables
|
|
# x variables: indices 0..n-1
|
|
# y variables: indices n, n+1, ... (one per pair)
|
|
H: dict[int, float] = {}
|
|
for i, coeff in linear_coeffs.items():
|
|
H[i] = H.get(i, 0.0) + coeff
|
|
|
|
y_index_map: dict[tuple[int, int], int] = {}
|
|
y_idx = n
|
|
for (i, j), coeff in pair_coeffs.items():
|
|
if abs(coeff) < 1e-15:
|
|
continue
|
|
H[y_idx] = coeff
|
|
y_index_map[(i, j)] = y_idx
|
|
y_idx += 1
|
|
|
|
# Build linearization constraints
|
|
constraints = [] # list of (var_coeffs: dict, sense, rhs)
|
|
for (i, j), yi_idx in y_index_map.items():
|
|
# y[i,j] <= x[i] → y[i,j] - x[i] <= 0
|
|
constraints.append(({yi_idx: 1.0, i: -1.0}, '<=', 0.0))
|
|
# y[i,j] <= x[j] → y[i,j] - x[j] <= 0
|
|
constraints.append(({yi_idx: 1.0, j: -1.0}, '<=', 0.0))
|
|
# y[i,j] >= x[i] + x[j] - 1 → -y[i,j] + x[i] + x[j] <= 1
|
|
constraints.append(({yi_idx: -1.0, i: 1.0, j: 1.0}, '<=', 1.0))
|
|
|
|
c = constraints
|
|
sense_list = [s for _, s, _ in c]
|
|
rhs_list = [r for _, _, r in c]
|
|
|
|
return H, c, sense_list, rhs_list
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Simulated annealing fallback
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _sa_solve_qubo(Q: dict[tuple[int, int], float], n: int,
|
|
time_limit: float = 60.0) -> dict:
|
|
"""Simulated annealing fallback for QUBO."""
|
|
if n is None or n == 0:
|
|
return {'solution': [], 'objective': 0.0, 'status': 'empty'}
|
|
|
|
def evaluate(x: list[int]) -> float:
|
|
return sum(Q.get((i, j), 0.0) * x[i] * x[j]
|
|
for i in range(n) for j in range(n) if (i, j) in Q)
|
|
|
|
# Initialize random solution
|
|
current = [random.randint(0, 1) for _ in range(n)]
|
|
current_val = evaluate(current)
|
|
best = current[:]
|
|
best_val = current_val
|
|
|
|
T = 10.0
|
|
T_min = 1e-8
|
|
alpha = 0.9995
|
|
start = time.time()
|
|
|
|
while T > T_min and (time.time() - start) < time_limit:
|
|
# Flip a random bit
|
|
idx = random.randint(0, n - 1)
|
|
current[idx] = 1 - current[idx]
|
|
new_val = evaluate(current)
|
|
|
|
delta = new_val - current_val
|
|
if delta < 0 or random.random() < math.exp(-delta / max(T, 1e-30)):
|
|
current_val = new_val
|
|
if current_val < best_val:
|
|
best = current[:]
|
|
best_val = current_val
|
|
else:
|
|
current[idx] = 1 - current[idx] # revert
|
|
|
|
T *= alpha
|
|
|
|
return {'solution': best, 'objective': best_val, 'status': 'SA_heuristic'}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# HiGHS-based QUBO solver
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def solve_qubo_highs(Q: dict[tuple[int, int], float],
|
|
time_limit: float = 60.0) -> dict:
|
|
"""Solve QUBO using HiGHS MIP solver.
|
|
|
|
Returns: {'solution': list[int], 'objective': float, 'status': str}
|
|
"""
|
|
indices = set()
|
|
for i, j in Q:
|
|
indices.add(i)
|
|
indices.add(j)
|
|
if not indices:
|
|
return {'solution': [], 'objective': 0.0, 'status': 'empty'}
|
|
|
|
n = max(indices) + 1
|
|
|
|
if not HAS_HIGHS:
|
|
print("[qubo_highs] highspy not installed, falling back to SA")
|
|
return _sa_solve_qubo(Q, n, time_limit)
|
|
|
|
H, c, sense_list, rhs_list = qubo_to_mip(Q)
|
|
if not H:
|
|
return {'solution': [0] * n, 'objective': 0.0, 'status': 'trivial'}
|
|
|
|
num_vars = max(H.keys()) + 1
|
|
num_rows = len(c)
|
|
|
|
# Build model using passModel (reliable API)
|
|
model = highspy.HighsModel()
|
|
lp = model.lp_
|
|
lp.num_col_ = num_vars
|
|
lp.num_row_ = num_rows
|
|
|
|
# Objective coefficients
|
|
obj = np.zeros(num_vars)
|
|
for v, coeff in H.items():
|
|
obj[v] = coeff
|
|
lp.col_cost_ = obj
|
|
|
|
# Variable bounds: all binary [0, 1]
|
|
lp.col_lower_ = np.zeros(num_vars)
|
|
lp.col_upper_ = np.ones(num_vars)
|
|
|
|
# Integrality
|
|
lp.integrality_ = [highspy.HighsVarType.kInteger] * num_vars
|
|
|
|
# Build constraint matrix in CSC format
|
|
# First, convert to column-wise structure
|
|
col_entries: dict[int, list[tuple[int, float]]] = {}
|
|
for row_idx, (var_coeffs, s, rhs) in enumerate(c):
|
|
for col, val in var_coeffs.items():
|
|
if col not in col_entries:
|
|
col_entries[col] = []
|
|
col_entries[col].append((row_idx, val))
|
|
|
|
starts = []
|
|
indices_list = []
|
|
values_list = []
|
|
nnz = 0
|
|
for col in range(num_vars):
|
|
starts.append(nnz)
|
|
if col in col_entries:
|
|
for row_idx, val in col_entries[col]:
|
|
indices_list.append(row_idx)
|
|
values_list.append(val)
|
|
nnz += 1
|
|
|
|
starts.append(nnz)
|
|
|
|
lp.a_matrix_.format_ = highspy.MatrixFormat.kColwise
|
|
lp.a_matrix_.start_ = np.array(starts, dtype=np.int32)
|
|
lp.a_matrix_.index_ = np.array(indices_list, dtype=np.int32) if indices_list else np.array([], dtype=np.int32)
|
|
lp.a_matrix_.value_ = np.array(values_list) if values_list else np.array([])
|
|
|
|
# Row bounds
|
|
row_lower = []
|
|
row_upper = []
|
|
for (var_coeffs, s, rhs) in c:
|
|
if s == '<=':
|
|
row_lower.append(-1e30)
|
|
row_upper.append(rhs)
|
|
else:
|
|
row_lower.append(rhs)
|
|
row_upper.append(1e30)
|
|
lp.row_lower_ = np.array(row_lower)
|
|
lp.row_upper_ = np.array(row_upper)
|
|
|
|
# Solve
|
|
h = highspy.Highs()
|
|
h.setOptionValue("time_limit", time_limit)
|
|
h.setOptionValue("output_flag", False)
|
|
h.passModel(model)
|
|
h.run()
|
|
|
|
sol = h.getSolution()
|
|
x_vals = sol.col_value
|
|
|
|
solution = [int(round(x_vals[i])) for i in range(n)]
|
|
objective = sum(Q.get((i, j), 0.0) * solution[i] * solution[j]
|
|
for i in range(n) for j in range(n) if (i, j) in Q)
|
|
|
|
# Get status
|
|
status_val = h.getInfoValue("primal_solution_status")[1]
|
|
status_map = {
|
|
0: 'unknown',
|
|
1: 'infeasible',
|
|
2: 'feasible',
|
|
3: 'optimal',
|
|
}
|
|
status_str = status_map.get(status_val, f'status_{status_val}')
|
|
|
|
return {'solution': solution, 'objective': objective, 'status': status_str}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Route LP relaxation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def solve_route_lp(cost_matrix: list[list[float]],
|
|
time_limit: float = 60.0) -> dict:
|
|
"""Solve TSP/VRP relaxation using HiGHS LP.
|
|
|
|
Treats the routing problem as an assignment LP relaxation:
|
|
min sum_{i,j} c[i][j] * x[i][j]
|
|
s.t. sum_j x[i][j] = 1 for all i (leave each city once)
|
|
sum_i x[i][j] = 1 for all j (enter each city once)
|
|
0 <= x[i][j] <= 1
|
|
|
|
Returns: {'route': list[int], 'cost': float, 'status': str}
|
|
"""
|
|
n = len(cost_matrix)
|
|
if n == 0:
|
|
return {'route': [], 'cost': 0.0, 'status': 'empty'}
|
|
|
|
if not HAS_HIGHS:
|
|
# Greedy nearest-neighbor fallback
|
|
return _greedy_route(cost_matrix)
|
|
|
|
# Variables: x[i][j] for i,j in 0..n-1, i != j
|
|
var_list = [(i, j) for i in range(n) for j in range(n) if i != j]
|
|
num_vars = len(var_list)
|
|
var_idx = {v: k for k, v in enumerate(var_list)}
|
|
|
|
model = highspy.HighsModel()
|
|
lp = model.lp_
|
|
lp.num_col_ = num_vars
|
|
lp.num_row_ = 2 * n # leave + enter constraints
|
|
|
|
# Objective
|
|
costs = np.zeros(num_vars)
|
|
for (i, j), vi in var_idx.items():
|
|
costs[vi] = cost_matrix[i][j]
|
|
lp.col_cost_ = costs
|
|
|
|
# Bounds
|
|
lp.col_lower_ = np.zeros(num_vars)
|
|
lp.col_upper_ = np.ones(num_vars)
|
|
|
|
# Build CSC constraint matrix
|
|
starts = []
|
|
indices_list = []
|
|
values_list = []
|
|
nnz = 0
|
|
|
|
col_entries: dict[int, list[tuple[int, float]]] = {}
|
|
|
|
# Leave constraints (rows 0..n-1)
|
|
for i in range(n):
|
|
for j in range(n):
|
|
if i != j:
|
|
vi = var_idx[(i, j)]
|
|
if vi not in col_entries:
|
|
col_entries[vi] = []
|
|
col_entries[vi].append((i, 1.0))
|
|
|
|
# Enter constraints (rows n..2n-1)
|
|
for j in range(n):
|
|
for i in range(n):
|
|
if i != j:
|
|
vi = var_idx[(i, j)]
|
|
if vi not in col_entries:
|
|
col_entries[vi] = []
|
|
col_entries[vi].append((n + j, 1.0))
|
|
|
|
for col in range(num_vars):
|
|
starts.append(nnz)
|
|
if col in col_entries:
|
|
for row_idx, val in col_entries[col]:
|
|
indices_list.append(row_idx)
|
|
values_list.append(val)
|
|
nnz += 1
|
|
starts.append(nnz)
|
|
|
|
lp.a_matrix_.format_ = highspy.MatrixFormat.kColwise
|
|
lp.a_matrix_.start_ = np.array(starts, dtype=np.int32)
|
|
lp.a_matrix_.index_ = np.array(indices_list, dtype=np.int32)
|
|
lp.a_matrix_.value_ = np.array(values_list)
|
|
|
|
# Row bounds: all equality (= 1)
|
|
lp.row_lower_ = np.ones(2 * n)
|
|
lp.row_upper_ = np.ones(2 * n)
|
|
|
|
# Solve
|
|
h = highspy.Highs()
|
|
h.setOptionValue("time_limit", time_limit)
|
|
h.setOptionValue("output_flag", False)
|
|
h.passModel(model)
|
|
h.run()
|
|
|
|
sol = h.getSolution()
|
|
x_vals = sol.col_value
|
|
|
|
# Extract route from assignment (greedy rounding)
|
|
route = _extract_route_from_assignment(x_vals, var_idx, n)
|
|
total_cost = sum(cost_matrix[route[i]][route[(i + 1) % n]]
|
|
for i in range(n))
|
|
|
|
status_raw = h.getInfoValue("primal_solution_status")[1]
|
|
status_str = {
|
|
0: 'unknown',
|
|
1: 'infeasible',
|
|
2: 'feasible',
|
|
3: 'optimal',
|
|
}.get(status_raw, f'status_{status_raw}')
|
|
|
|
return {'route': route, 'cost': total_cost, 'status': status_str}
|
|
|
|
|
|
def _extract_route_from_assignment(x_vals, var_idx, n):
|
|
"""Extract a tour from LP assignment solution via greedy rounding."""
|
|
# Greedy: follow highest-weight edges to build a tour
|
|
used = set()
|
|
route = [0]
|
|
current = 0
|
|
while len(route) < n:
|
|
candidates = [(x_vals[var_idx[(current, j)]], j)
|
|
for j in range(n) if j != current and j not in used
|
|
and (current, j) in var_idx]
|
|
if not candidates:
|
|
break
|
|
candidates.sort(reverse=True)
|
|
next_city = candidates[0][1]
|
|
route.append(next_city)
|
|
used.add(next_city)
|
|
current = next_city
|
|
|
|
return route
|
|
|
|
|
|
def _greedy_route(cost_matrix: list[list[float]]) -> dict:
|
|
"""Nearest-neighbor heuristic fallback."""
|
|
n = len(cost_matrix)
|
|
if n == 0:
|
|
return {'route': [], 'cost': 0.0, 'status': 'heuristic'}
|
|
|
|
visited = {0}
|
|
route = [0]
|
|
total = 0.0
|
|
current = 0
|
|
|
|
while len(route) < n:
|
|
best_j = -1
|
|
best_c = float('inf')
|
|
for j in range(n):
|
|
if j not in visited and cost_matrix[current][j] < best_c:
|
|
best_c = cost_matrix[current][j]
|
|
best_j = j
|
|
route.append(best_j)
|
|
visited.add(best_j)
|
|
total += best_c
|
|
current = best_j
|
|
|
|
total += cost_matrix[current][route[0]]
|
|
return {'route': route, 'cost': total, 'status': 'heuristic'}
|