mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
349 lines
13 KiB
Python
349 lines
13 KiB
Python
"""
|
|
Moving Sofa Problem as N-Space Tetris Problem
|
|
|
|
Treats the sofa as a tetromino-like shape that must navigate through
|
|
the corridor by rotating and translating, similar to tetris block fitting.
|
|
|
|
Approach:
|
|
- Discretize sofa into grid of unit squares (pixels)
|
|
- Model corridor as discrete grid
|
|
- Use pathfinding/search for navigation feasibility
|
|
- Optimize shape by adding/removing squares while maintaining navigability
|
|
- N-space: configuration space of (x, y, θ) for each position
|
|
"""
|
|
|
|
import numpy as np
|
|
import scipy.optimize as opt
|
|
from typing import List, Tuple, Set, Dict, Any
|
|
import json
|
|
from collections import deque
|
|
import math
|
|
|
|
# Problem parameters
|
|
GRID_RESOLUTION = 0.05 # Grid resolution for discretization
|
|
HALLWAY_WIDTH = 1.0
|
|
CORNER_ANGLE = math.pi / 2
|
|
|
|
# Known bounds
|
|
SOFA_LOWER_BOUND = 2.2195
|
|
SOFA_UPPER_BOUND = 2.8284
|
|
|
|
class TetrisSofa:
|
|
"""Sofa represented as collection of unit squares (tetris-like)."""
|
|
|
|
def __init__(self, squares: Set[Tuple[int, int]]):
|
|
"""
|
|
Initialize sofa from set of square coordinates.
|
|
Coordinates are in grid units (integer).
|
|
"""
|
|
self.squares = squares
|
|
self.area = len(squares) * (GRID_RESOLUTION ** 2)
|
|
self.center = self._calculate_center()
|
|
|
|
def _calculate_center(self) -> Tuple[float, float]:
|
|
"""Calculate geometric center of sofa."""
|
|
if not self.squares:
|
|
return (0.0, 0.0)
|
|
|
|
x_coords = [x for x, y in self.squares]
|
|
y_coords = [y for x, y in self.squares]
|
|
|
|
center_x = np.mean(x_coords) * GRID_RESOLUTION
|
|
center_y = np.mean(y_coords) * GRID_RESOLUTION
|
|
|
|
return (center_x, center_y)
|
|
|
|
def rotate(self, angle: float) -> 'TetrisSofa':
|
|
"""Rotate sofa by given angle around center."""
|
|
rotated_squares = set()
|
|
|
|
cos_a = math.cos(angle)
|
|
sin_a = math.sin(angle)
|
|
|
|
for x, y in self.squares:
|
|
# Translate to origin
|
|
x_centered = (x * GRID_RESOLUTION - self.center[0])
|
|
y_centered = (y * GRID_RESOLUTION - self.center[1])
|
|
|
|
# Rotate
|
|
x_rot = x_centered * cos_a - y_centered * sin_a
|
|
y_rot = x_centered * sin_a + y_centered * cos_a
|
|
|
|
# Translate back and discretize
|
|
x_new = int((x_rot + self.center[0]) / GRID_RESOLUTION)
|
|
y_new = int((y_rot + self.center[1]) / GRID_RESOLUTION)
|
|
|
|
rotated_squares.add((x_new, y_new))
|
|
|
|
return TetrisSofa(rotated_squares)
|
|
|
|
def translate(self, dx: float, dy: float) -> 'TetrisSofa':
|
|
"""Translate sofa by (dx, dy)."""
|
|
translated_squares = set()
|
|
|
|
dx_grid = int(dx / GRID_RESOLUTION)
|
|
dy_grid = int(dy / GRID_RESOLUTION)
|
|
|
|
for x, y in self.squares:
|
|
translated_squares.add((x + dx_grid, y + dy_grid))
|
|
|
|
return TetrisSofa(translated_squares)
|
|
|
|
def get_bounding_box(self) -> Tuple[float, float, float, float]:
|
|
"""Get bounding box (xmin, ymin, xmax, ymax)."""
|
|
if not self.squares:
|
|
return (0.0, 0.0, 0.0, 0.0)
|
|
|
|
x_coords = [x for x, y in self.squares]
|
|
y_coords = [y for x, y in self.squares]
|
|
|
|
xmin = min(x_coords) * GRID_RESOLUTION
|
|
ymin = min(y_coords) * GRID_RESOLUTION
|
|
xmax = max(x_coords) * GRID_RESOLUTION
|
|
ymax = max(y_coords) * GRID_RESOLUTION
|
|
|
|
return (xmin, ymin, xmax, ymax)
|
|
|
|
def is_connected(self) -> bool:
|
|
"""Check if all squares are connected (tetris property)."""
|
|
if not self.squares:
|
|
return True
|
|
|
|
# BFS to check connectivity
|
|
visited = set()
|
|
queue = deque([next(iter(self.squares))])
|
|
visited.add(queue[0])
|
|
|
|
while queue:
|
|
current = queue.popleft()
|
|
x, y = current
|
|
|
|
# Check 4 neighbors
|
|
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
|
neighbor = (x + dx, y + dy)
|
|
if neighbor in self.squares and neighbor not in visited:
|
|
visited.add(neighbor)
|
|
queue.append(neighbor)
|
|
|
|
return len(visited) == len(self.squares)
|
|
|
|
class CorridorGrid:
|
|
"""Discrete representation of L-shaped corridor."""
|
|
|
|
def __init__(self, resolution: float = GRID_RESOLUTION):
|
|
self.resolution = resolution
|
|
self.grid_size = int(HALLWAY_WIDTH / resolution) + 20 # Extra space for navigation
|
|
self.occupied = set()
|
|
|
|
def add_corridor(self):
|
|
"""Add L-shaped corridor to grid."""
|
|
# Horizontal corridor (y ≥ 0, x from -10 to 10)
|
|
for x in range(-10, 10):
|
|
for y in range(0, int(HALLWAY_WIDTH / self.resolution)):
|
|
self.occupied.add((x, y))
|
|
|
|
# Vertical corridor (x ≥ 0, y from -10 to 10)
|
|
for x in range(0, int(HALLWAY_WIDTH / self.resolution)):
|
|
for y in range(-10, 10):
|
|
self.occupied.add((x, y))
|
|
|
|
def is_valid_position(self, sofa: TetrisSofa) -> bool:
|
|
"""Check if sofa position is valid (within corridor)."""
|
|
for x, y in sofa.squares:
|
|
if (x, y) not in self.occupied:
|
|
return False
|
|
return True
|
|
|
|
class NSpaceNavigator:
|
|
"""Navigate sofa through corridor in n-dimensional configuration space."""
|
|
|
|
def __init__(self):
|
|
self.corridor = CorridorGrid()
|
|
self.corridor.add_corridor()
|
|
|
|
def can_navigate(self, sofa: TetrisSofa, num_steps: int = 50) -> bool:
|
|
"""
|
|
Check if sofa can navigate from horizontal to vertical corridor.
|
|
Uses BFS in configuration space (x, y, θ).
|
|
"""
|
|
# Configuration space: (x, y, θ)
|
|
# Start position: in horizontal corridor, θ = 0
|
|
# End position: in vertical corridor, θ = π/2
|
|
|
|
start_config = (10, 5, 0) # x, y, θ (grid coordinates)
|
|
end_config = (5, 10, math.pi/2)
|
|
|
|
# BFS in configuration space
|
|
visited = set()
|
|
queue = deque([start_config])
|
|
visited.add(start_config)
|
|
|
|
step_size_x = 1
|
|
step_size_y = 1
|
|
step_size_theta = CORNER_ANGLE / num_steps
|
|
|
|
while queue:
|
|
x, y, theta = queue.popleft()
|
|
|
|
# Check if reached end
|
|
if abs(x - end_config[0]) < 2 and abs(y - end_config[1]) < 2 and abs(theta - end_config[2]) < 0.1:
|
|
return True
|
|
|
|
# Generate neighbors in configuration space
|
|
neighbors = [
|
|
(x + step_size_x, y, theta),
|
|
(x - step_size_x, y, theta),
|
|
(x, y + step_size_y, theta),
|
|
(x, y - step_size_y, theta),
|
|
(x, y, theta + step_size_theta),
|
|
(x, y, theta - step_size_theta)
|
|
]
|
|
|
|
for nx, ny, ntheta in neighbors:
|
|
if (nx, ny, ntheta) in visited:
|
|
continue
|
|
|
|
# Check if this configuration is valid
|
|
test_sofa = sofa.translate(nx * self.corridor.resolution, ny * self.corridor.resolution)
|
|
test_sofa = test_sofa.rotate(ntheta)
|
|
|
|
if self.corridor.is_valid_position(test_sofa):
|
|
visited.add((nx, ny, ntheta))
|
|
queue.append((nx, ny, ntheta))
|
|
|
|
return False
|
|
|
|
class TetrisOptimizer:
|
|
"""Optimize sofa shape using tetris-like block assembly."""
|
|
|
|
def __init__(self):
|
|
self.navigator = NSpaceNavigator()
|
|
self.best_sofa = None
|
|
self.best_area = 0.0
|
|
|
|
def generate_initial_shapes(self, n: int = 100) -> List[TetrisSofa]:
|
|
"""Generate initial tetris-like shapes."""
|
|
shapes = []
|
|
|
|
for _ in range(n):
|
|
# Start with a single square
|
|
squares = {(0, 0)}
|
|
|
|
# Randomly add squares to build shape
|
|
num_squares = np.random.randint(5, 20)
|
|
|
|
for _ in range(num_squares):
|
|
# Pick a random existing square
|
|
if not squares:
|
|
break
|
|
|
|
existing = list(squares)[np.random.randint(len(squares))]
|
|
|
|
# Add neighbor
|
|
neighbor_offsets = [(0, 1), (0, -1), (1, 0), (-1, 0)]
|
|
dx, dy = neighbor_offsets[np.random.randint(len(neighbor_offsets))]
|
|
new_square = (existing[0] + dx, existing[1] + dy)
|
|
squares.add(new_square)
|
|
|
|
sofa = TetrisSofa(squares)
|
|
if sofa.is_connected():
|
|
shapes.append(sofa)
|
|
|
|
return shapes
|
|
|
|
def optimize_shape(self, initial_sofa: TetrisSofa) -> TetrisSofa:
|
|
"""Optimize a single shape by adding/removing squares."""
|
|
current_sofa = initial_sofa
|
|
|
|
for iteration in range(100):
|
|
# Try to add a square
|
|
neighbors = set()
|
|
for x, y in current_sofa.squares:
|
|
for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
|
|
neighbors.add((x + dx, y + dy))
|
|
|
|
# Remove squares that are already in sofa
|
|
neighbors = neighbors - current_sofa.squares
|
|
|
|
if neighbors:
|
|
# Try adding a random neighbor
|
|
new_square = list(neighbors)[np.random.randint(len(neighbors))]
|
|
new_squares = current_sofa.squares.copy()
|
|
new_squares.add(new_square)
|
|
test_sofa = TetrisSofa(new_squares)
|
|
|
|
if test_sofa.is_connected() and self.navigator.can_navigate(test_sofa):
|
|
if test_sofa.area > current_sofa.area:
|
|
current_sofa = test_sofa
|
|
|
|
return current_sofa
|
|
|
|
def solve(self, iterations: int = 50) -> Dict[str, Any]:
|
|
"""Solve Moving Sofa Problem using tetris approach."""
|
|
print("Solving Moving Sofa Problem as N-Space Tetris Problem...")
|
|
print(f"Current bounds: {SOFA_LOWER_BOUND} ≤ S ≤ {SOFA_UPPER_BOUND}")
|
|
print("=" * 70)
|
|
|
|
# Generate initial shapes
|
|
initial_shapes = self.generate_initial_shapes(200)
|
|
print(f"Generated {len(initial_shapes)} initial tetris shapes")
|
|
|
|
# Filter navigable shapes
|
|
navigable_shapes = []
|
|
for i, shape in enumerate(initial_shapes):
|
|
if self.navigator.can_navigate(shape):
|
|
navigable_shapes.append(shape)
|
|
if shape.area > self.best_area:
|
|
self.best_area = shape.area
|
|
self.best_sofa = shape
|
|
print(f" Shape {i+1}: area = {shape.area:.6f} (new best)")
|
|
|
|
print(f"Navigable shapes: {len(navigable_shapes)}")
|
|
|
|
# Optimize each navigable shape
|
|
for i, shape in enumerate(navigable_shapes):
|
|
print(f"\nOptimizing shape {i+1}/{len(navigable_shapes)}...")
|
|
optimized = self.optimize_shape(shape)
|
|
|
|
if optimized.area > self.best_area:
|
|
self.best_area = optimized.area
|
|
self.best_sofa = optimized
|
|
print(f" New best area: {self.best_area:.6f}")
|
|
|
|
# Results
|
|
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_area': float(self.best_area),
|
|
'best_squares': list(self.best_sofa.squares) if self.best_sofa else [],
|
|
'lower_bound': SOFA_LOWER_BOUND,
|
|
'upper_bound': SOFA_UPPER_BOUND,
|
|
'improvement': float(self.best_area - SOFA_LOWER_BOUND) if self.best_area > 0 else 0.0
|
|
}
|
|
|
|
if __name__ == "__main__":
|
|
optimizer = TetrisOptimizer()
|
|
results = optimizer.solve(iterations=50)
|
|
|
|
# Save results
|
|
output_file = "/home/allaun/Documents/Research Stack/5-Applications/text-to-cad/models/moving_sofa_tetris_results.json"
|
|
with open(output_file, 'w') as f:
|
|
json.dump(results, f, indent=2)
|
|
|
|
print(f"\nResults saved to: {output_file}")
|
|
print("\nMoving Sofa Problem (Tetris) solver complete!")
|