4

I'm working on a warehouse layout in Python where a route is calculated between multiple pick locations. The layout includes both wide and 'ultra-narrow' aisles. In the real-world setup, ultra-narrow aisles require that pickers leave their carts at the entrance and return the same way — meaning they must enter and exit from the same side (either the top or bottom). Only at designated cross-aisles can pickers fully traverse through.

I’m using a 2D walkable area approach and generating a networkx graph over allowed points (at 0.5m resolution), and computing the shortest path between pick locations by using nx.shortest_path.

What I’d like to do:

  • For specified ultra-narrow aisles, ensure that if the path enters the aisle from one side (say the top), it also exits from the same side.
  • Allow full traversal (in-out from either side) only at cross-aisles.

Is there an effective way to constrain traversal behavior in networkx or the graph itself to enforce this kind of "U-turn only" constraint for certain regions?

EDIT: Here is a simplified version of my code. Given the four pick locations, I want the route to access aisle 112 from the bottom to visit P1, then go to P2 in aisle 113. After that, it should leave the aisles from the bottom to then visit aisle 114 to visit P3 and P4 respectively.

# -*- coding: utf-8 -*-

import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
import networkx as nx


# ------------------- Define warehouse -------------------
# Visual X positions for vertical shelf aisles
visual_positions = {
    111: -5.4,
    112: -4.8,
    113: -3.2,
    114: -2.6,
}

# Aisles that only allow access from one side
shelf_access_side = {
    111: "left",
    112: "right",
    113: "left",
    114: "right"
}

# Generate Y-pick positions for vertical aisles
pick_locations = {
    111: [round(2.9 + i * (1.3 / 3), 3) for i in range(48)],
    112: [round(2.9 + i * (1.3 / 3), 3) for i in range(48)],
    113: [round(2.9 + i * (1.3 / 3), 3) for i in range(48)],
    114: [round(2.9 + i * (1.3 / 3), 3) for i in range(48)],
}

# Walkable zones (open spaces + narrow aisle access)
walkable_areas = [
    ((-7, 0), (0, 2.9)),            # bottom walkway
    ((-7, 24), (-2, 24.8)),         # top walkway
    ((-7, 0), (-5.4, 24)),          # left cross-aisle
    ((-2, 0), (0, 24.8))            # right cross-aisle
]

x_positions = sorted([visual_positions[a] for a in shelf_access_side])
for i in range(len(x_positions) - 1):
    left, right = x_positions[i], x_positions[i+1]
    if right - (left + 0.6) >= 0.5:
       x_start = round(round((left + 0.6) * 2) / 2, 2)
       x_end = round(round((right) * 2) / 2, 2)
       if x_start < (left+0.6):
           x_start += 0.5
       if x_end > right:
           x_end -= 0.5
       
       y_start, y_end = 2.5, 24
       walkable_areas.append(((x_start, y_start), (x_end, y_end)))


# ------------------- Input Picks -------------------
user_picks = ['112.6', '113.33', '114.43', '114.16']

def parse_user_picks(user_picks):
    parsed = []
    for p in user_picks:
        aisle_str, loc_str = p.split(".")
        aisle = int(aisle_str)
        location = int(loc_str)
        pick_slot_height = round(1.3 / 3, 3)  # ~0.433m
        pick_location = round(2.9 + (location - 1) * pick_slot_height, 3)
        parsed.append((aisle, pick_location))
    return parsed

parsed_picks = parse_user_picks(user_picks)

df = pd.DataFrame(list(parsed_picks), columns=['Aisle', 'Pick Location'])


# ------------------- Plotting Functions -------------------
def plot_pick_locations(ax, pick_locations, visual_positions, aisles):
    for aisle in aisles:
        pos = visual_positions[aisle]
        for location in pick_locations[aisle]:
            rect = Rectangle((pos, location), 0.6, round(1.3 / 3, 3), color='blue', alpha=0.3)
            ax.add_patch(rect)

def plot_selected_picks(ax, df, visual_positions):
    for _, row in df.iterrows():
        pos = visual_positions[row['Aisle']]
        rect = Rectangle((pos, row['Pick Location']), 0.6, round(1.3 / 3, 3), color='red')
        ax.add_patch(rect)

def plot_walkable_areas(ax, walkable_areas):
    for (x_start, y_start), (x_end, y_end) in walkable_areas:
        width = x_end - x_start
        height = y_end - y_start
        ax.add_patch(Rectangle((x_start, y_start), width, height, color='lightgrey'))


