무엇을 정하나요
주문에 든 물건마다 어느 카트가 몇 번째로 집을지 정합니다.
최소 실행 예제
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 필수 입력 side 필수 입력 LEFT 또는 RIGHT입니다.row 필수 입력 ProductModel
| 필드 | 의미 |
|---|
id 필수 입력 volume 필수 입력 location 필수 입력 OrderModel
| 필드 | 의미 |
|---|
id 필수 입력 items 필수 입력 TrolleyModel
| 필드 | 의미 |
|---|
id 필수 입력 bucket_ count 필수 입력 bucket_ capacity 필수 입력 location 필수 입력 initial_ route OrderPickingRequest
| 필드 | 의미 |
|---|
trolleys 필수 입력 orders 필수 입력 풀이 파라미터
풀이를 호출할 때 넘기는 값입니다. 시간 제한, 시드, 병렬 수가 여기에 있습니다.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
name | None | 요청에 붙이는 이름입니다. 선택 사항입니다. |
timelimit | 30.0 | 탐색 시간(초)이며, 0부터 1e9까지 줄 수 있습니다. |
random_ | 0 | 탐색에 쓰는 난수 시드입니다. |
num_ | 1 | 독립적으로 동시에 돌리는 탐색 수입니다. 가장 좋은 결과를 고릅니다. |
stop_ | False | 필수 점수가 0이 되는 즉시 멈춥니다. |
solver_ | {} | SolverConfig 필드를 이름별로 담은 딕셔너리입니다. 시간 제한, 시드, 탐색 수, on_new_best는 여기서 정할 수 없습니다. |
constraints | None | 점수에 넣을 제약과 그 설정입니다. 지정하지 않으면 기본 제약을 씁니다. |
탐색 옵션 전체 (27)
solver_parameters에 이름별로 넘기세요.
| 파라미터 | 기본값 | 의미 |
|---|---|---|
best_ | — | 최고 점수가 이 값에 닿으면 일찍 멈춥니다. 기본값으로는 작동하지 않습니다. |
distance_ | True | 실제로 가장 가까운 피킹 지점에서 이동 상대를 고릅니다. |
distance_ | 0.5 | 그렇게 고를 확률입니다. |
distance_ | 24 | 피킹 지점마다 유지할 이웃 수입니다. |
full_ | False | 증분 점수를 전체 재계산과 매번 비교합니다. 느리므로 디버깅할 때만 씁니다. |
late_ | 0 | 늦은 수락(late acceptance) 기록의 길이입니다. 0이면 문제 크기에 맞춰 정합니다. |
list_ | 0.25 | 목록 교환 이동의 상대적 비중입니다. 0이면 쓰지 않습니다. |
on_ | None | 새 최고 해를 찾을 때마다 부르는 콜백입니다. 콜백이 예외를 내면 풀이를 멈추고 그 예외를 그대로 냅니다. |
or_ | 0.1 | 짧은 구간을 옮기는 Or-opt 이동의 상대적 비중입니다. 0이면 쓰지 않습니다. |
order_ | True | 나뉜 주문을 한 카트로 모으는 이동을 씁니다. |
parallel_ | 1 | 독립적으로 동시에 돌리는 탐색 수입니다. 탐색마다 시드가 다르고 시간은 똑같이 쓰며, 가장 좋은 결과를 고릅니다. |
random_ | 0 | 탐색에 쓰는 난수 시드입니다. |
relocate_ | 0.4 | 재배치 이동의 상대적 비중입니다. 0이면 쓰지 않습니다. |
ruin_ | True | 탐색이 정체되면 적응형 파괴·재구성을 실행합니다. |
ruin_ | 60 | 한 번에 제거하는 항목 수의 절대 상한입니다. |
ruin_ | 0.3 | 한 번에 제거하는 해의 최대 비율입니다. |
ruin_ | 0.1 | 한 번에 제거하는 해의 최소 비율입니다. |
ruin_ | 0.8 | 연산자 가중치가 적응하는 속도입니다. |
ruin_ | 5000 | 실행 후 다듬는 언덕 오르기 이동 수입니다. step_count_limit에는 세지 않습니다. |
ruin_ | [33.0, 9.0, 3.0, 0.0] | 새 최고, 개선, 수락, 거절 결과에 주는 연산자 보상입니다. |
ruin_ | 50000 | 파괴·재구성을 실행하기까지 새 최고 해 없이 지나는 걸음 수입니다. |
spent_ | 30.0 | 탐색 시간(초)이며, 0부터 1e9까지 줄 수 있습니다. |
step_ | 0 | 이 걸음 수에 도달하면 멈춥니다. 0이면 제한이 없습니다. |
tail_ | 0.1 | 두 목록의 뒷부분을 맞바꾸는 이동(2-opt*)의 상대적 비중입니다. 0이면 쓰지 않습니다. |
two_ | 0.15 | 2-opt 이동의 상대적 비중입니다. 0이면 쓰지 않습니다. |
unimproved_ | 0.0 | 새 최고 해 없이 이 시간(초)이 지나면 멈춥니다. 0이면 제한이 없습니다. |
unimproved_ | 0 | 새 최고 해 없이 이 걸음 수가 지나면 멈춥니다. 0이면 제한이 없습니다. |
돌아오는 결과
solve_request는 OrderPickingResponse를 돌려줍니다. 결정 내용은 routes에 있습니다.
OrderPickingResponse
| 필드 | 의미 |
|---|
routes 필수 입력 pick_task_id, order_id, product_name, volume, location이 있습니다.unassigned distance_ by_ trolley 필수 입력 total_ distance 필수 입력 order_ split_ count 필수 입력 score 필수 입력 feasible 필수 입력 제약
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_ | 필수 | 1 | 카트에 모자란 버킷 수에 벌점을 줍니다. 주문끼리 버킷을 나눠 쓰지 않으므로 주문마다 따로 올림해 계산합니다. |
distance_ | 선호 | 1 | 피킹 지점 사이의 거리를 줄입니다. 첫 피킹은 카트의 출발 위치에서 잽니다. |
distance_ | 선호 | 1 | 마지막 피킹에서 출발 위치로 돌아오는 거리를 줄입니다. 빈 카트는 비용이 없습니다. |
order_ | 선호 | 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로 나눠 올림한 값의 합입니다.