From 36722074ca4b11f7f7cd4d613ad2bacb2ac8fb52 Mon Sep 17 00:00:00 2001
From: Adam Sitabkhan <adam.sitabkhan@gmail.com>
Date: Sun, 19 Jan 2025 16:01:20 -0600
Subject: [PATCH] Subgoals added to place_grid without center matching

---
 guided_mrmp/controllers/place_grid.py | 299 +++++++++++++++++---------
 1 file changed, 192 insertions(+), 107 deletions(-)

diff --git a/guided_mrmp/controllers/place_grid.py b/guided_mrmp/controllers/place_grid.py
index b940dca..b1766aa 100644
--- a/guided_mrmp/controllers/place_grid.py
+++ b/guided_mrmp/controllers/place_grid.py
@@ -1,31 +1,38 @@
 import cvxpy as cp
 import numpy as np
+import time
 
-def place_grid(robot_locations, cell_size=1, grid_shape=(5, 5), return_loss=False):
+def place_grid(robot_locations, cell_size, grid_size=5, subgoals=[], obstacles=[]):
     """
         Place a grid to cover robot locations with alignment to centers.
 
         inputs:
             - robot_locations (list): locations of robots involved in conflict [[x,y], [x,y], ...]
             - cell_size (float): the width of each grid cell in continuous space
-            - grid_shape (tuple): (# of rows, # of columns) of the grid
+            - grid_size (tuple): width of the grid in cells
+            - obstacles (list): locations of circular obstacles [[x,y,r], [x,y,r], ...]
         outputs:
             - origin (tuple): bottom-left corner of the grid in continuous space
             - cell_centers (list): centers of grid cells for each robot (same order as robot_locations)
-            - loss: when return_loss=True, sum of squared differences loss
     """
+    start_time = time.time()
+    
     robot_locations = np.array(robot_locations)
+    subgoals = np.array(subgoals)
+    obstacles = np.array(obstacles)
     N = len(robot_locations)
     
     # Decision variable: Bottom-left corner of the grid in continuous space
-    origin = cp.Variable(2, name='origin')
+    bottom_left = cp.Variable(2, name='origin')
+    # Defin top right for convenience
+    top_right = bottom_left + grid_size * cell_size
     
     # Decision variable: Integer grid indices for each robot
     grid_indices = cp.Variable((N, 2), integer=True, name='grid_indices')
     
     # Calculate cell centers for each robot based on grid indices
     # Reshape origin to (1, 2) for broadcasting
-    cell_centers = cp.reshape(origin, (1, 2), order='C') + grid_indices * cell_size + cell_size / 2
+    cell_centers = cp.reshape(bottom_left, (1, 2), order='C') + grid_indices * cell_size + cell_size / 2
     
     # Objective: Minimize the sum of squared distances
     cost = cp.sum_squares(robot_locations - cell_centers)
