Skip to content

●  사용방법 · 경로 · 물류

오더 피킹

피킹할 물건을 카트별로 묶고 통로를 도는 순서를 정합니다. 카트 용량을 지키면서 작업자가 걷는 거리를 줄입니다.

무엇을 정하나요

주문에 든 물건마다 어느 카트가 몇 번째로 집을지 정합니다.

최소 실행 예제

from optimaru.order_picking.models import (
    LocationModel, OrderModel, OrderPickingRequest, ProductModel, TrolleyModel, solve_request,
)

def shelf(column, shelving_row, side, row):
    return LocationModel(column=column, shelving_row=shelving_row, side=side, row=row)

request = OrderPickingRequest(
    timelimit=2,
    random_seed=42,
    trolleys=[
        TrolleyModel(id="T1", bucket_count=2, bucket_capacity=48000, location=shelf("A", 1, "LEFT", 0)),
    ],
    orders=[
        OrderModel(id="O1", items=[
            ProductModel(id="milk", volume=1232, location=shelf("D", 1, "RIGHT", 4)),
            ProductModel(id="rice", volume=9000, location=shelf("B", 2, "LEFT", 7)),
        ]),
        OrderModel(id="O2", items=[
            ProductModel(id="soap", volume=800, location=shelf("B", 2, "RIGHT", 3)),
        ]),
    ],
)

solution = solve_request(request)
print(solution.feasible, solution.score, solution.total_distance)
for trolley_id, stops in solution.routes.items():
    print(trolley_id, [stop.pick_task_id for stop in stops])
print(solution.unassigned, solution.order_split_count)

같은 유스케이스를 그림과 실행 결과가 담긴 Jupyter 노트북으로도 볼 수 있습니다. examples/order-picking.ipynb ↗

입력 파라미터

LocationModel

필드 의미
column 필수 입력 선반 열입니다. A부터 E까지입니다. shelving_row 필수 입력 선반 행입니다. 1부터 3까지입니다. side 필수 입력 LEFT 또는 RIGHT입니다. row 필수 입력 선반 안의 줄 번호입니다. 0부터 10까지입니다.

ProductModel

필드 의미
id 필수 입력 상품 id입니다. volume 필수 입력 부피(cm³)입니다. location 필수 입력 집을 위치입니다.

OrderModel

필드 의미
id 필수 입력 주문 id입니다. items 필수 입력 집을 물건 목록입니다. 목록에서의 위치가 피킹 작업 id의 일부가 됩니다.

TrolleyModel

필드 의미
id 필수 입력 카트 id입니다. bucket_count 필수 입력 카트에 실린 버킷 수입니다. bucket_capacity 필수 입력 버킷 하나의 용량(cm³)입니다. location 필수 입력 카트의 출발 위치입니다. initial_route 경로의 시작점이 되는 피킹 작업 id 목록입니다.

OrderPickingRequest

필드 의미
trolleys 필수 입력 카트 목록입니다. orders 필수 입력 주문 목록입니다.

풀이 파라미터

풀이를 호출할 때 넘기는 값입니다. 시간 제한, 시드, 병렬 수가 여기에 있습니다.

파라미터 기본값 의미
name None 요청에 붙이는 이름입니다. 선택 사항입니다.
timelimit 30.0 탐색 시간(초)이며, 0부터 1e9까지 줄 수 있습니다.
random_seed 0 탐색에 쓰는 난수 시드입니다.
num_workers 1 독립적으로 동시에 돌리는 탐색 수입니다. 가장 좋은 결과를 고릅니다.
stop_at_first_feasible False 필수 점수가 0이 되는 즉시 멈춥니다.
solver_parameters {} SolverConfig 필드를 이름별로 담은 딕셔너리입니다. 시간 제한, 시드, 탐색 수, on_new_best는 여기서 정할 수 없습니다.
constraints None 점수에 넣을 제약과 그 설정입니다. 지정하지 않으면 기본 제약을 씁니다.
탐색 옵션 전체 (27)

solver_parameters에 이름별로 넘기세요.

