From 88eaa391bf076e603a45c6ab728e937bd11640da Mon Sep 17 00:00:00 2001
From: "oceanpdoshi@g.ucla.edu" <oceanpdoshi@.ucla.edu>
Date: Tue, 23 Apr 2019 13:37:33 -0700
Subject: [PATCH] -fixed robot_vision, -working on incorporating multiple
 landmarks -working on incorporating multiple robots -working on handling of
 end of simulation vs. no meas. data available

---
 .../simulated_dataset_manager.py              | 168 ++++++++++--------
 1 file changed, 97 insertions(+), 71 deletions(-)

diff --git a/CoLo-AT/dataset_manager/simulated_dataset_manager.py b/CoLo-AT/dataset_manager/simulated_dataset_manager.py
index 2651dc5..3155991 100644
--- a/CoLo-AT/dataset_manager/simulated_dataset_manager.py
+++ b/CoLo-AT/dataset_manager/simulated_dataset_manager.py
@@ -18,25 +18,33 @@ from requests.request_response import Request_response
 class SimulatedDataSetManager:
 
     def __init__(self, dataset_name, boundary, landmarks, duration, robot_labels,
-    velocity_noise = 0, angular_velocity_noise = 0, measurement_range_noise = 0, bearing_noise = 0,  communication_noise = 0, robot_vision = pi/3):
+    velocity_noise = 0, angular_velocity_noise = 0, measurement_range_noise = 0, bearing_noise = 0,  communication_noise = 0, 
+    robot_fov = 2*pi/3):
         
-        self.name = dataset_name
 
-        # Alternates between 'odometry' and 'measurement'
-        self.next_request = 'odometry'
-        
+        self.name = dataset_name
         self.duration = duration
         self.boundary = boundary
         self.robot_labels = robot_labels
         self.num_robots = len(robot_labels)
+        self.start_time = 20.0
+        self.end_time = self.start_time + self.duration
+        self.start_moving_times = [self.start_time for label in robot_labels]
+        
+        # angular width of robots's vision with respect to robot's x_axis (radians)
+        # robot_fov = angular width (radians) of robot's view in reference to x_axis
+        # ex: robot_fov = 2pi/3 means 60 degrees above and below x-axis are within vision
+        self.robot_fov = robot_fov
         
-        # Internal time "counter"
+        # Alternates between 'odometry' and 'measurement'
+        self.next_request = 'odometry'
+        # cycles through robot_labels
+        self.current_robot_idx = 0
+        
+        # Internal time index/counter
         self.time_idx = 0
         self.delta_t = 1.0 
         
-        # angular width of robots's vision with respect to the x_axis (radians)
-        self.robot_vision = robot_vision
-        
         # User-specified noises, if none given: defaulted to 0
         self.velocity_noise = velocity_noise
         self.angular_velocity_noise = angular_velocity_noise
@@ -44,22 +52,19 @@ class SimulatedDataSetManager:
         self.bearing_noise = bearing_noise
         self.communication_noise = communication_noise
 
-        # All robots begin moving at t = 0s
-        self.start_time = 20.0
-        self.end_time = self.start_time + self.duration
-        self.start_moving_times = [0 for label in robot_labels]
-
         # landmarks = {landmark ID: [x,y]}
-        # NOTE - landmark IDs must be bigger than 5.
         self.landmark_map = landmarks
+        for landmark_id in landmarks:
+            if landmark_id < 5:
+                raise Exception("Invalid landmark ID: landmark IDs must be bigger than 5.")
 
         # starting_states =	{robot label: [time, x_pos, y_pos, orientation], ... }
-        # TODO - Update this to be user-specified on how to initialize robot locations. (esecially in more advanced movement patterns)
-        self.starting_states = {label : [self.start_time, float(i), 1.0, 0.0] for i,label in enumerate(robot_labels)}
-        
-        # in the simulation all the times for all the measurements are the same.
-        self.time_arr = {'odometry': [[] for robot in robot_labels], 'measurement': [[] for robot in robot_labels], 'groundtruth': [[] for robot in robot_labels]}
+        # TODO - modify generation of starting tates
+        self.starting_states = {label : [self.start_time, float(i), float(i), 0.0] for i,label in enumerate(robot_labels)}
 
