""" 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!")