Example for how to do large-scale data collection with RotorPy and multiprocessing
This commit is contained in:
443
examples/parallel_data_collection.py
Normal file
443
examples/parallel_data_collection.py
Normal file
@@ -0,0 +1,443 @@
|
||||
from rotorpy.controllers.quadrotor_control import SE3Control
|
||||
from rotorpy.vehicles.multirotor import Multirotor
|
||||
from rotorpy.trajectories.minsnap import MinSnap
|
||||
from rotorpy.vehicles.crazyflie_params import quad_params
|
||||
from rotorpy.environments import Environment
|
||||
from rotorpy.world import World
|
||||
from rotorpy.utils.occupancy_map import OccupancyMap
|
||||
|
||||
import numpy as np # For array creation/manipulation
|
||||
import matplotlib.pyplot as plt # For plotting, although the simulator has a built in plotter
|
||||
from scipy.spatial.transform import Rotation # For doing conversions between different rotation descriptions, applying rotations, etc.
|
||||
import os # For path generation
|
||||
|
||||
import multiprocessing
|
||||
import csv
|
||||
|
||||
from tqdm import tqdm
|
||||
|
||||
import time
|
||||
import datetime
|
||||
|
||||
"""
|
||||
In this script we demonstrate parallel data collection utilizing multiple CPU cores.
|
||||
The script generates random minimum snap trajectories and simulates a quadrotor tracking these trajectories.
|
||||
If the parallel flag is set to True, the script will use multiprocessing to run multiple simulations in parallel.
|
||||
"""
|
||||
|
||||
def sample_waypoints(num_waypoints, world, world_buffer=2, check_collision=True, min_distance=1, max_distance=3, max_attempts=1000, start_waypoint=None, end_waypoint=None):
|
||||
"""
|
||||
Samples random waypoints (x,y,z) in the world. Ensures waypoints do not collide with objects, although there is no guarantee that
|
||||
the path you generate with these waypoints will be collision free.
|
||||
Inputs:
|
||||
num_waypoints: Number of waypoints to sample.
|
||||
world: Instance of World class containing the map extents and any obstacles.
|
||||
world_buffer: Buffer around the world used for sampling. This is used to ensure that waypoints are at least this distance away
|
||||
from the edge of the world.
|
||||
check_collision: If True, checks for collisions with obstacles. If False, does not check for collisions. Checking collisions slows down the script.
|
||||
min_distance: Minimum distance between waypoints consecutive waypoints.
|
||||
max_distance: Maximum distance between consecutive waypoints.
|
||||
max_attempts: Maximum number of attempts to sample a waypoint.
|
||||
start_waypoint: If specified, the first waypoint will be this point.
|
||||
end_waypoint: If specified, the last waypoint will be this point.
|
||||
Outputs:
|
||||
waypoints: A list of (x,y,z) waypoints. [[waypoint_1], [waypoint_2], ... , [waypoint_n]]
|
||||
"""
|
||||
|
||||
if min_distance > max_distance:
|
||||
raise Exception("min_distance must be less than or equal to max_distance.")
|
||||
|
||||
def check_distance(waypoint, waypoints, min_distance, max_distance):
|
||||
"""
|
||||
Checks if the waypoint is at least min_distance away from all other waypoints.
|
||||
Inputs:
|
||||
waypoint: The waypoint to check.
|
||||
waypoints: A list of waypoints.
|
||||
min_distance: The minimum distance the waypoint must be from all other waypoints.
|
||||
max_distance: The maximum distance the waypoint can be from all other waypoints.
|
||||
Outputs:
|
||||
collision: True if the waypoint is at least min_distance away from all other waypoints. False otherwise.
|
||||
"""
|
||||
collision = False
|
||||
for w in waypoints:
|
||||
if (np.linalg.norm(waypoint-w) < min_distance) or (np.linalg.norm(waypoint-w) > max_distance):
|
||||
collision = True
|
||||
return collision
|
||||
|
||||
def check_obstacles(waypoint, occupancy_map):
|
||||
"""
|
||||
Checks if the waypoint is colliding with any obstacles in the world.
|
||||
Inputs:
|
||||
waypoint: The waypoint to check.
|
||||
occupancy_map: An instance of the occupancy map.
|
||||
Outputs:
|
||||
collision: True if the waypoint is colliding with any obstacles in the world. False otherwise.
|
||||
"""
|
||||
collision = False
|
||||
if occupancy_map.is_occupied_metric(waypoint):
|
||||
collision = True
|
||||
return collision
|
||||
|
||||
def single_sample(world, current_waypoints, world_buffer, occupancy_map, min_distance, max_distance, max_attempts=1000, rng=None):
|
||||
"""
|
||||
Samples a single waypoint.
|
||||
Inputs:
|
||||
world: Instance of World class containing the map extents and any obstacles.
|
||||
world_buffer: Buffer around the world used for sampling. This is used to ensure that waypoints are at least this distance away
|
||||
from the edge of the world.
|
||||
occupancy_map: An instance of the occupancy map.
|
||||
min_distance: Minimum distance between waypoints consecutive waypoints.
|
||||
max_distance: Maximum distance between consecutive waypoints.
|
||||
max_attempts: Maximum number of attempts to sample a waypoint.
|
||||
rng: Random number generator. If None, uses numpy's random number generator.
|
||||
Outputs:
|
||||
waypoint: A single (x,y,z) waypoint.
|
||||
"""
|
||||
|
||||
num_attempts = 0
|
||||
|
||||
world_lower_limits = np.array(world.world['bounds']['extents'][0::2])+world_buffer
|
||||
world_upper_limits = np.array(world.world['bounds']['extents'][1::2])-world_buffer
|
||||
|
||||
if len(current_waypoints) == 0:
|
||||
max_distance_lower_limits = world_lower_limits
|
||||
max_distance_upper_limits = world_upper_limits
|
||||
else:
|
||||
max_distance_lower_limits = current_waypoints[-1] - max_distance
|
||||
max_distance_upper_limits = current_waypoints[-1] + max_distance
|
||||
|
||||
lower_limits = np.max(np.vstack((world_lower_limits, max_distance_lower_limits)), axis=0)
|
||||
upper_limits = np.min(np.vstack((world_upper_limits, max_distance_upper_limits)), axis=0)
|
||||
|
||||
waypoint = np.random.uniform(low=lower_limits,
|
||||
high=upper_limits,
|
||||
size=(3,))
|
||||
while check_obstacles(waypoint, occupancy_map) or (check_distance(waypoint, current_waypoints, min_distance, max_distance) if occupancy_map is not None else False):
|
||||
waypoint = np.random.uniform(low=lower_limits,
|
||||
high=upper_limits,
|
||||
size=(3,))
|
||||
num_attempts += 1
|
||||
if num_attempts > max_attempts:
|
||||
raise Exception("Could not sample a waypoint after {} attempts. Issue with obstacles: {}, Issue with min/max distance: {}".format(max_attempts, check_obstacles(waypoint, occupancy_map), check_distance(waypoint, current_waypoints, min_distance, max_distance)))
|
||||
return waypoint
|
||||
|
||||
######################################################################################################################
|
||||
|
||||
waypoints = []
|
||||
|
||||
if check_collision:
|
||||
# Create occupancy map from the world. This can potentially be slow, so only do it if the user wants to check for collisions.
|
||||
occupancy_map = OccupancyMap(world=world, resolution=[0.5, 0.5, 0.5], margin=0.1)
|
||||
else:
|
||||
occupancy_map = None
|
||||
|
||||
if start_waypoint is not None:
|
||||
waypoints = [start_waypoint]
|
||||
else:
|
||||
# Randomly sample a start waypoint.
|
||||
waypoints.append(single_sample(world, waypoints, world_buffer, occupancy_map, min_distance, max_distance, max_attempts))
|
||||
|
||||
num_waypoints -= 1
|
||||
|
||||
if end_waypoint is not None:
|
||||
num_waypoints -= 1
|
||||
|
||||
for _ in range(num_waypoints):
|
||||
waypoints.append(single_sample(world, waypoints, world_buffer, occupancy_map, min_distance, max_distance, max_attempts))
|
||||
|
||||
if end_waypoint is not None:
|
||||
waypoints.append(end_waypoint)
|
||||
|
||||
return np.array(waypoints)
|
||||
|
||||
def compute_cost(sim_result):
|
||||
"""
|
||||
Computes the cost from the output of a simulator instance.
|
||||
Inputs:
|
||||
sim_result: The output of a simulator instance.
|
||||
Outputs:
|
||||
cost: The cost of the trajectory.
|
||||
"""
|
||||
|
||||
# Some useful values from the trajectory.
|
||||
|
||||
time = sim_result['time']
|
||||
x = sim_result['state']['x'] # Position
|
||||
v = sim_result['state']['v'] # Velocity
|
||||
q = sim_result['state']['q'] # Attitude
|
||||
w = sim_result['state']['w'] # Body rates
|
||||
rotor_speeds = sim_result['state']['rotor_speeds'] # Rotor speeds
|
||||
|
||||
x_des = sim_result['flat']['x'] # Desired position
|
||||
v_des = sim_result['flat']['x_dot'] # Desired velocity
|
||||
q_des = sim_result['control']['cmd_q'] # Desired attitude
|
||||
rotor_speeds_des = sim_result['control']['cmd_motor_speeds'] # Desired rotor speeds
|
||||
|
||||
# Write your cost function here. For example this is average position error over the trajectory.
|
||||
sim_cost = np.linalg.norm(x-x_des, axis=1).mean()
|
||||
|
||||
return sim_cost
|
||||
|
||||
def write_to_csv(output_file, row):
|
||||
with open(output_file, 'a', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
writer.writerow(row)
|
||||
return None
|
||||
|
||||
def single_minsnap_instance(world, vehicle, controller, num_waypoints, start_waypoint=None, end_waypoint=None,
|
||||
world_buffer=2, min_distance=1, max_distance=3, vavg=2, random_yaw=True, yaw_min=-0.85*np.pi, yaw_max=0.85*np.pi,
|
||||
seed=None, save_trial=False):
|
||||
"""
|
||||
Generate a single instance of the simulator with a minsnap trajectory.
|
||||
Inputs:
|
||||
world: Instance of World class containing the map extents and any obstacles.
|
||||
vehicle: Instance of a vehicle class.
|
||||
controller: Instance of a controller class.
|
||||
num_waypoints: Number of waypoints to sample.
|
||||
start_waypoint: If specified, the first waypoint will be this point.
|
||||
end_waypoint: If specified, the last waypoint will be this point.
|
||||
world_buffer: Buffer around the world used for sampling. This is used to ensure that waypoints are at least this distance away
|
||||
from the edge of the world.
|
||||
min_distance: Minimum distance between waypoints consecutive waypoints.
|
||||
max_distance: Maximum distance between consecutive waypoints.
|
||||
vavg: Average velocity of the vehicle.
|
||||
random_yaw: If True, the yaw angles will be randomly sampled. If False, the yaw angles will be 0.
|
||||
yaw_min: The minimum yaw angle to sample.
|
||||
yaw_max: The maximum yaw angle to sample.
|
||||
seed: The seed for the random number generator. If None, uses numpy's random number generator.
|
||||
save_trial: If True, saves the trial data to a .csv file.
|
||||
Outputs:
|
||||
output: the cost of the trajectory followed by the polynomial coefficients for the position and yaw.
|
||||
"""
|
||||
|
||||
if seed is not None:
|
||||
np.random.seed(seed)
|
||||
traj_id = seed
|
||||
else:
|
||||
np.random.seed()
|
||||
traj_id = np.random.randint(0, 2**16) # Randomly assign trajectory ID
|
||||
|
||||
# First sample the waypoints.
|
||||
waypoints = sample_waypoints(num_waypoints=num_waypoints, world=world, world_buffer=world_buffer,
|
||||
min_distance=min_distance, max_distance=max_distance,
|
||||
start_waypoint=start_waypoint, end_waypoint=end_waypoint)
|
||||
|
||||
# Sample the yaw angles
|
||||
if random_yaw:
|
||||
yaw_angles=np.random.uniform(low=yaw_min, high=yaw_max, size=len(waypoints))
|
||||
else:
|
||||
yaw_angles=np.zeros(len(waypoints))
|
||||
|
||||
# Generate the minsnap trajectory
|
||||
traj = MinSnap(points=waypoints, yaw_angles=yaw_angles, v_avg=vavg)
|
||||
|
||||
# Now create an instance of the simulator and run it.
|
||||
sim_instance = Environment(vehicle=vehicle, controller=controller, trajectory=traj, wind_profile=None, sim_rate=100)
|
||||
|
||||
# Set the initial state to the first waypoint at hover.
|
||||
x0 = {'x': waypoints[0],
|
||||
'v': np.zeros(3,),
|
||||
'q': np.array([0, 0, 0, 1]), # [i,j,k,w]
|
||||
'w': np.zeros(3,),
|
||||
'wind': np.array([0,0,0]), # Since wind is handled elsewhere, this value is overwritten
|
||||
'rotor_speeds': np.array([1788.53, 1788.53, 1788.53, 1788.53])}
|
||||
|
||||
sim_instance.vehicle.initial_state = x0
|
||||
|
||||
# Now run the simulator for the length of the trajectory.
|
||||
sim_result = sim_instance.run(t_final = traj.t_keyframes[-1],
|
||||
use_mocap=False,
|
||||
terminate=False,
|
||||
plot=False,
|
||||
plot_mocap=False,
|
||||
plot_estimator=False,
|
||||
plot_imu=False,
|
||||
animate_bool=False,
|
||||
animate_wind=False,
|
||||
verbose=False)
|
||||
|
||||
if save_trial:
|
||||
savepath = os.path.join(os.path.dirname(__file__), 'trial_data')
|
||||
if not os.path.exists(savepath):
|
||||
os.makedirs(savepath)
|
||||
sim_instance.save_to_csv(os.path.join(savepath, 'trial_{}.csv'.format(traj_id)))
|
||||
|
||||
# Compute the cost of the trajectory from result
|
||||
trajectory_cost = compute_cost(sim_result)
|
||||
|
||||
# Now extract the polynomial coefficients for the trajectory.
|
||||
pos_poly = traj.x_poly
|
||||
yaw_poly = traj.yaw_poly
|
||||
|
||||
summary_output = np.concatenate((np.array([int(traj_id)]), np.array([trajectory_cost]), pos_poly.ravel(), yaw_poly.ravel()))
|
||||
|
||||
return summary_output
|
||||
|
||||
def generate_data(output_csv_file, world, vehicle, controller,
|
||||
num_simulations, num_waypoints, vavg,
|
||||
random_yaw, yaw_min, yaw_max,
|
||||
world_buffer, min_distance, max_distance,
|
||||
start_waypoint, end_waypoint,
|
||||
parallel=True,
|
||||
save_individual_trials=False):
|
||||
"""
|
||||
Generates data.
|
||||
Inputs:
|
||||
output_file: The name of the output file.
|
||||
world: Instance of World class containing the map extents and any obstacles.
|
||||
vehicle: Instance of a vehicle class.
|
||||
controller: Instance of a controller class.
|
||||
num_simulations: The number of simulations to run.
|
||||
num_waypoints: The number of waypoints to sample.
|
||||
vavg: The average velocity of the vehicle.
|
||||
random_yaw: If True, the yaw angles will be randomly sampled. If False, the yaw angles will be 0.
|
||||
yaw_min: The minimum yaw angle to sample.
|
||||
yaw_max: The maximum yaw angle to sample.
|
||||
world_buffer: Buffer around the world used for sampling. This is used to ensure that waypoints are at least this distance away
|
||||
from the edge of the world.
|
||||
min_distance: Minimum distance between waypoints consecutive waypoints.
|
||||
max_distance: Maximum distance between consecutive waypoints.
|
||||
start_waypoint: If specified, the first waypoint will be this point.
|
||||
end_waypoint: If specified, the last waypoint will be this point.
|
||||
Outputs:
|
||||
None. It writes to the output file.
|
||||
"""
|
||||
|
||||
if not parallel:
|
||||
for _ in tqdm(range(num_simulations), desc="Running simulations (sequentially)..."):
|
||||
result = single_minsnap_instance(world, vehicle, controller,
|
||||
num_waypoints, start_waypoint, end_waypoint,
|
||||
world_buffer, min_distance, max_distance, vavg,
|
||||
random_yaw, yaw_min, yaw_max, seed=None, save_trial=save_individual_trials)
|
||||
|
||||
write_to_csv(output_csv_file, result)
|
||||
|
||||
else:
|
||||
# Use multiprocessing to run multiple simulations in parallel.
|
||||
|
||||
num_cores = min(multiprocessing.cpu_count(), 40)
|
||||
|
||||
print("Running {} simulations in parallel with up to {} cores.".format(num_simulations, num_cores))
|
||||
|
||||
pool = multiprocessing.Pool(num_cores)
|
||||
|
||||
# Use numpy random to generate seeds for each simulation.
|
||||
seeds = np.random.choice(np.arange(num_simulations), size=num_simulations, replace=False)
|
||||
|
||||
manager = multiprocessing.Manager()
|
||||
|
||||
lock = manager.Lock()
|
||||
|
||||
def update_results(result):
|
||||
with lock:
|
||||
write_to_csv(output_file=output_csv_file, row=result)
|
||||
|
||||
code_rate = 1.33 # simulations per second, emperically determined from our machine, but will differ for yours.
|
||||
expected_duration_seconds = num_simulations/code_rate
|
||||
|
||||
current_time = datetime.datetime.now()
|
||||
end_time = current_time + datetime.timedelta(seconds=expected_duration_seconds)
|
||||
|
||||
print(f"Start time: {current_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("Expected duration: %3.2f seconds (%3.2f minutes, or %3.2f hours)" % (expected_duration_seconds, expected_duration_seconds/60, expected_duration_seconds/3600))
|
||||
print(f"Program *may* end around: {end_time.strftime('%Y-%m-%d %H:%M:%S')}, depending on your machine specs, number of waypoints, distance between waypoints, etc.")
|
||||
|
||||
print("Running simulations (in parallel)...")
|
||||
for _ in range(num_simulations):
|
||||
pool.apply_async(single_minsnap_instance, args=(world, vehicle, controller,
|
||||
num_waypoints, start_waypoint, end_waypoint,
|
||||
world_buffer, min_distance, max_distance, vavg,
|
||||
random_yaw, yaw_min, yaw_max, seeds[_], save_individual_trials),
|
||||
callback=update_results)
|
||||
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
return None
|
||||
|
||||
def main(num_simulations, parallel_bool, save_trials=False):
|
||||
"""
|
||||
Main function for generating data.
|
||||
Inputs:
|
||||
num_simulations: The number of simulations to run.
|
||||
parallel_bool: If True, runs the simulations in parallel. If False, runs the simulations sequentially.
|
||||
save_trials: If True, saves each trial data to a separate .csv file. Uses more memory, but allows you to see the results of each trial at a later date.
|
||||
Outputs:
|
||||
None. It writes to the output file.
|
||||
"""
|
||||
|
||||
world_size = 10
|
||||
num_waypoints = 4
|
||||
vavg = 2
|
||||
random_yaw = False
|
||||
yaw_min = -0.85*np.pi
|
||||
yaw_max = 0.85*np.pi
|
||||
|
||||
world_buffer = 2
|
||||
min_distance = 1
|
||||
max_distance = min_distance+3
|
||||
start_waypoint = None # If you want to start at a specific waypoint, specify it using [xstart, ystart, zstart]
|
||||
end_waypoint = None # If you want to end at a specific waypoint, specify it using [xend, yend, zend]
|
||||
|
||||
# Create the output file
|
||||
output_csv_file = os.path.dirname(__file__) + '/data.csv'
|
||||
|
||||
if os.path.exists(output_csv_file):
|
||||
# Ask the user if they want to remove the existing file.
|
||||
user_input = input("The file {} already exists. Do you want to remove the existing file? (y/n)".format(output_csv_file))
|
||||
if user_input == 'y':
|
||||
# Remove the existing file
|
||||
os.remove(output_csv_file)
|
||||
elif user_input == 'n':
|
||||
raise Exception("Please delete or rename the file {} before running this script.".format(output_csv_file))
|
||||
else:
|
||||
raise Exception("Invalid input. Please enter 'y' or 'n'.")
|
||||
|
||||
if save_trials:
|
||||
savepath = os.path.join(os.path.dirname(__file__), 'trial_data')
|
||||
if not os.path.exists(savepath):
|
||||
os.makedirs(savepath)
|
||||
else:
|
||||
# Ask the user if they want to remove the existing files in the directory.
|
||||
user_input = input("The directory {} already exists. Do you want to remove the existing files? (y/n)".format(savepath))
|
||||
if user_input == 'y':
|
||||
# Remove existing files in the directory
|
||||
for file in os.listdir(savepath):
|
||||
os.remove(os.path.join(savepath, file))
|
||||
elif user_input == 'n':
|
||||
raise Exception("Please delete or rename the files in the directory {} before running this script.".format(savepath))
|
||||
else:
|
||||
raise Exception("Invalid input. Please enter 'y' or 'n'.")
|
||||
|
||||
# Append headers to the output file
|
||||
with open(output_csv_file, 'w', newline='') as file:
|
||||
writer = csv.writer(file)
|
||||
# This depends on the number of waypoints and the order of the polynomial. Currently pos is 7th order and yaw is 7th order.
|
||||
writer.writerow(['traj_number'] + ['cost']
|
||||
+ ['x_poly_seg_{}_coeff_{}'.format(i,j) for i in range(num_waypoints-1) for j in range(8)]
|
||||
+ ['y_poly_seg_{}_coeff_{}'.format(i,j) for i in range(num_waypoints-1) for j in range(8)]
|
||||
+ ['z_poly_seg_{}_coeff_{}'.format(i,j) for i in range(num_waypoints-1) for j in range(8)]
|
||||
+ ['yaw_poly_seg_{}_coeff_{}'.format(i,j) for i in range(num_waypoints-1) for j in range(8)])
|
||||
|
||||
# Now create the world, vehicle, and controller objects.
|
||||
world = World.empty([-world_size/2, world_size/2, -world_size/2, world_size/2, -world_size/2, world_size/2])
|
||||
vehicle = Multirotor(quad_params)
|
||||
controller = SE3Control(quad_params)
|
||||
|
||||
# Generate the data
|
||||
start_time = time.time()
|
||||
generate_data(output_csv_file, world, vehicle, controller,
|
||||
num_simulations, num_waypoints, vavg,
|
||||
random_yaw, yaw_min, yaw_max,
|
||||
world_buffer, min_distance, max_distance,
|
||||
start_waypoint, end_waypoint,
|
||||
parallel=parallel_bool,
|
||||
save_individual_trials=save_trials)
|
||||
end_time = time.time()
|
||||
print("Time elapsed: %3.2f seconds, parallel: %s" % (end_time-start_time, parallel_bool))
|
||||
|
||||
return None
|
||||
|
||||
if __name__=="__main__":
|
||||
|
||||
main(num_simulations=10, parallel_bool=True, save_trials=True)
|
||||
Reference in New Issue
Block a user