Conveyor Repeatability Tests @ Amazon Robotics
When I was at Amazon Robotics, I had to determine how to control the stopping position of totes on automated conveyor rollers. This was important for one of Amazon Robotics’ workcells, where totes needed to land +0/-60mm from the end of a conveyor so another robot could consistently pick the tote up. To figure out this tote-control problem, I wrote a PLC latter logic program to move the totes and changed variables, like speed, deceleration distance, and tote weight. I initially measured where the totes stopped manually and got some preliminary results. I soon realized this procedure was tedious and could not collect certain kinds of data. So, I applied my robotics skills to automate my experiment. I used a distance sensor to do the measuring for me and created a Python script that parsed through days’ worth of data at a time. After 46 tests, I found several correlations between conveyor parameters and the stopped position of the tote. I created a linear regression model that would intake any combination of settings and predict where the tote would stop. The result of my comprehensive analysis helped my team better understand tote control and laid the groundwork for future automated tote tests.
The outfeed conveyor on one of Amazon Robotics’ stations needed all totes, which carry inventory items, to stop within +0/-60 mm from the front edge of the station’s front frame. Our team needed to understand how parameters on a conveyor system (e.g. speed, deceleration) impact where the tote stops on the outfeed conveyor.
Goals
Get a list of parameters that impact the tote’s ability to stop within +0/-60 mm from the front edge of the frame
Assess how certain edge conditions (e.g. highly frontloaded tote) affect where the tote lands
Develop a PLC program that uses the existing conveyor control boards to move a tote forward and backward
Develop a method to predict where the tote will stop and the position’s tolerance
Non-goals
Performing hundreds of tests over several months. The test was dependent on shared resources that other teams needed for their projects. Therefore, the experiment needed to happen within the span of 1-2 months at most.
Changing the design of the conveyor. The conveyor design was completed, and many other system components were dependent on it. Changing the design would reverse the progress the team had made.
Conveyor Documentation
The conveyors for this station used proprietary control boards, which had their own software packages. The software packages included a control board configurator and a PLC ladder-logic program writer. The supplier also provided documentation describing how to adjust the speed and acceleration of the rollers if photoeyes turned on or off. So, I made my program move totes back and forth depending on whether the front or back photoeye detected a tote above it.
Figure 1 - Illustration of the Outfeed Test Setup on Open Conveyors
Pseudocode
To check the stopped position of the tote, it needed to move forward at a set velocity and then stop. So, I wrote a program using PLC ladder logic and two sensors that caused the tote to move forward at a set speed, then slowly roll backwards to its starting position, ready to move forward again:
turn off motors; MachineState = 0; //Do nothing state while(True) //Loop forever if FirstSensor == T and MachineState != 1, then turn off rollers; wait 3 seconds; RollerSpeed = 610; // mm/s MotorDirection = Forward; turn on motors; MachineState = 1; //Roll forward state else if SecondSensor == T and MachineState != 2, then turn off motors; wait 7 seconds; RollerSpeed = 15; // mm/s MotorDirection = Backward; turn on motors; MachineState = 2; //Roll backward state
The result of this pseudocode code was the following motion:
Video 1 - Tote rolling backwards and forwards on a set of outfeed rollers.
Tote Stopping Theory
The parameters I would set in the configurator, like speed or acceleration, may not match what happens physically with the rollers. So, I used a tachometer and got calibration curve between set roller speeds and actual roller speeds. This helped me convert the value for speed/acceleration in the configurator to the actual speed/acceleration of the rollers. I also looked at how tote behavior changed when accelerations and decelerations were high. In such situations, the tote may trigger a sensor before reaching the desired speed. This could have skewed my data and made it seem like fast roller speeds could not move tote quickly. So, I created an Excel calculator to check whether a set speed and acceleration would reach the desired speed before performing a test. The figures adjacent show velocity vs. position and time and show the difference between a tote getting up to speed and a tote not getting up to speed.
Figure 2 - Velocity of a tote vs time and distance when reaching two desired constant speeds
Figure 3 - Velocity of a tote vs time and distance when not reaching two desired constant speeds
Proposed solution
The high-level approach I took was to make totes move back and forth across a conveyor system and to measure where they stopped. I changed conveyor parameters and measured the closest point the tote got to the edge (overshoot) and the resting position of the tote (stopping position). The variables I tested included: Tote weight, Tote type, O-ring type, Speed, Deceleration, Braking method, Tote weight distribution
Manual Procedure
I initially measured the position manually by taping a tape measure to the side of the outfeed frame and checking the position of the tote using a thin meter stick. I made it such that when the conveyor moved the tote forward and stopped, I would place the meter stick against the tote’s edge and read where it landed along the tape measure. Then, I manually recorded the value in an Excel sheet while the tote automatically rolled back for another measurement. The video below shows one cycle of this procedure. I repeated it 30 times for every set of parameters I wanted to test. It was admittedly a tedious process.
Video 2 - Demonstration of the manual procedure
What Functional Goals Did I Achieve?
This procedure gave me a general understanding of how the conveyor system worked and an intuition for what speeds the conveyors could work with. I learned:
How different O-ring materials impacted the movement of the conveyor.
How weight impacted the variation of the tote’s stopped position.
What Needed Fixing?
After the first few tests, my interest in taking the measurements manually wore off. I could only take a maximum of 30 samples per test, or else I would be spending hours of my time just collecting data. Additionally, I only knew the tote's final stopped position, not the closest it got to the end of the frame. So, I decided to put my engineering skills to work and automate this procedure.
Automated Procedure
My automated procedure allowed me to measure all sorts of new things. It could measure the tote’s position with millimeter precision, measure the closest the tote got to the outfeed edge, measure where it stopped, steam the data to a laptop for later analysis, and do all of this for days at a time. From a high level, I designed two automated systems: A) the conveyor, which moved the totes back and forth with photoeyes, and B) a distance measuring setup that observed the tote and sent data to a laptop.
I then selected a set of components that could make this automated setup a reality. I chose an LM550-KQP laser distance sensor from Banner Engineering and mounted it 200mm from the end of the conveyor. This was a great sensor because it could measure up to a meter away and do it precisely. The sensor streamed its data to a DCMR90-4K controller, which then forwarded the data to a laptop over ethernet using Modbus/TCP. I then had the laptop run a third-party software called Modbus Poll to collect distance and time data points and log them onto a text file.
This setup allowed me to run tests and collect data without having to check every measurement with a meter stick. I could collect lots of data about the tote’s behavior while being able to work on other tasks. Automating the procedure was a huge boost in efficiency.
Figure 4 - Illustration of the automated setup on existing rollers
Video 3 - Demonstration of the automated procedure
Whenever a tote moved on the rollers and stopped at the end of the outfeed, it would overshoot and then settle at a stable position. To track this overshoot and settling effect, I wrote a Python script that parsed through the distance vs time text files that Modbus Poll would output. The results of one test, which ran for about 40 minutes, looked like the following figure. Each red dot was where the tote got closest to the distance sensor. Each green dot was the tote’s settled position. I found these points by tracking when the direction of speed switched and picking the median between the tote overshooting and moving back. The yellow highlighted region captures one forward-backward movement. The pseudocode for my script was as follows. The full code can be found in Appendix A below.
file = get "file"; tote_info = get_tote_info(file); //Gets filtering information for tote type in file name time, distance, delta_distance = extract_data(file, tote_file); //Reads raw data file overshoot, overshoot_times, stopped_pos, stopped_pos_times = ... ... get_overshoot_stopped(time, distance, delta_distance); export(overshoot, overshoot_times, stopped_pos, stopped_pos_times); //Outputs the ... ... //overshoot and stopped positions and their times into a text file show_data(file, time, distance, overshoot, overshoot_times, stopped_pos,... ... stopped_pos_times); // Dispays the data in a graph function get_overshoot_stopped(time, distance, delta_distance): while not_end_of_file: if delta_distance goes from negative to positive at time t: log distance into overshoot array; log time into overshoot_time array; log all distances and times for the next X seconds into a tmp array log the median distance of the tmp array into stopped_pos array; log the median time of the tmp array into the stopped_position_time array;
Figure 5 - Plots of the tote position over time with each movement cycle highlighted in yellow
What Functional Goals did I Achieve?
This procedure gave many advantages over the manual version of the procedure:
Tests could run for hours instead of minutes because the Modbus Poll program removed any need for human participation in the procedure.
The data was much more fidelity— instead of measuring just the stopped position, I was able to measure both the overshoot and stopped position.
What Needed Fixing?
Although this experiment tested many parameters to assess how reliable a tote stopped on rubber rollers, there were many open questions and opportunities for improving testing. Such as:
What if there were three sensors in the outfeed such that the tote would move fast, slow down to a fixed speed, and then slow to a stop?
What happens when totes become worn down, and their base has a different texture?
Results
What went well
I successfully collected thousands of data points across 46 tests varying tote type, weight, speed, and deceleration.
I successfully assessed the effects of different braking methods, how weight is distributed within the tote, and how O-rings with different durometer ratings are compared.
I obtained a linear regression equation for predicting the tote's overshoot and stopping position given its weight, speed, tote type, and deceleration.
I used this to create a calculator for checking different settings for tote behavior.
I used bar charts, scatter plots, and trendlines to understand the relationship between stopping position and weight, weight distribution, and accelerations.
I applied ANOVAs to check whether there were significant differences between the tests
What needed improvement
Since R^2 values were about 0.7 for the linear regression model, I should have done more tests with more setting variation. This could be optimized with a Design of Experiments (DOE).
Overall, the over 111,000 overshoot and stopping position data points improved our understanding of how different settings affect where the tote stops on the outfeed conveyor. I performed analysis, including ANOVAs, linear regressions, and trendlines, to create calculators that predicted where the tote would overshoot and where it would stop.
Conclusion
In conclusion, I investigated tote-stopping behavior on Amazon Robotics’ outfeed conveyors and found valuable insights. These insights uncovered how parameters like tote weight, tote type, O-ring type, speed, and deceleration distances affected where the tote stopped and where it got closest to the conveyor edge. I initially did my experiment by hand with a tape measure and meter stick, but I soon used my skills to automate the procedure. I used PLC programs, configured proprietary control boards, and read distance sensors over Modbus/TCP. These changes significantly enhanced efficiency and allowed me to collect hundreds of thousands of data points for days at a time. Although my linear regression model had a small correlation R^2, my experiment provided a deeper understanding of the conveyor system's dynamics, enabled my team to make data-informed decisions, and set the groundwork for future automated tests. The skills I learned include:
Automated tote stopping on conveyors for multi-day experiments using PLC ladder logic programs
Originated Python scripts to parse through tote position data and visualize patterns over distance and time
Performed linear regression and ANOVAs to assess how tote speeds, weight, and types impacted stopping
Authored procedures and troubleshooting steps that others used to automate containerized storage tests
Appendix A: Full Data-Parsing Python Script
import os import matplotlib.pyplot as plt import csv SENSOR_OFFSET = 0 # Distance of sensor from edge of outbound conveyor in mm SITTING_TIME = 4.5 # Time between rollers turning off and turning on again SHCT_TOTE_OFFSET = 10.72 # Distance between where distances are measured and the front-most edge of the SHCT tote NYT_TOTE_OFFSET = 20.08 # Distance between where distances are measured and the front-most edge of the NYT tote SHCT_TOTE_FILTER = 190 # Distance threshold for data points to ignore for SHCT totes NYT_TOTE_FILTER = 270 # Distance threshold for data points to ignore for NYT totes FILE_NAME = 'R34lb_NYT_2S_SBrake_669.2mms_107.2acdc.txt' INPUT_DIR_NAME = 'C:/input_dir' OUTPUT_DIR_NAME = 'C:/output_dir' SHOW_INFO = True # Toggle option for showing data EXPORT_DATA = False # Toggle option for exporting data as a .csv ANALYZE_ALL_FILES = False # Toggle option for analyzing multiple files or a select file with the name FILE_NAME BASELINE_MEASUREMENT = True # Toggle option for analyzing the data file as a baseline if BASELINE_MEASUREMENT: SENSOR_OFFSET = 0 def show_data(filename: str, time: list, distance: list, overshoot: list, overshoot_times: list, stopped_pos: list, stopped_pos_times: list): """ Displays the number of overshoot, the number of stopped positions, the overshoot list, and the stopped position list in the console. Also displays measured distances over time in a plot with highlighted overshoot points, stopped position points, and the range of zero acceleration. :param filename: the name of the .txt file that contains distance over time :param time: a list of times for each distance and change in distance in seconds :param distance: a list of distances that were measured in millimeters :param overshoot: the minimum points for when a tote stops close to the distance sensor :param overshoot_times: the times for when the overshoots occur :param stopped_pos: the settling position for when a tote stops close to a distance sensor :param stopped_pos_times: the times for when the stopped positions occur :return: break condition """ if not SHOW_INFO: return print(str(len(overshoot)) + " Overshoots\t\t\t\t\t" + "Overshoot (mm): " + str(overshoot)) print(str(len(stopped_pos)) + " Stopped Positions\t" + "Stopped Positions (mm): " + str(stopped_pos)) fig1, p1 = plt.subplots(1, 1) p1.plot(time, distance) for i in range(len(overshoot_times)): p1.axvspan(overshoot_times[i], overshoot_times[i] + (stopped_pos_times[i] - overshoot_times[i]) / 0.5, alpha=0.2, color='y') p1.scatter(overshoot_times, overshoot, c='r') p1.scatter(stopped_pos_times, stopped_pos, c='g') p1.set_xlabel('Time (s)') p1.set_ylabel('Distance (mm)') plt.suptitle(filename) plt.get_current_fig_manager().resize(2000, 1000) # Set your screen resolution here plt.show() plt.close('all') def export_data(filename: str, array1: list, array2: list, array3: list): """ Exports data from three arrays into three columns of a .csv with the name 'filename' :param filename: Name of the file to export :param array1: Array of values for column 1 :param array2: Array of values for column 2 :return: None """ if not EXPORT_DATA: return try: with open(os.path.abspath(os.path.join(OUTPUT_DIR_NAME, filename[:-4] + '.csv')), 'w', newline='') as file: writer = csv.writer(file) for i in range(len(array1)): writer.writerow([array1[i], array2[i], array3[i]]) except PermissionError: print("\n================================================" "\nPERMISSION ERROR: CLOSE THE FILE AND RUN AGAIN!" "\n================================================") def get_time_info(data: list, shift: bool, line_num: int, time_init: float) -> tuple[float, float, bool]: """ Converts the time for each data point from hh:mm:ss.ss to ss.ss and tracks the initial time, actual time, and whether a test ran past midnight :param data: raw time and distance data from the .txt file as a list :param shift: a boolean for whether the test ran past midnight :param line_num: the row number within the .txt file :param time_init: the starting time of the test from the first data point :return: the actual time in ss.ss format, the initial time in ss.ss format, and the shifting condition """ logged_time = data[0].strip().split(':') if logged_time[0] == "00": shift = True if shift: logged_time[0] = str(int(logged_time[0]) + 24) time_actual = float(logged_time[0]) * 60 * 60 + float(logged_time[1]) * 60 + float(logged_time[2]) if line_num == 0: time_init = time_actual return time_actual, time_init, shift def extract_data(filename: str, tote_info: tuple) -> tuple[list[float], list[float], list[float]]: """ Extracts time, distance, and change in distance as three lists from a file given the type of tote used in a test :param filename: the name of the .txt file that contains distance over time :param tote_info: a tuple of the tote name as a string, the value of a data filter, and the tote's offset distance :return: all data points as lists for time in seconds, distance in millimeters, and change in distance """ time = [] distance = [] delta_dist = [] time_init = float() distance_prior = float() shift = False with open(os.path.abspath(os.path.join(INPUT_DIR_NAME, filename)), 'r') as file: for line_num, line in enumerate(file): data = line.strip().split(',') time_actual, time_init, shift = get_time_info(data, shift, line_num, time_init) if float(data[1]) / 8000 < (tote_info[1] + SENSOR_OFFSET): time.append(time_actual - time_init) distance.append(float(data[1]) / 8000 - SENSOR_OFFSET - tote_info[2]) delta_dist.append(float(data[1]) / 8000 - SENSOR_OFFSET - tote_info[2] - distance_prior) distance_prior = float(data[1]) / 8000 - SENSOR_OFFSET - tote_info[2] return time, distance, delta_dist def get_tote_info(filename: str) -> tuple[str, int, float]: """ Checks the file name for the type of Amazon tote used during the test and gets properties based on the tote :param filename: the name of the .txt file that contains distance over time :return: a tuple of the tote name as a string, the value of a data filter, and the tote's offset distance """ file_info = filename.strip().split('_') if BASELINE_MEASUREMENT: return "BASELINE", 1000, 0 elif file_info[1] == "SHCT": return "SHCT", SHCT_TOTE_FILTER, SHCT_TOTE_OFFSET elif file_info[1] == "NYT": return "NYT", NYT_TOTE_FILTER, NYT_TOTE_OFFSET else: print("\n================================================" "\nNAMING ERROR: UNKNOWN TOTE TYPE \"" + file_info[1] + "\"\nIN FILE NAMED: \"" + filename + "\"\n================================================") return "ERR", 1000, 0 def get_overshoot_stopped(time: list, distance: list, delta_dist: list) -> tuple[list, list, list, list]: """ Takes an array of distance, the change in distance, and time and calculates overshoot positions, stopped positions, and when they occur as lists :param time: a list of times for each distance and change in distance in seconds :param distance: a list of distances that were measured in millimeters :param delta_dist: a list of changes in distances between a distance measurement and the following distance measurement :return: four lists including: overshoot - the minimum points for when a tote stops close to the distance sensor; overshoot times - the times for when the overshoots occur; stopped positions - the settling position for when a tote stops close to a distance sensor; stopped position times - the times for when the stopped positions occur """ overshoot, overshoot_times, stopped_pos, stopped_pos_times = [], [], [], [] i = 0 while i < len(delta_dist): if time[i] + SITTING_TIME > time[-1]: # check if loop will reach end of file break # Set up temporary arrays position_set, time_set = [], [] if delta_dist[i] < 0 <= delta_dist[i + 1] or ( delta_dist[i] == 0 and (delta_dist[i - 1] < 0 < delta_dist[i + 1])): lowerBound = time[i] while time[i] < lowerBound + SITTING_TIME: position_set.append(distance[i]) time_set.append(time[i]) i += 1 if -300 <= min(position_set) <= 800: # These bounds were found to be useful in early tests, but may be vestigial. overshoot.append(min(position_set)) overshoot_times.append(time_set[position_set.index(min(position_set))]) stopped_pos.append(position_set[int(len(position_set) * 0.5)]) stopped_pos_times.append(time_set[int(len(time_set) * 0.5)]) i += 1 # Neglect the first and last elements in each dataset return overshoot, overshoot_times, stopped_pos, stopped_pos_times def analyze_file(filename: str): """ Takes a string 'filename' and calls functions to get the overshoot and stopping positions, when they occur, as well as overall time, distance, and change in distance :param filename: the name of the .txt file that contains distance over time :return: None """ tote_info = get_tote_info(filename) time, distance, delta_dist = extract_data(filename, tote_info) overshoot, overshoot_times, stopped_pos, stopped_pos_times = get_overshoot_stopped(time, distance, delta_dist) if BASELINE_MEASUREMENT: export_data(filename, time, distance, delta_dist) else: export_data(filename, overshoot_times, overshoot, stopped_pos) show_data(filename, time, distance, overshoot, overshoot_times, stopped_pos, stopped_pos_times) if ANALYZE_ALL_FILES: for inputFile in os.listdir(INPUT_DIR_NAME): if inputFile == 'Extracted': continue else: analyze_file(inputFile) else: analyze_file(FILE_NAME)