파라미터 기본값 의미
best_score_limit — 최고 점수가 이 값에 닿으면 일찍 멈춥니다. 기본값으로는 작동하지 않습니다.
distance_nearby True 실제로 가장 가까운 피킹 지점에서 이동 상대를 고릅니다.
distance_nearby_probability 0.5 그렇게 고를 확률입니다.
distance_nearby_size 24 피킹 지점마다 유지할 이웃 수입니다.
full_assert False 증분 점수를 전체 재계산과 매번 비교합니다. 느리므로 디버깅할 때만 씁니다.
late_acceptance_size 0 늦은 수락(late acceptance) 기록의 길이입니다. 0이면 문제 크기에 맞춰 정합니다.
list_swap_move_weight 0.25 목록 교환 이동의 상대적 비중입니다. 0이면 쓰지 않습니다.
on_new_best None 새 최고 해를 찾을 때마다 부르는 콜백입니다. 콜백이 예외를 내면 풀이를 멈추고 그 예외를 그대로 냅니다.
or_opt_move_weight 0.1 짧은 구간을 옮기는 Or-opt 이동의 상대적 비중입니다. 0이면 쓰지 않습니다.
order_consolidate_moves True 나뉜 주문을 한 카트로 모으는 이동을 씁니다.
parallel_solver_count 1 독립적으로 동시에 돌리는 탐색 수입니다. 탐색마다 시드가 다르고 시간은 똑같이 쓰며, 가장 좋은 결과를 고릅니다.
random_seed 0 탐색에 쓰는 난수 시드입니다.
relocate_move_weight 0.4 재배치 이동의 상대적 비중입니다. 0이면 쓰지 않습니다.
ruin_recreate True 탐색이 정체되면 적응형 파괴·재구성을 실행합니다.
ruin_recreate_max_ruin_count 60 한 번에 제거하는 항목 수의 절대 상한입니다.
ruin_recreate_max_ruin_fraction 0.3 한 번에 제거하는 해의 최대 비율입니다.
ruin_recreate_min_ruin_fraction 0.1 한 번에 제거하는 해의 최소 비율입니다.
ruin_recreate_operator_decay 0.8 연산자 가중치가 적응하는 속도입니다.
ruin_recreate_polish_steps 5000 실행 후 다듬는 언덕 오르기 이동 수입니다. step_count_limit에는 세지 않습니다.
ruin_recreate_rewards [33.0, 9.0, 3.0, 0.0] 새 최고, 개선, 수락, 거절 결과에 주는 연산자 보상입니다.
ruin_recreate_unimproved_step_trigger 50000 파괴·재구성을 실행하기까지 새 최고 해 없이 지나는 걸음 수입니다.
spent_limit_seconds 30.0 탐색 시간(초)이며, 0부터 1e9까지 줄 수 있습니다.
step_count_limit 0 이 걸음 수에 도달하면 멈춥니다. 0이면 제한이 없습니다.
tail_swap_move_weight 0.1 두 목록의 뒷부분을 맞바꾸는 이동(2-opt*)의 상대적 비중입니다. 0이면 쓰지 않습니다.
two_opt_move_weight 0.15 2-opt 이동의 상대적 비중입니다. 0이면 쓰지 않습니다.
unimproved_spent_limit_seconds 0.0 새 최고 해 없이 이 시간(초)이 지나면 멈춥니다. 0이면 제한이 없습니다.
unimproved_step_count_limit 0 새 최고 해 없이 이 걸음 수가 지나면 멈춥니다. 0이면 제한이 없습니다.

돌아오는 결과

solve_request는 OrderPickingResponse를 돌려줍니다. 결정 내용은 routes에 있습니다.

OrderPickingResponse

필드 의미
routes 필수 입력 카트 id별로 순서가 정해진 피킹 목록입니다. 항목마다 pick_task_id, order_id, product_name, volume, location이 있습니다. unassigned 어느 카트에도 들어가지 못한 피킹 작업 id입니다. distance_by_trolley 필수 입력 카트별 이동 거리(미터)입니다. 돌아오는 구간도 포함합니다. total_distance 필수 입력 모든 카트의 이동 거리 합계(미터)입니다. order_split_count 필수 입력 주문별로 나뉜 카트 수를 모두 더한 값입니다. score 필수 입력 필수, 선호 수준으로 이루어진 문자열 점수입니다. feasible 필수 입력 필수 제약을 하나도 어기지 않으면 True입니다.

제약

optimaru.order_picking.constraints에서 클래스를 가져와 TrolleyCapacityCost()처럼 호출해서 넘깁니다. 넘긴 제약만 점수에 들어가고, 아무것도 넘기지 않으면 기본 제약을 씁니다. 목록에 같은 제약을 여러 번 넣을 수 있으며, 넣은 것마다 따로 점수에 들어갑니다. 제약마다 다음 파라미터를 받습니다.