@@ -37,102 +44,126 @@ def place_grid(robot_locations, cell_size=1, grid_shape=(5, 5), return_loss=Fals
     constraints.append(grid_indices >= 0)
     
     # Grid indices must fit within grid bounds
-    if grid_shape[0] == grid_shape[1]: # Square grid
-        constraints.append(grid_indices <= grid_shape[0] - 1)
-    else: # Rectangular grid
-        constraints.append(grid_indices[:,0] <= grid_shape[1] - 1)
-        constraints.append(grid_indices[:,1] <= grid_shape[0] - 1)
+    constraints.append(grid_indices <= grid_size - 1)
     
     # No two robots can share a cell
     # Use Big M method to ensure unique grid indices
-    M = max(grid_shape) * 10
+    M_ind = 10 * grid_size # Big M relative to grid indices
+    M_cts = 10 * max(max(robot_locations[:,0]) - min(robot_locations[:,0]), max(robot_locations[:,1]) - min(robot_locations[:,1])) # Big M relative to robot locations
     for i in range(N):
         for j in range(i+1, N):
             # At least one of the two constraints below must be true
-            y1 = cp.Variable(boolean=True)
-            y2 = cp.Variable(boolean=True)
-            constraints.append(y1 + y2 >= 1)
+            xsep = cp.Variable(boolean=True)
+            ysep = cp.Variable(boolean=True)
+            constraints.append(xsep + ysep >= 1)
             
             # Enforces separation by at least 1 in the x direction
-            if robot_locations[i, 0] >= robot_locations[j, 0]:
-                constraints.append(grid_indices[i, 0] - grid_indices[j, 0] + M * (1 - y1) >= 1)
-            else:
-                constraints.append(grid_indices[j, 0] - grid_indices[i, 0] + M * (1 - y1) >= 1)
+            b0 = cp.Variable(boolean=True) # b0 = 0 if robot i's x >= robot j's x, 1 otherwise
+            # b0 = 0
+            constraints.append(robot_locations[j, 0] - robot_locations[i, 0] <= M_cts * b0)
+            constraints.append(grid_indices[i, 0] - grid_indices[j, 0] + M_ind * b0 + M_ind * (1 - xsep) >= 1)
+            # b0 = 1
+            constraints.append(robot_locations[i, 0] - robot_locations[j, 0] <= M_cts * (1 - b0))
+            constraints.append(grid_indices[j, 0] - grid_indices[i, 0] + M_ind * (1 - b0) + M_ind * (1 - xsep) >= 1)
             
             # Enforces separation by at least 1 in the y direction
-            if robot_locations[i, 1] >= robot_locations[j, 1]:
-                constraints.append(grid_indices[i, 1] - grid_indices[j, 1] + M * (1 - y2) >= 1)
-            else:
-                constraints.append(grid_indices[j, 1] - grid_indices[i, 1] + M * (1 - y2) >= 1)
+            b1 = cp.Variable(boolean=True) # b1 = 0 if robot i's y >= robot j's y, 1 otherwise
+            # b1 = 0
+            constraints.append(robot_locations[j, 1] - robot_locations[i, 1] <= M_cts * b1)
+            constraints.append(grid_indices[i, 1] - grid_indices[j, 1] + M_ind * b1 + M_ind * (1 - ysep) >= 1)
+            # b1 = 1
+            constraints.append(robot_locations[i, 1] - robot_locations[j, 1] <= M_cts * (1 - b1))
+            constraints.append(grid_indices[j, 1] - grid_indices[i, 1] + M_ind * (1 - b1) + M_ind * (1 - ysep) >= 1)
+    
+    # All robots and subgoals must be within grid bounds
+    for sg in subgoals:
+        constraints.append(bottom_left <= sg)
+        constraints.append(sg <= top_right)
     
     # Solve the optimization problem
+    prob_init_start_time = time.time()
     prob = cp.Problem(cp.Minimize(cost), constraints)
+    solve_start_time = time.time()
     prob.solve(solver=cp.SCIP)
+    solve_end_time = time.time()
+    
+    print("Time to add vars/constraints:", prob_init_start_time - start_time)
+    print("Time to parse:", solve_start_time - prob_init_start_time)
+    print("Time to solve:", solve_end_time - solve_start_time)
 
-    if prob.status not in ["optimal", "optimal_inaccurate"]:
+    if prob.status != "optimal":
         print("Problem could not be solved to optimality.")
         return None
     
-    if return_loss:
-        return origin.value, cell_centers.value, prob.value
-    return origin.value, cell_centers.value
+    return bottom_left.value, cell_centers.value
 
 
-# This currently does not follow DCP, working on it
-def place_grid_with_rotation(robot_locations, grid_size=5, return_loss=False):
+def two_corner_place_grid(robot_locations, grid_size=5, subgoals=[], obstacles=[]):
     """
-        Place a square grid to cover robot locations with alignment to centers. Allows for rotation and scaling of the grid.
+        Place a grid to cover robot locations with alignment to centers.
 
         inputs:
             - robot_locations (list): locations of robots involved in conflict [[x,y], [x,y], ...]
-            - grid_size (float): the number of cells in each row/column of the grid
+            - cell_size (float): the width of each grid cell in continuous space
+            - grid_size (tuple): width of the grid in cells
+            - obstacles (list): locations of circular obstacles [[x,y,r], [x,y,r], ...]
         outputs:
-            - bottom_left (tuple): bottom-left corner of the grid in continuous space
-            - top_right (tuple): top-right corner of the grid in continuous space
+            - origin (tuple): bottom-left corner of the grid in continuous space
             - cell_centers (list): centers of grid cells for each robot (same order as robot_locations)
-            - loss: when return_loss=True, sum of squared differences loss
     """
+    start_time = time.time()
+    
     robot_locations = np.array(robot_locations)
+    subgoals = np.array(subgoals)
+    obstacles = np.array(obstacles)
     N = len(robot_locations)
     
-    # Decision variables: Bottom-left and top-right corners of the grid in continuous space
+    # Decision variable: Bottom-left corner of the grid in continuous space
     bottom_left = cp.Variable(2, name='bottom_left')
     top_right = cp.Variable(2, name='top_right')
     
+    # Bottom-right and top-left corners of the grid for convenience
+    # bottom_right = 0.5 * cp.hstack([bottom_left[0] + top_right[0] - bottom_left[1] + top_right[1],
+    #                                 bottom_left[0] - top_right[0] + bottom_left[1] + top_right[1]])
+    # top_left = 0.5 * cp.hstack([bottom_left[0] + top_right[0] + bottom_left[1] - top_right[1],
+    #                             -bottom_left[0] + top_right[0] + bottom_left[1] + top_right[1]])
+    bottom_right = cp.Variable(2, name='bottom_right')
+    top_left = cp.Variable(2, name='top_left')
+    
+    grid_x_hat = cp.Variable(2, name='grid_x_hat')
+    grid_y_hat = cp.Variable(2, name='grid_y_hat')
+    
     # Decision variable: Integer grid indices for each robot
     grid_indices = cp.Variable((N, 2), integer=True, name='grid_indices')
-
-    # Define bottom-right and top-left corners of the grid
-    bottom_right = (1 / 2) * cp.hstack([
-        bottom_left[0] + top_right[0] - bottom_left[1] + top_right[1],
-        bottom_left[0] - top_right[0] + bottom_left[1] + top_right[1]
-    ])
-    top_left = (1 / 2) * cp.hstack([
-        bottom_left[0] + top_right[0] + bottom_left[1] - top_right[1],
-        -bottom_left[0] + top_right[0] + bottom_left[1] + top_right[1]
-    ])
-    
-    # Define basis vectors for the grid
-    # Vector pointing from **left -> right** on the grid, with length equal to the width of one cell (1 grid index)
-    x_prime_hat = (bottom_right - bottom_left) * (1 / grid_size)
-    # Vector pointing from **bottom -> top** on the grid, with length equal to the width of one cell (1 grid index)
-    y_prime_hat = (top_left - bottom_left) * (1 / grid_size)
     
     # Calculate cell centers for each robot based on grid indices
-    cell_centers = cp.vstack([bottom_left for _ in range(N)]) # Grid origin point
-    cell_centers += cp.vstack([x_prime_hat * (grid_indices[i,0] + 0.5) for i in range(N)]) # Component of cell centers in the x_prime direction
-    cell_centers += cp.vstack([y_prime_hat * (grid_indices[i,1] + 0.5) for i in range(N)]) # Component of cell centers in the y_prime direction
-    print(cell_centers)
+    # Reshape origin to (1, 2) for broadcasting
+    grid_x_offsets = cp.Variable((N, 2), name='grid_x_offsets')
+    grid_y_offsets = cp.Variable((N, 2), name='grid_y_offsets')
+    cell_centers = cp.reshape(bottom_left, (1, 2), order='C') + grid_x_offsets + grid_y_offsets
     
     # Objective: Minimize the sum of squared distances
     cost = cp.sum_squares(robot_locations - cell_centers)
     
-    # Initialize constraints
+    # Constraints
     constraints = []
     
-    # The top right corner of the grid can't be below or to the left of the bottom left corner
+    # Ensure top-right and bottom-left corners are in the right orientation
     constraints.append(top_right >= bottom_left)
     
+    # Fixing bottom-right and top-left corners
+    constraints.append(2 * bottom_right[0] == bottom_left[0] + top_right[0] - bottom_left[1] + top_right[1])
+    constraints.append(2 * bottom_right[1] == bottom_left[0] - top_right[0] + bottom_left[1] + top_right[1])
+    constraints.append(2 * top_left[0] == bottom_left[0] + top_right[0] + bottom_left[1] - top_right[1])
+    constraints.append(2 * top_left[1] == -bottom_left[0] + top_right[0] + bottom_left[1] + top_right[1])
+    
+    # Defining grid_x_hat and grid_y_hat based on corners
+    constraints.append(grid_x_hat == (bottom_right - bottom_left) * (1 / grid_size))
+    constraints.append(grid_y_hat == (top_left - bottom_left) * (1 / grid_size))
+    
+    # Defining offsets in cell centers calculation
+    constraints.append(grid_x_offsets == grid_x_hat * grid_indices)
+    
     # Grid indices must be non-negative
     constraints.append(grid_indices >= 0)
     
@@ -140,52 +171,64 @@ def place_grid_with_rotation(robot_locations, grid_size=5, return_loss=False):
     constraints.append(grid_indices <= grid_size - 1)
     
     # No two robots can share a cell
-    M = 1e6 # Sufficiently large constant for Big-M
+    # Use Big M method to ensure unique grid indices
+    M_ind = 10 * grid_size # Big M relative to grid indices
+    M_cts = 10 * max(max(robot_locations[:,0]) - min(robot_locations[:,0]), max(robot_locations[:,1]) - min(robot_locations[:,1])) # Big M relative to robot locations
     for i in range(N):
         for j in range(i+1, N):
-            x_separated = cp.Variable(boolean=True)
-            y_separated = cp.Variable(boolean=True)
-            
-            # Robot i's coordinate in the x_prime direction
-            robot_i_x_prime = robot_locations[i] @ x_prime_hat
-            # Robot j's coordinate in the x_prime direction
-            robot_j_x_prime = robot_locations[j] @ x_prime_hat
-            
-            b0 = cp.Variable(boolean=True)
-            # When b1 = 1, robot i's x_prime coordinate is greater than robot j's
-            constraints.append(robot_i_x_prime - robot_j_x_prime <= M * b0)  
-            # When b1 = 0, robot j's x_prime coordinate is greater than robot i's
-            constraints.append(robot_j_x_prime - robot_i_x_prime <= M * (1 - b0)) 
-            # Enforces separation by at least 1 between the robots' x indices on the grid
-            constraints.append((2 * b0 - 1) * (grid_indices[i, 0] - grid_indices[j, 0]) + M * (1 - x_separated) >= 1)
+            # At least one of the two constraints below must be true
+            xsep = cp.Variable(boolean=True)
+            ysep = cp.Variable(boolean=True)
+            constraints.append(xsep + ysep >= 1)
             
-            # Robot i's coordinate in the y_prime direction
-            robot_i_y_prime = robot_locations[i] @ y_prime_hat
-            # Robot j's coordinate in the y_prime direction
-            robot_j_y_prime = robot_locations[j] @ y_prime_hat
+            # Enforces separation by at least 1 in the x direction
+            b0 = cp.Variable(boolean=True) # b0 = 0 if robot i's x >= robot j's x, 1 otherwise
+            # b0 = 0
+            constraints.append(robot_locations[j, 0] - robot_locations[i, 0] <= M_cts * b0)
+            constraints.append(grid_indices[i, 0] - grid_indices[j, 0] + M_ind * b0 + M_ind * (1 - xsep) >= 1)
+            # b0 = 1
+            constraints.append(robot_locations[i, 0] - robot_locations[j, 0] <= M_cts * (1 - b0))
+            constraints.append(grid_indices[j, 0] - grid_indices[i, 0] + M_ind * (1 - b0) + M_ind * (1 - xsep) >= 1)
             
-            b1 = cp.Variable(boolean=True)
-            # When b1 = 1, robot i's y_prime coordinate is greater than robot j's
-            constraints.append(robot_i_y_prime - robot_j_y_prime <= M * b1)  
-            # When b1 = 0, robot j's y_prime coordinate is greater than robot i's
-            constraints.append(robot_j_y_prime - robot_i_y_prime <= M * (1 - b1)) 
-            # Enforces separation by at least 1 between the robots' y indices on the grid
-            constraints.append((2 * b1 - 1) * (grid_indices[i, 1] - grid_indices[j, 1]) + M * (1 - y_separated) >= 1)
-                
-            # Robots must be separated in at least one of the x, y directions
-            constraints.append(x_separated + y_separated >= 1)
+            # Enforces separation by at least 1 in the y direction
+            b1 = cp.Variable(boolean=True) # b1 = 0 if robot i's y >= robot j's y, 1 otherwise
+            # b1 = 0
+            constraints.append(robot_locations[j, 1] - robot_locations[i, 1] <= M_cts * b1)
+            constraints.append(grid_indices[i, 1] - grid_indices[j, 1] + M_ind * b1 + M_ind * (1 - ysep) >= 1)
+            # b1 = 1
+            constraints.append(robot_locations[i, 1] - robot_locations[j, 1] <= M_cts * (1 - b1))
+            constraints.append(grid_indices[j, 1] - grid_indices[i, 1] + M_ind * (1 - b1) + M_ind * (1 - ysep) >= 1)
     
     # Solve the optimization problem
+    prob_init_start_time = time.time()
     prob = cp.Problem(cp.Minimize(cost), constraints)
+    solve_start_time = time.time()
     prob.solve(solver=cp.SCIP)
+    solve_end_time = time.time()
+    
+    print("Time to add vars/constraints:", prob_init_start_time - start_time)
+    print("Time to parse:", solve_start_time - prob_init_start_time)
+    print("Time to solve:", solve_end_time - solve_start_time)
 
-    if prob.status not in ["optimal", "optimal_inaccurate"]:
+    if prob.status != "optimal":
         print("Problem could not be solved to optimality.")
         return None
     
-    if return_loss:
-        return bottom_left.value, top_right.value, cell_centers.value, prob.value
-    return bottom_left.value, top_right.value, cell_centers.value
+    print("Grid Indices:", grid_indices.value)
+    
+    return bottom_left.value, cell_centers.value
+
+
+def mccormick_envelope(w, x, xl, xu, y, yl, yu):
+    """
+    Generates McCormick envelope constraints
+    """
+    mec = []
+    mec.append(w >= xl*y + x*yl - xl*yl)
+    mec.append(w >= xu*y + x*yu - xu*yu)
+    mec.append(w <= xu*y + x*yl - xu*yl)
+    mec.append(w >= x*yu + xl*y - xl*yu)
+    return mec
 
 
 def plot_grid(bottom_left, top_right, grid_size):
@@ -211,13 +254,52 @@ def plot_grid(bottom_left, top_right, grid_size):
                  [(bottom_left + i * y_prime_hat)[1], (bottom_right + i * y_prime_hat)[1]], 'k-')
 
 