+        # NOTE - length of odometry and groundtruth is fixed to be duration
+        # measurment can be greater or equal length depending on number of measurments made
+        self.time_arr = {'odometry': [[] for robot in robot_labels], 'measurement': [[] for robot in robot_labels], 'groundtruth': [[] for robot in robot_labels]}
         for data_type in self.time_arr:
             for i in range(self.num_robots):
                 self.time_arr[data_type][i] = np.arange(self.start_time, self.end_time)
@@ -77,7 +82,7 @@ class SimulatedDataSetManager:
             'bearing': 0.29890824572449504
         }
         '''
-        self.measurement_data = [[] for i in range(self.num_robots)]
+        self.measurement_data = [[] for robot in range(self.num_robots)]
         
         # Odometry Data Format
         '''
@@ -89,9 +94,9 @@ class SimulatedDataSetManager:
             'delta_t': 0.09999990463256836
         }
         '''
-        self.odometry_data = [[] for i in range(self.num_robots)]
+        self.odometry_data = [[] for robot in range(self.num_robots)]
 
-        # ground_truth formatting
+        # Groundtruth format
         # ORIENTATION DEFINED WITH RESPECT TO GLOBAL FRAME OF REFERENCE
         '''
         {
@@ -101,18 +106,28 @@ class SimulatedDataSetManager:
             'orientation': 0.03251611228318508
         }
         '''
-        self.groundtruth_data = [ [] for i in range(self.num_robots)]
+        self.groundtruth_data = [ [] for robot in range(self.num_robots)]
+
+    def simulate_dataset(self, path='line'):
+        if (path == 'line'):
+            return self.generate_straight_line_data()
+        # TODO
+        elif(path=='circle'):
+            pass
+        else:
+            raise Exception("Unsupported robot path specified.")
 
-    def simulate_dataset(self):
+    # TODO - create a separate class file that does data generation
+    def generate_straight_line_data(self):
+        
         # Generate ground truth data
-        # NOTE - FOR NOW THIS WILL JUST MOVE ROBOTS IN A STRAIGHT LINE UNTIL THEY REACH THE BOUNDARY - THEN THEY WILL BE STATIONARY
         for i, label in enumerate(self.robot_labels, 0):
             # append starting state
             self.groundtruth_data[i].append({'time' : self.starting_states[label][0], 'x_pos': self.starting_states[label][1], 'y_pos': self.starting_states[label][2], 'orientation' : self.starting_states[label][3]})
-            for j in range(1, len(self.time_arr['groundtruth'][i])):
-                curr_x = self.groundtruth_data[i][j-1]['x_pos']
-                curr_y = self.groundtruth_data[i][j-1]['y_pos']
-                curr_orientation = self.groundtruth_data[i][j-1]['orientation']
+            for time_idx in range(1, len(self.time_arr['groundtruth'][i])):
+                curr_x = self.groundtruth_data[i][time_idx-1]['x_pos']
+                curr_y = self.groundtruth_data[i][time_idx-1]['y_pos']
+                curr_orientation = self.groundtruth_data[i][time_idx-1]['orientation']
 
                 next_x = curr_x
                 next_y = curr_y
@@ -122,38 +137,45 @@ class SimulatedDataSetManager:
                    next_x = curr_x + 1
                    next_y = curr_y
                 
-                self.groundtruth_data[i].append({'time' : self.time_arr['groundtruth'][i][j], 'x_pos': next_x, 'y_pos': next_y, 'orientation': next_orientation})
+                self.groundtruth_data[i].append({'time' : self.time_arr['groundtruth'][i][time_idx], 'x_pos': next_x, 'y_pos': next_y, 'orientation': next_orientation})
        
-        # NOTE - modify incorporation of noise
         # Generate Odometery Data
         for i, label in enumerate(self.robot_labels, 0):
             # Initial robot state
             self.odometry_data[i].append({'time': self.start_time, 'velocity' : 0, 'angular velocity' : 0, 'orientation' : self.groundtruth_data[i][0]['orientation'],
              'delta_t' : 0})
-            for j in range(1, len(self.time_arr['odometry'][i])):
-                # NOTE - delta_t is currently hard coded to 1
-                loc1 = [self.groundtruth_data[i][j-1]['x_pos'], self.groundtruth_data[i][j-1]['y_pos']]
-                loc2 = [self.groundtruth_data[i][j]['x_pos'], self.groundtruth_data[i][j]['y_pos']]
+            for time_idx in range(1, len(self.time_arr['odometry'][i])):
+                loc1 = [self.groundtruth_data[i][time_idx-1]['x_pos'], self.groundtruth_data[i][time_idx-1]['y_pos']]
+                loc2 = [self.groundtruth_data[i][time_idx]['x_pos'], self.groundtruth_data[i][time_idx]['y_pos']]
                 velocity = self.calc_distance(loc1, loc2)/self.delta_t + np.random.normal(loc=0.0,scale=self.velocity_noise)
-                theta_1 = self.groundtruth_data[i][j-1]['orientation']
-                theta_2 = self.groundtruth_data[i][j]['orientation']
+                
+                theta_1 = self.groundtruth_data[i][time_idx-1]['orientation']
+                theta_2 = self.groundtruth_data[i][time_idx]['orientation']
                 angular_velocity = (theta_2-theta_1)/self.delta_t + np.random.normal(loc=0.0,scale=self.angular_velocity_noise)
 
-                self.odometry_data[i].append({'time': self.time_arr['odometry'][i][j], 'velocity' : velocity, 'angular velocity' : angular_velocity, 
-                'orientation' : self.groundtruth_data[i][j]['orientation'], 'delta_t' : self.delta_t})
+                self.odometry_data[i].append({'time': self.time_arr['odometry'][i][time_idx], 'velocity' : velocity, 'angular velocity' : angular_velocity, 
+                'orientation' : self.groundtruth_data[i][time_idx]['orientation'], 'delta_t' : self.delta_t})
 
         # Generate Measurement Data
-        for i, label in enumerate(self.robot_labels, 0):
-            for j in range(0, len(self.time_arr['measurement'][i])):
-                for landmarkID, landmark_loc in self.landmark_map.items():
-                    robot_loc_x = self.groundtruth_data[i][j]['x_pos']
-                    robot_loc_y = self.groundtruth_data[i][j]['y_pos']
+        for robot_idx, label in enumerate(self.robot_labels, 0):
+            # NOTE - time_arr is used to emplace a one-to-one index-to-time correspondence 
+            # between measurement_data and time_arr['measurement'] to account for varing number of landmark observations
+            time_arr = []
+            # iterate over all possible times and (potentially) observable landmarks
+            for time_idx in range(0, len(self.time_arr['measurement'][i])):
+               for landmarkID, landmark_loc in self.landmark_map.items():
+                    
+                    time = self.time_arr['measurement'][robot_idx][time_idx]
+                    robot_loc_x = self.groundtruth_data[robot_idx][time_idx]['x_pos']
+                    robot_loc_y = self.groundtruth_data[robot_idx][time_idx]['y_pos']
                     robot_loc = [robot_loc_x, robot_loc_y]
-                    if (True):
+                    
+                    if (self.within_vision(self.robot_fov, robot_loc, landmark_loc)):
                         measurement_range = self.calc_distance(robot_loc, landmark_loc) + np.random.normal(loc=0.0,scale=self.measurement_range_noise)
                         bearing = atan2((landmark_loc[1]-robot_loc[1]), (landmark_loc[0]-robot_loc[0])) + np.random.normal(loc=0.0,scale=self.bearing_noise)
-                        self.measurement_data[i].append({'time': self.time_arr['measurement'][i][j], 'subject_ID': landmarkID, 'measurment_range':measurement_range, 'bearing':bearing})
-                
+                        self.measurement_data[robot_idx].append({'time': time, 'subject_ID': landmarkID, 'measurment_range':measurement_range, 'bearing':bearing})
+                        time_arr.append(time) # we expect repeats/skipped times in time array
+            self.time_arr['measurement'][robot_idx] = time_arr
         self.dataset_data = {'odometry': self.odometry_data, 'measurement': self.measurement_data, 'groundtruth': self.groundtruth_data}
         return self.start_time, self.starting_states, self.dataset_data, self.time_arr
 
@@ -184,9 +206,7 @@ class SimulatedDataSetManager:
 
     def respond(self, req, current_time, need_specific_time = False):
 
-        valid_respond, message, req_type, robot_idx, time_idx = self.get_dataline(req, current_time)
-
-        # current_time == self.time_arr[req_type][robot_idx][time_idx]
+        valid_respond, message, req_type, robot_idx = self.get_dataline(req, current_time)
 
         if (valid_respond):
             req.set_time(current_time)
@@ -194,15 +214,18 @@ class SimulatedDataSetManager:
             req.set_robot_idx(robot_idx)
             req.set_type(req_type)
 
+        # TODO - add in update parameters for multiple robots (and robot_ctr about to reset)
         if (req_type == 'measurement'):
             current_time += self.delta_t
 
         return valid_respond, current_time, req
 
-    # Alternates odometry and measurement updates 
+    # Alternates odometry and measurement updates
+    # Cycles through multiple robots
     def get_dataline(self, req, current_time):
         message = req.get_message()
 
+        # Selection of req_type, robot_idx
         if message['robot_index'] == None:
             if req.get_type() == None:
                 # Alternate between measurement and odometry requests
@@ -221,22 +244,22 @@ class SimulatedDataSetManager:
             
             message['robot_index'] = robot_idx
 
-        time_idx = np.where(self.time_arr[req_type][robot_idx] == current_time)[0][0]
-        valid_dataline= True 
-
-        # TODO - handling of case where no data is available
-        if req_type == 'measurement' and (time_idx >= len (self.dataset_data[req_type][robot_idx])):
-            valid_dataline = False
+        # Selection of time_idx
+        
+        # Stop simulation when you are the end of the dataset (no more measurements or groundtruths/odometry)
+        valid_dataline = current_time <= self.end_time and current_time <= max(self.time_arr['measurement'][robot_idx])
 
         if valid_dataline:
             if req_type == 'odometry':
-                message['data'] = self.odometry_data[robot_idx][time_idx]
+                odo_time_idx = np.where(self.time_arr[req_type][robot_idx] == current_time)[0][0] 
+                message['data'] = self.odometry_data[robot_idx][odo_time_idx]
+            # if no measurement data available (ex: no landmarks were in range) message['data'] = None
             elif req_type == 'measurement':
-                message['data'] = self.find_measurement(current_time, robot_idx) # there may exist a built-in function to do a linear search through a list of dictionaries
-
+                message['data'] = self.find_measurement(current_time, robot_idx)
 
-        message['groundtruth'] = self.groundtruth_data[robot_idx][time_idx]
-        return valid_dataline, message, req_type, robot_idx, time_idx
+        gt_time_idx = np.where(self.time_arr['groundtruth'][robot_idx] == current_time)[0][0]
+        message['groundtruth'] = self.groundtruth_data[robot_idx][gt_time_idx]
+        return valid_dataline, message, req_type, robot_idx
         
     # Check if a given loc [x,y] is within map
     def within_map(self, loc): 
@@ -258,18 +281,21 @@ class SimulatedDataSetManager:
         distance = sqrt((x1-x2)**2 + (y1-y2)**2)
         return distance
 
-    # TODO - Check this function
-    # robot_vision = angular width of robot's view in reference to x_axis
-    # robot_loc & landmark_loc = [x,y]
-    def within_vision(self, robot_vision, robot_loc, landmark_loc):
-
-        bearing = atan2((landmark_loc[1]-robot_loc[1]),(landmark_loc[0]-robot_loc[0])) + np.random.normal(loc=0.0,scale=0.1)
-        return abs(bearing) < robot_vision/2
+    # TODO - add distance limitations to robot_vision
+    # [x,y] for landmark and robot_loc
+    def within_vision(self, robot_fov, robot_loc, landmark_loc):
+        
+        # between pi and -pi with respect to the x axis
+        bearing = atan2((landmark_loc[1]-robot_loc[1]),(landmark_loc[0]-robot_loc[0]))
+        
+        return abs(bearing) <= robot_fov/2
 
+    # TODO - add handling for multiple landmarks
+    # TODO - add handling for no measurement available (but not at end of simulation)
     def find_measurement(self, current_time, robot_idx):
         for measurement in self.measurement_data[robot_idx]:
             if (measurement['time'] == current_time):
                 return measurement
 
-        # TODO - handling of case where no measurmeent data available (ex: no landmarks in vision at given time)
+        # No measurement data available (ex: no landmarks in vision at given time)
         return None
\ No newline at end of file
-- 
GitLab