파라미터 기본값 의미
weight None 제약 비용에 곱하는 값이며, 적용되는 수준의 단위로 셉니다. 지정하지 않으면 아래 기본 가중치를 씁니다. 기본 가중치가 0인 제약은 가중치를 줘야 작동합니다.
level None "hard"면 필수, "soft"면 선호로 바꿉니다. Level.HARD, Level.SOFT도 됩니다. 지정하지 않으면 아래 수준을 그대로 씁니다. 이 유스케이스에는 중간 수준이 없어서 "medium"을 주면 ValueError가 납니다. 보상에 "hard"를 주거나 수준이 고정된 제약의 수준을 바꾸려 해도 ValueError가 납니다.
exponent 1.0 부호를 유지한 채 비용에 거듭제곱을 적용하며, 0보다 커야 합니다. offset을 반영한 뒤에 적용합니다. 2.0이면 제곱이라 한곳에 몰린 위반의 비용이 크게 늘어납니다. 차량이나 날짜 같은 단위마다 적용하고, 합계 하나로 계산하는 제약에는 그 합계에 적용합니다.
offset 0.0 거듭제곱 전에 비용 x에서 빼는 값입니다. exponent처럼 단위마다, 합계로 계산하는 제약은 그 합계에 적용하며, 비용과 같은 척도(초, 분, 개수 등)로 셉니다. 부호와 상관없이 유한한 수면 되고, 키워드로만 줄 수 있습니다. 이 값 아래에서 어떻게 계산할지는 clip이 정합니다.
clip True True면 offset을 문턱값으로 씁니다. 넘은 부분만 계산하므로(max(0, x - offset) ** exponent) 그 이하의 비용은 아무것도 더하지 않습니다. False면 부호를 유지한 채 (x - offset) ** exponent를 그대로 계산하므로, 오프셋보다 작은 단위는 비용이 0이어도 음수, 곧 보상으로 들어갑니다. 하드 점수에 들어가는 수준에서는 clip=False에 0이 아닌 오프셋을 주면 ValueError가 납니다. 하드 0이 위반 없음이라는 뜻을 지키기 위해서입니다. 키워드로만 줄 수 있습니다.

위의 최소 실행 예제에 이어서 실행합니다.

from optimaru.order_picking.constraints import (
    DistanceFromPreviousPickTaskCost, OrderSplitByTrolleyCost, TrolleyCapacityCost,
    get_constraints,
)

picked = get_constraints([
    TrolleyCapacityCost(),
    DistanceFromPreviousPickTaskCost(),
    OrderSplitByTrolleyCost(weight=5000),
])
print(solve_request(request, constraints=picked).score)
이름 수준 기본 가중치 하는 일
trolley_capacity_cost 필수 1 카트에 모자란 버킷 수에 벌점을 줍니다. 주문끼리 버킷을 나눠 쓰지 않으므로 주문마다 따로 올림해 계산합니다.
distance_from_previous_pick_task_cost 선호 1 피킹 지점 사이의 거리를 줄입니다. 첫 피킹은 카트의 출발 위치에서 잽니다.
distance_from_last_pick_task_to_path_origin_cost 선호 1 마지막 피킹에서 출발 위치로 돌아오는 거리를 줄입니다. 빈 카트는 비용이 없습니다.
order_split_by_trolley_cost 선호 1000 (주문, 카트) 쌍의 수를 줄입니다. 카트 세 대에 나뉜 주문은 한 대에 실린 주문의 세 배 비용이 듭니다.

제약 사용법 →

기존 해에서 시작하기

최신 버전이 필요합니다. pip install -U optimaru로 올려 주세요.

with_initial_solution(request, solution)은 카트마다 initial_route(피킹 작업 id 목록)를 채운 새 요청을 돌려줍니다.

위의 최소 실행 예제에 이어서 실행합니다.

from optimaru.order_picking.models import with_initial_solution

seeded = with_initial_solution(request, solution)  # solution.model_dump(mode="json")도 됩니다
better = solve_request(seeded)
print(better.score, better.total_distance)
  • 완전한 해를 넣으면 구성 단계를 건너뛰고 그 해에서 바로 탐색을 시작합니다. 일부만 넣으면 넣은 부분은 그대로 두고, 빠진 피킹 작업만 새로 구성합니다.
  • timelimit=0을 주면 바로 멈추므로 완전한 해는 넣은 그대로 돌아옵니다. step_count_limit = 0은 제한이 없다는 뜻이므로, 바로 멈추려면 시간 제한을 쓰세요.
  • 모르는 id가 있거나 같은 작업이 두 번 나오면 ValueError가 납니다.
  • 피킹 작업 id는 <주문 id>-<물건 위치> 형식입니다. 그래서 주문의 items 순서를 풀었을 때와 똑같이 유지해야 합니다. 순서가 바뀌면 같은 id가 다른 물건을 가리킵니다.

예외와 제한

  • 창고 구조는 고정되어 있습니다. 열 A~E, 선반 행 1~3, 양쪽 면, 선반 안의 줄 0~10입니다.
  • 주문끼리 버킷을 나눠 쓰지 않습니다. 카트에 필요한 버킷 수는 주문별 부피를 bucket_capacity로 나눠 올림한 값의 합입니다.