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)
Previous
Previous

Robot Sanitizer

Next
Next

ADAPTS Research Project - NUAV