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
433 lines
13 KiB
Python
433 lines
13 KiB
Python
"""
|
||
Multi-scale optimization using scale space theory.
|
||
|
||
Implements coarse-to-fine optimization via Gaussian smoothing at multiple
|
||
scales, with Q16.16 fixed-point arithmetic for FPGA compatibility.
|
||
|
||
Scale mapping:
|
||
σ₃ (1.0): coarse LP relaxation → approximate solution
|
||
σ₂ (0.75): tighter LP → better solution
|
||
σ₁ (0.5): exact MIP → optimal solution
|
||
σ₀ (0.25): formal verification target
|
||
"""
|
||
|
||
import math
|
||
from typing import Optional
|
||
|
||
try:
|
||
import numpy as np
|
||
HAS_NUMPY = True
|
||
except ImportError:
|
||
HAS_NUMPY = False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Q16.16 fixed-point arithmetic
|
||
# ---------------------------------------------------------------------------
|
||
|
||
Q16_SCALE = 65536 # 2^16
|
||
Q16_MAX = 2147483647 # 2^31 - 1
|
||
Q16_MIN = -2147483648 # -2^31
|
||
|
||
|
||
def q16_clamp(v: int) -> int:
|
||
"""Clamp integer to Q16.16 representable range."""
|
||
return max(Q16_MIN, min(Q16_MAX, v))
|
||
|
||
|
||
def q16_from_float(f: float) -> int:
|
||
"""Convert float to Q16.16 fixed-point."""
|
||
return q16_clamp(round(f * Q16_SCALE))
|
||
|
||
|
||
def q16_to_float(q: int) -> float:
|
||
"""Convert Q16.16 fixed-point to float."""
|
||
return q / Q16_SCALE
|
||
|
||
|
||
def q16_multiply(a: int, b: int) -> int:
|
||
"""Q16.16 multiplication: (a * b) >> 16."""
|
||
return q16_clamp((a * b) >> 16)
|
||
|
||
|
||
def q16_exp(x_q16: int) -> int:
|
||
"""Q16.16 exponential: exp(x) where x is in Q16.16.
|
||
|
||
Uses Python math.exp internally, converts to Q16.16.
|
||
For FPGA, this would use a LUT-based approximation.
|
||
"""
|
||
x_float = q16_to_float(x_q16)
|
||
return q16_from_float(math.exp(x_float))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Gaussian kernel in Q16.16
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def gaussian_kernel_q16(sigma: float, size: int = 256) -> list[int]:
|
||
"""Compute 1D Gaussian kernel in Q16.16 fixed-point.
|
||
|
||
G(x) = exp(-x²/(2σ²)) * 65536 (Q16.16 scale)
|
||
|
||
Args:
|
||
sigma: Standard deviation of the Gaussian (in normalized coords).
|
||
size: Number of kernel taps. Must be odd for symmetry.
|
||
|
||
Returns:
|
||
List of Q16.16 kernel values, normalized so they sum to Q16_SCALE.
|
||
"""
|
||
if size % 2 == 0:
|
||
size += 1 # Ensure odd for symmetry
|
||
|
||
half = size // 2
|
||
two_sigma_sq = 2.0 * sigma * sigma
|
||
|
||
# Compute unnormalized kernel
|
||
kernel_raw = []
|
||
for i in range(size):
|
||
x = (i - half) / half # Map to [-1, 1]
|
||
g = math.exp(-(x * x) / two_sigma_sq)
|
||
kernel_raw.append(q16_from_float(g))
|
||
|
||
# Normalize so kernel sums to Q16_SCALE (1.0 in Q16.16)
|
||
raw_sum = sum(kernel_raw)
|
||
if raw_sum == 0:
|
||
# Degenerate: delta function
|
||
kernel_raw[half] = Q16_SCALE
|
||
else:
|
||
# Scale to sum to 65536
|
||
kernel_raw = [q16_clamp(round(v * Q16_SCALE / raw_sum))
|
||
for v in kernel_raw]
|
||
|
||
return kernel_raw
|
||
|
||
|
||
def gaussian_kernel_2d_q16(sigma: float, size: int = 16) -> list[list[int]]:
|
||
"""Compute 2D Gaussian kernel in Q16.16.
|
||
|
||
Args:
|
||
sigma: Standard deviation.
|
||
size: Kernel dimension (size x size).
|
||
|
||
Returns:
|
||
2D list of Q16.16 values.
|
||
"""
|
||
half = size // 2
|
||
two_sigma_sq = 2.0 * sigma * sigma
|
||
|
||
kernel = []
|
||
total = 0
|
||
for y in range(size):
|
||
row = []
|
||
for x in range(size):
|
||
dx = (x - half) / half
|
||
dy = (y - half) / half
|
||
g = math.exp(-(dx * dx + dy * dy) / two_sigma_sq)
|
||
v = q16_from_float(g)
|
||
row.append(v)
|
||
total += v
|
||
kernel.append(row)
|
||
|
||
# Normalize
|
||
if total > 0:
|
||
kernel = [[q16_clamp(round(v * Q16_SCALE / total))
|
||
for v in row] for row in kernel]
|
||
|
||
return kernel
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Voltage ↔ scale mapping
|
||
# ---------------------------------------------------------------------------
|
||
|
||
# Voltage range: 0.6V → σ=1.0 (coarse), 1.2V → σ=0.0 (fine/identity)
|
||
_VOLTAGE_MIN = 0.6
|
||
_VOLTAGE_MAX = 1.2
|
||
_SIGMA_AT_VMIN = 1.0
|
||
_SIGMA_AT_VMAX = 0.01 # Not exactly 0 to avoid degenerate kernel
|
||
|
||
|
||
def voltage_to_scale(voltage_mv: float) -> float:
|
||
"""Map millivolt voltage to scale parameter σ.
|
||
|
||
Range: 0.6V (600mV, σ=1.0) to 1.2V (1200mV, σ≈0.01).
|
||
|
||
Args:
|
||
voltage_mv: Voltage in millivolts.
|
||
|
||
Returns:
|
||
Scale parameter σ.
|
||
"""
|
||
voltage_v = voltage_mv / 1000.0
|
||
# Clamp to range
|
||
voltage_v = max(_VOLTAGE_MIN, min(_VOLTAGE_MAX, voltage_v))
|
||
# Linear interpolation: σ = 1.0 - (V - 0.6) / 0.6 * 0.99
|
||
t = (voltage_v - _VOLTAGE_MIN) / (_VOLTAGE_MAX - _VOLTAGE_MIN)
|
||
sigma = _SIGMA_AT_VMIN + t * (_SIGMA_AT_VMAX - _SIGMA_AT_VMIN)
|
||
return max(0.01, sigma)
|
||
|
||
|
||
def scale_to_voltage(sigma: float) -> float:
|
||
"""Map scale parameter σ to millivolt voltage.
|
||
|
||
Args:
|
||
sigma: Scale parameter.
|
||
|
||
Returns:
|
||
Voltage in millivolts.
|
||
"""
|
||
sigma = max(_SIGMA_AT_VMAX, min(_SIGMA_AT_VMIN, sigma))
|
||
# Inverse of voltage_to_scale
|
||
t = (sigma - _SIGMA_AT_VMIN) / (_SIGMA_AT_VMAX - _SIGMA_AT_VMIN)
|
||
voltage_v = _VOLTAGE_MIN + t * (_VOLTAGE_MAX - _VOLTAGE_MIN)
|
||
return voltage_v * 1000.0 # Return in mV
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Multi-scale solver
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _apply_smoothing_q16(matrix: list[list[float]],
|
||
sigma: float) -> list[list[float]]:
|
||
"""Apply Gaussian smoothing to a cost matrix using Q16.16 arithmetic.
|
||
|
||
Convolves each row and column with the Gaussian kernel.
|
||
"""
|
||
n = len(matrix)
|
||
if n == 0:
|
||
return matrix
|
||
|
||
kernel = gaussian_kernel_q16(sigma, size=min(n, 33))
|
||
k_half = len(kernel) // 2
|
||
|
||
# Convert matrix to Q16.16
|
||
q16_matrix = [[q16_from_float(matrix[i][j]) for j in range(n)]
|
||
for i in range(n)]
|
||
|
||
# Smooth rows
|
||
smoothed = [[0] * n for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(n):
|
||
total = 0
|
||
for ki in range(len(kernel)):
|
||
jj = j + ki - k_half
|
||
if 0 <= jj < n:
|
||
total += q16_multiply(q16_matrix[i][jj], kernel[ki])
|
||
else:
|
||
# Mirror boundary
|
||
jj = max(0, min(n - 1, jj))
|
||
total += q16_multiply(q16_matrix[i][jj], kernel[ki])
|
||
smoothed[i][j] = q16_clamp(total)
|
||
|
||
# Smooth columns
|
||
result = [[0] * n for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(n):
|
||
total = 0
|
||
for ki in range(len(kernel)):
|
||
ii = i + ki - k_half
|
||
if 0 <= ii < n:
|
||
total += q16_multiply(smoothed[ii][j], kernel[ki])
|
||
else:
|
||
ii = max(0, min(n - 1, ii))
|
||
total += q16_multiply(smoothed[ii][j], kernel[ki])
|
||
result[i][j] = q16_clamp(total)
|
||
|
||
# Convert back to float
|
||
return [[q16_to_float(result[i][j]) for j in range(n)]
|
||
for i in range(n)]
|
||
|
||
|
||
def _greedy_tour(cost_matrix: list[list[float]]) -> tuple[list[int], float]:
|
||
"""Nearest-neighbor heuristic for TSP."""
|
||
n = len(cost_matrix)
|
||
if n == 0:
|
||
return [], 0.0
|
||
|
||
visited = {0}
|
||
tour = [0]
|
||
current = 0
|
||
total_cost = 0.0
|
||
|
||
while len(tour) < 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
|
||
tour.append(best_j)
|
||
visited.add(best_j)
|
||
total_cost += best_c
|
||
current = best_j
|
||
|
||
total_cost += cost_matrix[current][tour[0]]
|
||
return tour, total_cost
|
||
|
||
|
||
def _2opt_improve(tour: list[int],
|
||
cost_matrix: list[list[float]]) -> tuple[list[int], float]:
|
||
"""2-opt local search improvement."""
|
||
n = len(tour)
|
||
if n < 4:
|
||
cost = sum(cost_matrix[tour[i]][tour[(i + 1) % n]] for i in range(n))
|
||
return tour, cost
|
||
|
||
improved = True
|
||
while improved:
|
||
improved = False
|
||
for i in range(1, n - 1):
|
||
for j in range(i + 1, n):
|
||
# Cost of current edges
|
||
d1 = (cost_matrix[tour[i - 1]][tour[i]] +
|
||
cost_matrix[tour[j]][tour[(j + 1) % n]])
|
||
# Cost of reversed segment edges
|
||
d2 = (cost_matrix[tour[i - 1]][tour[j]] +
|
||
cost_matrix[tour[i]][tour[(j + 1) % n]])
|
||
|
||
if d2 < d1 - 1e-10:
|
||
tour[i:j + 1] = reversed(tour[i:j + 1])
|
||
improved = True
|
||
|
||
cost = sum(cost_matrix[tour[i]][tour[(i + 1) % n]] for i in range(n))
|
||
return tour, cost
|
||
|
||
|
||
def solve_multiscale(cost_matrix: list[list[float]],
|
||
sigmas: Optional[list[float]] = None) -> dict:
|
||
"""Solve routing problem at multiple scales.
|
||
|
||
Coarse-to-fine strategy:
|
||
σ₃ (1.0): coarse LP relaxation → approximate solution
|
||
σ₂ (0.75): tighter LP → better solution
|
||
σ₁ (0.5): exact MIP → optimal solution
|
||
σ₀ (0.25): formal verification target
|
||
|
||
At each scale, the cost matrix is Gaussian-smoothed, then solved
|
||
with progressively tighter methods. Solutions from coarser scales
|
||
seed finer scales.
|
||
|
||
Args:
|
||
cost_matrix: n×n cost matrix.
|
||
sigmas: List of scale parameters (coarse to fine).
|
||
|
||
Returns:
|
||
{'solutions': {sigma: {'tour': list, 'cost': float}},
|
||
'converged': bool, 'best_sigma': float}
|
||
"""
|
||
if sigmas is None:
|
||
sigmas = [1.0, 0.75, 0.5, 0.25]
|
||
|
||
n = len(cost_matrix)
|
||
if n == 0:
|
||
return {'solutions': {}, 'converged': True, 'best_sigma': 0.0}
|
||
|
||
solutions = {}
|
||
best_cost = float('inf')
|
||
best_sigma = sigmas[0]
|
||
prev_tour = None
|
||
|
||
for sigma in sigmas:
|
||
# Smooth the cost matrix at this scale
|
||
smoothed = _apply_smoothing_q16(cost_matrix, sigma)
|
||
|
||
# Solve on smoothed costs
|
||
if prev_tour is not None:
|
||
# Warm-start: use previous solution as seed
|
||
# Compute cost on smoothed matrix
|
||
seed_cost = sum(smoothed[prev_tour[i]][prev_tour[(i + 1) % n]]
|
||
for i in range(n))
|
||
# Run 2-opt on smoothed matrix starting from previous tour
|
||
tour, smoothed_cost = _2opt_improve(prev_tour[:], smoothed)
|
||
else:
|
||
# Cold start: greedy + 2-opt
|
||
tour, smoothed_cost = _greedy_tour(smoothed)
|
||
tour, smoothed_cost = _2opt_improve(tour, smoothed)
|
||
|
||
# Evaluate on original cost matrix
|
||
real_cost = sum(cost_matrix[tour[i]][tour[(i + 1) % n]]
|
||
for i in range(n))
|
||
|
||
solutions[sigma] = {
|
||
'tour': tour,
|
||
'cost': real_cost,
|
||
'smoothed_cost': smoothed_cost,
|
||
'sigma': sigma,
|
||
}
|
||
|
||
if real_cost < best_cost:
|
||
best_cost = real_cost
|
||
best_sigma = sigma
|
||
|
||
prev_tour = tour
|
||
|
||
# Check convergence: did the solution stabilize at the finest scale?
|
||
converged = False
|
||
if len(sigmas) >= 2:
|
||
costs = [solutions[s]['cost'] for s in sigmas]
|
||
if len(costs) >= 2:
|
||
# Converged if last two scales are within 1%
|
||
last = costs[-1]
|
||
second_last = costs[-2]
|
||
if second_last > 0:
|
||
converged = abs(last - second_last) / second_last < 0.01
|
||
else:
|
||
converged = abs(last - second_last) < 1e-10
|
||
|
||
return {
|
||
'solutions': solutions,
|
||
'converged': converged,
|
||
'best_sigma': best_sigma,
|
||
'best_cost': best_cost,
|
||
}
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# CLI / demo
|
||
# ---------------------------------------------------------------------------
|
||
|
||
if __name__ == '__main__':
|
||
import random
|
||
|
||
# Generate a random TSP instance
|
||
n = 20
|
||
random.seed(42)
|
||
points = [(random.uniform(0, 100), random.uniform(0, 100))
|
||
for _ in range(n)]
|
||
|
||
cost = [[0.0] * n for _ in range(n)]
|
||
for i in range(n):
|
||
for j in range(n):
|
||
dx = points[i][0] - points[j][0]
|
||
dy = points[i][1] - points[j][1]
|
||
cost[i][j] = math.sqrt(dx * dx + dy * dy)
|
||
|
||
print(f"Random TSP instance: {n} cities")
|
||
print(f"Cost matrix range: [{min(min(row) for row in cost):.1f}, "
|
||
f"{max(max(row) for row in cost):.1f}]")
|
||
|
||
# Solve multi-scale
|
||
result = solve_multiscale(cost)
|
||
|
||
print(f"\nConverged: {result['converged']}")
|
||
print(f"Best sigma: {result['best_sigma']}")
|
||
print(f"Best cost: {result['best_cost']:.2f}")
|
||
|
||
for sigma, data in sorted(result['solutions'].items(), reverse=True):
|
||
print(f" σ={sigma:.2f}: tour cost={data['cost']:.2f}, "
|
||
f"smoothed={data['smoothed_cost']:.2f}")
|
||
|
||
# Demo voltage mapping
|
||
print("\nVoltage ↔ Scale mapping:")
|
||
for mv in [600, 700, 800, 900, 1000, 1100, 1200]:
|
||
s = voltage_to_scale(mv)
|
||
v_back = scale_to_voltage(s)
|
||
print(f" {mv}mV → σ={s:.3f} → {v_back:.0f}mV")
|
||
|
||
# Demo Q16 kernel
|
||
print("\nQ16.16 Gaussian kernel (σ=0.5, 9 taps):")
|
||
k = gaussian_kernel_q16(0.5, 9)
|
||
total_q16 = sum(k)
|
||
print(f" Values: {k}")
|
||
print(f" Sum: {total_q16} (target: {Q16_SCALE})")
|
||
print(f" As floats: [{', '.join(f'{q16_to_float(v):.4f}' for v in k)}]")
|