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