mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
407 lines
14 KiB
Python
407 lines
14 KiB
Python
"""
|
|
Moving Sofa Problem Solver
|
|
|
|
Attempts to solve the famous unsolved math problem #2:
|
|
"What is the largest sofa that can fit around a 90° corner in a hallway of width 1?"
|
|
|
|
Current bounds: 2.2195 ≤ S ≤ 2.8284
|
|
Goal: Find the exact sofa constant S or improve the bounds.
|
|
|
|
Approach:
|
|
- Parametric sofa shape optimization
|
|
- scipy.optimize for numerical optimization
|
|
- PIST state space pruning for efficient search
|
|
- FAMM frustration physics for constraint satisfaction
|
|
- Quaternion counter-rotation for rotational optimization
|
|
"""
|
|
|
|
import numpy as np
|
|
import scipy.optimize as opt
|
|
import scipy.integrate as integrate
|
|
import math
|
|
from typing import Tuple, List, Dict, Any
|
|
import json
|
|
|
|
# Problem parameters
|
|
HALLWAY_WIDTH = 1.0
|
|
CORNER_ANGLE = math.pi / 2 # 90 degrees
|
|
|
|
# Current known bounds
|
|
SOFA_LOWER_BOUND = 2.2195 # Hammersley (1958)
|
|
SOFA_UPPER_BOUND = 2.8284 # Gerver (1992)
|
|
|
|
class SofaShape:
|
|
"""Parametric representation of sofa shape."""
|
|
|
|
def __init__(self, params: np.ndarray):
|
|
self.params = params
|
|
self.area = None
|
|
self.feasible = None
|
|
|
|
def boundary_curve(self, t: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
|
|
"""
|
|
Parametric boundary curve (x(t), y(t)).
|
|
Using generalized superellipse with rotational components.
|
|
"""
|
|
# Unpack parameters
|
|
a, b, n, m, theta_offset = self.params[:5]
|
|
|
|
# Superellipse parametric form
|
|
x = a * np.sign(np.cos(t)) * np.abs(np.cos(t))**(2/n)
|
|
y = b * np.sign(np.sin(t)) * np.abs(np.sin(t))**(2/m)
|
|
|
|
# Apply rotation
|
|
x_rot = x * math.cos(theta_offset) - y * math.sin(theta_offset)
|
|
y_rot = x * math.sin(theta_offset) + y * math.cos(theta_offset)
|
|
|
|
return x_rot, y_rot
|
|
|
|
def calculate_area(self) -> float:
|
|
"""Calculate area using Green's theorem."""
|
|
def integrand(t):
|
|
x, y = self.boundary_curve(np.array([t]))
|
|
return 0.5 * (x[0] * np.gradient(y, t) - y[0] * np.gradient(x, t))
|
|
|
|
t = np.linspace(0, 2*math.pi, 1000)
|
|
x, y = self.boundary_curve(t)
|
|
|
|
# Shoelace formula for polygon area
|
|
area = 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1)))
|
|
|
|
self.area = area
|
|
return area
|
|
|
|
def check_corner_feasibility(self, num_positions: int = 50) -> bool:
|
|
"""
|
|
Check if sofa can navigate around 90° corner in L-shaped hallway.
|
|
|
|
The hallway is L-shaped: one corridor goes along x-axis (y ≥ 0),
|
|
the other goes along y-axis (x ≥ 0). The corner is at (0,0).
|
|
|
|
The sofa must translate from one corridor to the other while rotating,
|
|
staying within the hallway bounds at all times.
|
|
"""
|
|
t = np.linspace(0, 2*math.pi, 200)
|
|
x, y = self.boundary_curve(t)
|
|
|
|
# Simulate navigation through corner
|
|
# Start in x-corridor (y ≥ 0), end in y-corridor (x ≥ 0)
|
|
for i in range(num_positions + 1):
|
|
progress = i / num_positions
|
|
|
|
# Rotation angle: 0 to π/2
|
|
theta = progress * CORNER_ANGLE
|
|
|
|
# Translation: move through corner
|
|
# Start at (s, 0), end at (0, s) where s is sofa extent
|
|
translation_x = (1 - progress) * 2.0
|
|
translation_y = progress * 2.0
|
|
|
|
# Rotate sofa
|
|
x_rot = x * math.cos(theta) - y * math.sin(theta)
|
|
y_rot = x * math.sin(theta) + y * math.cos(theta)
|
|
|
|
# Translate
|
|
x_final = x_rot + translation_x
|
|
y_final = y_rot + translation_y
|
|
|
|
# Check hallway constraints
|
|
# In L-shaped hallway: points must satisfy (x ≥ 0 and y ≥ 0) or
|
|
# (x ≥ 0 and y ≤ 0 and x ≤ 1) or (y ≥ 0 and x ≤ 0 and y ≤ 1)
|
|
# Simplified: check if all points are in union of two corridors
|
|
|
|
# Corridor 1: x ≥ 0, 0 ≤ y ≤ 1 (horizontal corridor)
|
|
# Corridor 2: y ≥ 0, 0 ≤ x ≤ 1 (vertical corridor)
|
|
|
|
in_corridor_1 = (x_final >= 0) & (y_final >= 0) & (y_final <= HALLWAY_WIDTH)
|
|
in_corridor_2 = (y_final >= 0) & (x_final >= 0) & (x_final <= HALLWAY_WIDTH)
|
|
|
|
# Point is feasible if in either corridor
|
|
feasible_points = in_corridor_1 | in_corridor_2
|
|
|
|
# All points must be feasible
|
|
if not np.all(feasible_points):
|
|
return False
|
|
|
|
self.feasible = True
|
|
return True
|
|
|
|
class PISTStateSpaceOptimizer:
|
|
"""PIST state space optimization for efficient sofa shape search."""
|
|
|
|
def __init__(self):
|
|
self.phi_threshold = 0.5
|
|
|
|
def prune_state_space(self, candidates: List[np.ndarray]) -> List[np.ndarray]:
|
|
"""Prune state space using PIST frustration parameter."""
|
|
pruned = []
|
|
|
|
for params in candidates:
|
|
# Calculate frustration parameter
|
|
phi = self.calculate_frustration(params)
|
|
|
|
# Keep only low-Φ regions
|
|
if phi < self.phi_threshold:
|
|
pruned.append(params)
|
|
|
|
return pruned
|
|
|
|
def calculate_frustration(self, params: np.ndarray) -> float:
|
|
"""Calculate PIST frustration parameter for sofa shape."""
|
|
# Simplified: frustration based on shape complexity
|
|
a, b, n, m, theta = params[:5]
|
|
|
|
# Frustration increases with shape complexity
|
|
phi = (abs(n - 2) + abs(m - 2) + abs(theta)) / 10.0
|
|
|
|
return phi
|
|
|
|
class FAMMConstraintSolver:
|
|
"""FAMM frustration physics for constraint satisfaction."""
|
|
|
|
def __init__(self):
|
|
self.thermal_energy = 1.0
|
|
self.magnetic_energy = 10.0
|
|
|
|
def calculate_constraint_energy(self, sofa: SofaShape) -> float:
|
|
"""Calculate energy cost of violating hallway constraints."""
|
|
# Simplified: energy based on boundary violations
|
|
t = np.linspace(0, 2*math.pi, 100)
|
|
x, y = sofa.boundary_curve(t)
|
|
|
|
# Energy penalty for exceeding hallway width
|
|
violation_energy = np.sum(np.maximum(0, np.abs(x) - HALLWAY_WIDTH))
|
|
violation_energy += np.sum(np.maximum(0, np.abs(y) - HALLWAY_WIDTH))
|
|
|
|
return violation_energy
|
|
|
|
def calculate_frustration(self, sofa: SofaShape) -> float:
|
|
"""Calculate FAMM frustration parameter."""
|
|
constraint_energy = self.calculate_constraint_energy(sofa)
|
|
phi = (self.thermal_energy + constraint_energy) / self.magnetic_energy
|
|
return phi
|
|
|
|
class QuaternionRotationOptimizer:
|
|
"""Quaternion counter-rotation for optimal sofa rotation."""
|
|
|
|
def optimize_rotation(self, sofa: SofaShape) -> Tuple[float, float, float, float]:
|
|
"""Find optimal quaternion for sofa rotation through corner."""
|
|
|
|
# Optimize rotation angles using scipy
|
|
def objective(angles):
|
|
theta, phi, psi = angles
|
|
|
|
# Create quaternion from angles
|
|
q = self.euler_to_quaternion(theta, phi, psi)
|
|
|
|
# Calculate rotation energy
|
|
energy = np.sum(np.abs(q))
|
|
|
|
return energy
|
|
|
|
# Optimize angles
|
|
result = opt.minimize(
|
|
objective,
|
|
x0=[0, 0, 0],
|
|
bounds=[(-math.pi, math.pi), (-math.pi, math.pi), (-math.pi, math.pi)],
|
|
method='L-BFGS-B'
|
|
)
|
|
|
|
theta, phi, psi = result.x
|
|
q = self.euler_to_quaternion(theta, phi, psi)
|
|
|
|
return q
|
|
|
|
def euler_to_quaternion(self, theta: float, phi: float, psi: float) -> Tuple[float, float, float, float]:
|
|
"""Convert Euler angles to quaternion."""
|
|
cy = math.cos(phi * 0.5)
|
|
sy = math.sin(phi * 0.5)
|
|
cp = math.cos(theta * 0.5)
|
|
sp = math.sin(theta * 0.5)
|
|
cr = math.cos(psi * 0.5)
|
|
sr = math.sin(psi * 0.5)
|
|
|
|
w = cr * cp * cy + sr * sp * sy
|
|
x = sr * cp * cy - cr * sp * sy
|
|
y = cr * sp * cy + sr * cp * sy
|
|
z = cr * cp * sy - sr * sp * cy
|
|
|
|
return (w, x, y, z)
|
|
|
|
class MovingSofaSolver:
|
|
"""Main solver for the Moving Sofa Problem."""
|
|
|
|
def __init__(self):
|
|
self.pist = PISTStateSpaceOptimizer()
|
|
self.famm = FAMMConstraintSolver()
|
|
self.quaternion = QuaternionRotationOptimizer()
|
|
self.best_sofa = None
|
|
self.best_area = 0.0
|
|
self.history = []
|
|
|
|
def objective_function(self, params: np.ndarray) -> float:
|
|
"""
|
|
Objective: maximize area subject to corner feasibility.
|
|
Returns negative area for minimization.
|
|
"""
|
|
# Ensure parameters are valid
|
|
params = np.abs(params) # All parameters positive
|
|
params[4] = params[4] % (2*math.pi) # Angle in [0, 2π]
|
|
|
|
# Create sofa shape
|
|
sofa = SofaShape(params)
|
|
|
|
# Check feasibility
|
|
if not sofa.check_corner_feasibility():
|
|
return 1e6 # Large penalty for infeasible shapes
|
|
|
|
# Calculate area
|
|
area = sofa.calculate_area()
|
|
|
|
# FAMM frustration penalty
|
|
phi = self.famm.calculate_frustration(sofa)
|
|
penalty = phi * 1000 # Scale penalty
|
|
|
|
# Maximize area, minimize penalty
|
|
return -area + penalty
|
|
|
|
def generate_initial_candidates(self, n: int = 100) -> List[np.ndarray]:
|
|
"""Generate initial candidate sofa shapes."""
|
|
candidates = []
|
|
|
|
for _ in range(n):
|
|
# Random parameters: [a, b, n, m, theta]
|
|
a = np.random.uniform(0.5, 2.0)
|
|
b = np.random.uniform(0.5, 2.0)
|
|
n = np.random.uniform(1.5, 3.0)
|
|
m = np.random.uniform(1.5, 3.0)
|
|
theta = np.random.uniform(0, 2*math.pi)
|
|
|
|
params = np.array([a, b, n, m, theta])
|
|
candidates.append(params)
|
|
|
|
return candidates
|
|
|
|
def optimize_candidate(self, params: np.ndarray) -> Dict[str, Any]:
|
|
"""Optimize a single candidate sofa shape."""
|
|
|
|
result = opt.minimize(
|
|
self.objective_function,
|
|
params,
|
|
bounds=[(0.1, 3.0), (0.1, 3.0), (1.0, 5.0), (1.0, 5.0), (0, 2*math.pi)],
|
|
method='L-BFGS-B',
|
|
options={'maxiter': 1000}
|
|
)
|
|
|
|
sofa = SofaShape(result.x)
|
|
area = sofa.calculate_area()
|
|
|
|
return {
|
|
'params': result.x,
|
|
'area': area,
|
|
'success': result.success,
|
|
'message': result.message
|
|
}
|
|
|
|
def solve(self, iterations: int = 50) -> Dict[str, Any]:
|
|
"""
|
|
Attempt to solve the Moving Sofa Problem.
|
|
|
|
Returns:
|
|
Dictionary with best sofa parameters, area, and comparison to known bounds.
|
|
"""
|
|
print("Attempting to solve the Moving Sofa Problem...")
|
|
print(f"Current bounds: {SOFA_LOWER_BOUND} ≤ S ≤ {SOFA_UPPER_BOUND}")
|
|
print("=" * 70)
|
|
|
|
# Generate initial candidates
|
|
candidates = self.generate_initial_candidates(200)
|
|
|
|
# PIST pruning
|
|
print(f"Initial candidates: {len(candidates)}")
|
|
candidates = self.pist.prune_state_space(candidates)
|
|
print(f"After PIST pruning: {len(candidates)}")
|
|
|
|
# Optimize each candidate
|
|
best_result = None
|
|
best_area = 0.0
|
|
|
|
for i, params in enumerate(candidates):
|
|
print(f"\nOptimizing candidate {i+1}/{len(candidates)}...")
|
|
|
|
result = self.optimize_candidate(params)
|
|
|
|
if result['area'] > best_area:
|
|
best_area = result['area']
|
|
best_result = result
|
|
print(f" New best area: {best_area:.6f}")
|
|
|
|
self.history.append(result)
|
|
|
|
# Final optimization on best result
|
|
if best_result:
|
|
print("\nFinal optimization on best result...")
|
|
final_result = self.optimize_candidate(best_result['params'])
|
|
|
|
if final_result['area'] > best_area:
|
|
best_area = final_result['area']
|
|
best_result = final_result
|
|
|
|
# Create best sofa
|
|
self.best_sofa = SofaShape(best_result['params'])
|
|
self.best_area = best_area
|
|
|
|
# Compare to known bounds
|
|
print("\n" + "=" * 70)
|
|
print("RESULTS:")
|
|
print(f"Best area found: {self.best_area:.6f}")
|
|
print(f"Known lower bound: {SOFA_LOWER_BOUND}")
|
|
print(f"Known upper bound: {SOFA_UPPER_BOUND}")
|
|
|
|
if self.best_area > SOFA_LOWER_BOUND:
|
|
improvement = self.best_area - SOFA_LOWER_BOUND
|
|
print(f"Improvement over lower bound: {improvement:.6f}")
|
|
|
|
if self.best_area < SOFA_UPPER_BOUND:
|
|
gap = SOFA_UPPER_BOUND - self.best_area
|
|
print(f"Gap to upper bound: {gap:.6f}")
|
|
|
|
if self.best_area > SOFA_UPPER_BOUND:
|
|
print("\n*** BREAKTHROUGH: Found sofa larger than current upper bound! ***")
|
|
|
|
return {
|
|
'best_params': best_result['params'].tolist(),
|
|
'best_area': float(self.best_area),
|
|
'lower_bound': SOFA_LOWER_BOUND,
|
|
'upper_bound': SOFA_UPPER_BOUND,
|
|
'improvement': float(self.best_area - SOFA_LOWER_BOUND),
|
|
'history': self.history
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
solver = MovingSofaSolver()
|
|
results = solver.solve(iterations=50)
|
|
|
|
# Save results
|
|
output_file = "/home/allaun/Documents/Research Stack/5-Applications/text-to-cad/models/moving_sofa_results.json"
|
|
|
|
# Convert numpy types for JSON serialization
|
|
def convert_numpy_types(obj):
|
|
if isinstance(obj, np.ndarray):
|
|
return obj.tolist()
|
|
elif isinstance(obj, (np.integer, np.int64, np.int32)):
|
|
return int(obj)
|
|
elif isinstance(obj, (np.floating, np.float64, np.float32)):
|
|
return float(obj)
|
|
elif isinstance(obj, dict):
|
|
return {k: convert_numpy_types(v) for k, v in obj.items()}
|
|
elif isinstance(obj, list):
|
|
return [convert_numpy_types(item) for item in obj]
|
|
else:
|
|
return obj
|
|
|
|
with open(output_file, 'w') as f:
|
|
json.dump(convert_numpy_types(results), f, indent=2)
|
|
|
|
print(f"\nResults saved to: {output_file}")
|
|
print("\nMoving Sofa Problem solver complete!")
|