-def main(seed, num_robots, plot):
+def get_roomba_locs(low, high, num_robots, radius=0.5):
+    """
+    Generates a list of roomba locations within the box bounded by points (low, low), (high, low), (high, high), (low, high).
+    The roombas must be separated by at least 2 * radius
+    """
+    locs = []
+    while len(locs) < num_robots:
+        locs.append(np.random.uniform(low, high, 2))
+        for other_loc in locs[:-1]:
+            if np.linalg.norm(np.array(locs[-1]) - np.array(other_loc)) <= 2 * radius:
+                locs = locs[:-1]
+                break
+    return np.array(locs)
+
+
+def main(seed, num_robots, plot, two_corner):
     if seed is not None:
         np.random.seed(seed)
     
-    robot_locations = np.random.uniform(low=0, high=7.5, size=(num_robots, 2))
-    cell_size = 1.5
-    grid_shape = (5, 5)
+    if not two_corner:
+        roomba_radius = 0.5
+        robot_locations = get_roomba_locs(low=0, high=6, num_robots=num_robots, radius=roomba_radius)
+        # robot_locations = np.random.uniform(low=0, high=5, size=(num_robots, 2))
+        cell_size = 2.5 * roomba_radius
+        grid_size = 5
+        # subgoals = np.random.uniform(low=0, high=6, size=(num_robots, 2))
+        subgoals = np.array([[0, 0], [0, 6], [6, 6], [6, 0]])
+        
+        bottom_left, cell_centers = place_grid(robot_locations=robot_locations, 
+                                               cell_size=cell_size,
+                                               grid_size=grid_size,
+                                               subgoals=subgoals)
+        
+        print("Grid Origin (Bottom-Left Corner):", bottom_left)
+        print("Cell Centers:", cell_centers)
+        
+        top_right = np.array(bottom_left) + grid_size * cell_size
+    else:
+        grid_size = 5
+        robot_locations = np.random.uniform(low=0, high=5, size=(num_robots, 2))
+        print("Robot Locations:", robot_locations)
+        
+        bottom_left, top_right, grid_indices = two_corner_place_grid(robot_locations, grid_size)
+        print("Grid Bottom-Left Corner:", bottom_left)
+        print("Grid Top-Right Corner:", top_right)
+        print("Grid Indices:", grid_indices)
     
     if plot:
         import matplotlib.pyplot as plt