# ------------------- Graphing Functions -------------------
# Build walkable graph
def generate_walkable_graph(walkable_areas, resolution=0.5):
    walkable_points = set()
    for (x_start, y_start), (x_end, y_end) in walkable_areas:
        x = x_start
        while x <= x_end:
            y = y_start
            while y <= y_end:
                point = (round(x, 3), round(y, 3))
                walkable_points.add(point)
                y += resolution
            x += resolution
    G = nx.Graph()
    directions = [(resolution, 0), (-resolution, 0), (0, resolution), (0, -resolution)]
    for x, y in walkable_points:
        for dx, dy in directions:
            neighbor = (round(x + dx, 3), round(y + dy, 3))
            if neighbor in walkable_points:
                G.add_edge((x, y), neighbor, weight=resolution)
    return G, walkable_points

walkable_graph, walkable_points = generate_walkable_graph(walkable_areas)

# Helper function to find nearest walkable point
def nearest_walkable_point(x, y, walkable_points, resolution=0.5):
    rounded_x = round(round(x / resolution) * resolution, 3)
    rounded_y = round(round(y / resolution) * resolution, 3)
    candidate = (rounded_x, rounded_y)
    if candidate in walkable_points:
        return candidate
    min_dist = float('inf')
    nearest = None
    for point in walkable_points:
        dist = (point[0] - x)**2 + (point[1] - y)**2
        if dist < min_dist:
            min_dist = dist
            nearest = point
    return nearest

def get_visual_coords(pick):
    aisle, location = pick
    if shelf_access_side[aisle] == 'left':
        x = visual_positions[aisle]
        y = location
    else:
        x = visual_positions[aisle] + 0.6
        y = location
    return x, y


# ------------------- Creating Path -------------------

# TODO: I want to prevent full traversal through aisles 112–113.
# For example, if the graph enters aisle 113 from the bottom, it should NOT be able to exit from the top.


pick_nodes = []
for aisle, loc in parsed_picks:
    x, y = get_visual_coords((aisle, loc))
    node = nearest_walkable_point(x, y, walkable_points)
    pick_nodes.append(node)


depot_coords = (-2.0, -2.0)
depot_node = nearest_walkable_point(*depot_coords, walkable_points)
stops = [depot_node] + pick_nodes + [depot_node]

# Build full path
full_path = []
for i in range(len(stops) - 1):
    segment = nx.shortest_path(walkable_graph, stops[i], stops[i+1], weight='weight')
    if full_path:
        full_path += segment[1:]
    else:
        full_path += segment

# Total length across both segments
path_length = sum(
    walkable_graph[full_path[i]][full_path[i + 1]]['weight']
    for i in range(len(full_path) - 1)
)


# ------------------- Plot -------------------
plt.figure(figsize=(14, 10))
ax = plt.gca()

# Existing layout and picks
plot_walkable_areas(ax, walkable_areas)
plot_pick_locations(ax, pick_locations, visual_positions, shelf_access_side)
plot_selected_picks(ax, df, visual_positions)


if full_path:
    path_x, path_y = zip(*full_path)
    ax.plot(path_x, path_y, color='red', linewidth=2, label='User Path')

    # Depot
    ax.plot(depot_node[0], depot_node[1], 'ks', markersize=8, label='Depot')

    # User picks
    for node in pick_nodes:
        ax.plot(node[0], node[1], 'o', color='purple', markersize=8)

for i, node in enumerate(pick_nodes):
    ax.text(node[0] + 0.3, node[1], f"P{i+1}", color='black', fontsize=9)

plt.xticks(
    [visual_positions[a] for a in visual_positions],
    [f"{a}" for a in visual_positions],
    rotation=45
)

ax.set_aspect("equal")
plt.show()
2
  • 2
    I'm not at all familiar with networkx, so this may or may not be a practical suggestion - but it seems like the proper way to model your narrow aisles would not be as a through-path, but instead as two stub paths - that happen to give access to identical sets of pick locations. Commented Apr 23 at 17:22
  • While @jasonharper's idea is excellent and almost certainly the right way to think about this problem, do note that you are modelling each item as a set of two items (of which only has to be collected). As a result, the number of graph searches doubles for each item in sequence that is in a narrow walkway. If the sequence of pickups is pre-determined (as above), that is not big issue. However, if you are eventually planning on searching for a travelling salesman solution (i.e. optimising the sequence of pickups as well), then this will become problematic (see references for set TSP problems). Commented Apr 25 at 12:07

1 Answer 1

0

An effective way to solve your problem would be to create dummy nodes for your ultra narrow aisles. A set of normal nodes for the points in ultra narrow aisles and a set of dummy nodes for those points. The entrance to an aisle should be either a dummy node or normal node, depending on the side of the aisle you are entering from. If you now set the distance between dummy nodes and normal nodes in an aisle to a very large number (e.g. infinite), you will always exit trough the side you came in, as that path is always shorter.

Note: for heuristic approaches (which I assume you are using) this may have a neglible effect on your results or solving time. For exact solutions (using linear programms) this increases the problem size by the amount of nodes in narrow aisles, and may exponentially increasse the solving time for this problem.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.