@@ -225,12 +307,7 @@ def main(seed, num_robots, plot):
         
         fig, ax = plt.subplots()
         
-        origin, cell_centers = place_grid(robot_locations, cell_size, grid_shape)
-        print("Grid Origin (Bottom-Left Corner):", origin)
-        
-        
-        top_right = np.array(origin) + np.array(grid_shape) * cell_size
-        plot_grid(origin, top_right, grid_size=5)
+        plot_grid(bottom_left, top_right, grid_size=grid_size)
         
         # Plot cell centers
         cell_centers = np.array(cell_centers)
@@ -243,13 +320,18 @@ def main(seed, num_robots, plot):
         robot_locations = np.array(robot_locations)
         plt.scatter(robot_locations[:, 0], robot_locations[:, 1], c='r', label='Robot Locations')
         for (x, y) in robot_locations:
-            circle = patches.Circle((x, y), radius=0.5, edgecolor='r', fill=False, linewidth=2)
+            circle = patches.Circle((x, y), radius=roomba_radius, edgecolor='r', fill=False, linewidth=2)
             ax.add_patch(circle)
+            
+        if not two_corner:
+            subgoals = np.array(subgoals)
+            plt.scatter(subgoals[:, 0], subgoals[:, 1], c='orange', marker='^', label='Subgoals')
+            for (x, y) in subgoals:
+                circle = patches.Circle((x, y), radius=roomba_radius, edgecolor='orange', fill=False, linewidth=2)
+                ax.add_patch(circle)
         
         plt.legend(loc='upper left')
         
-        # ax.set_xlim(0, 5)
-        # ax.set_ylim(0, 5)   
         ax.set_aspect('equal')
 
         plt.show()
@@ -266,13 +348,16 @@ if __name__ == "__main__":
     parser.add_argument(
         "--num_robots", 
         type=int, 
-        default=2
+        default=3
     )
     parser.add_argument(
         "--plot", 
-        type=bool, 
-        default=False
+        action='store_true'
+    )
+    parser.add_argument(
+        "--two_corner", 
+        action='store_true'
     )
     args = parser.parse_args()
 
-    main(args.seed, args.num_robots, args.plot)
\ No newline at end of file
+    main(args.seed, args.num_robots, args.plot, args.two_corner)
\ No newline at end of file
-- 
GitLab