무엇을 정하나요
포장 작업마다 생산 라인과 그 라인에서의 순서를 정합니다. 기본 풀이에서는 라인별 작업자도 다시 배정합니다.
최소 실행 예제
from datetime import date, datetime, timedelta
from optimaru import food_packaging as fp
from optimaru.food_packaging.models import (
JobModel, LineModel, OperatorModel, PackagingScheduleModel, ProductModel,
)
start = datetime(2026, 1, 5, 8)
minutes = timedelta(minutes=1)
def job(id, product_id, hours):
return JobModel(id=id, name=id, product_id=product_id, duration=timedelta(hours=hours),
min_start_time=start, ideal_end_time=start + timedelta(hours=12),
max_end_time=start + timedelta(hours=24))
problem = PackagingScheduleModel(
from_date=date(2026, 1, 5),
to_date=date(2026, 1, 7),
products=[
ProductModel(id="milk", name="Milk", cleaning_durations={"milk": 0 * minutes, "juice": 30 * minutes}),
ProductModel(id="juice", name="Juice", cleaning_durations={"juice": 0 * minutes, "milk": 45 * minutes}),
],
operators=[OperatorModel(id="O1"), OperatorModel(id="O2")],
lines=[
LineModel(id="L1", name="Line 1", start_date_time=start, operator_id="O1"),
LineModel(id="L2", name="Line 2", start_date_time=start, operator_id="O2"),
],
jobs=[job("J1", "milk", 3), job("J2", "juice", 2), job("J3", "milk", 4), job("J4", "juice", 3)],
)
core = fp.solve(problem.to_core(), time_limit_seconds=2.0, random_seed=42)
solved = PackagingScheduleModel.from_core(core)
print(solved.score_hard, solved.score_medium, solved.score_soft)
print({line.id: (line.operator_id, line.job_ids) for line in solved.lines})
print([(j.id, j.line_id, j.start_production_date_time, j.end_date_time) for j in solved.jobs])
같은 유스케이스를 그림과 실행 결과가 담긴 Jupyter 노트북으로도 볼 수 있습니다. examples/food-packaging.ipynb ↗
ProductModel
id 필수 입력 | 제품 id입니다. |
name 필수 입력 | 제품 이름입니다. |
cleaning_durations | 바로 앞에 만든 제품의 id별 청소 시간입니다. |
OperatorModel
id 필수 입력 | 작업자 id입니다. |
LineModel
id 필수 입력 | 라인 id입니다. |
name 필수 입력 | 라인 이름입니다. |
start_date_time 필수 입력 | 라인이 시작하는 시각입니다. |
operator_id | 라인을 맡은 작업자입니다. |
JobModel
id 필수 입력 | 작업 id입니다. |
name 필수 입력 | 작업 이름입니다. |
product_id 필수 입력 | 만드는 제품입니다. |
duration 필수 입력 | 생산 시간입니다. |
min_start_time 필수 입력 | 가장 이른 시작 시각입니다. 값은 보관하지만 이를 읽는 제약은 없습니다. |
ideal_end_time 필수 입력 | 작업이 끝나면 좋은 시각입니다. |
max_end_time 필수 입력 | 작업이 반드시 끝나야 하는 시각입니다. |
pinned | 작업을 지금 위치에 고정합니다. |
PackagingScheduleModel
from_date 필수 입력 | 기간의 첫날입니다. 이 날짜를 포함합니다. |
to_date 필수 입력 | 기간의 끝입니다. 이 날짜는 포함하지 않습니다. |
products 필수 입력 | 제품 목록입니다. |
operators 필수 입력 | 작업자 목록입니다. |
lines 필수 입력 | 생산 라인 목록입니다. |
jobs 필수 입력 | 포장 작업 목록입니다. |
풀이 파라미터
풀이를 호출할 때 넘기는 값입니다. 시간 제한, 시드, 병렬 수가 여기에 있습니다.
탐색 옵션 전체 (26)
SolverConfig에 값을 넣어 config=로 넘기세요.
돌아오는 결과
solve는 엔진 객체를 받아 그 자리에서 풀고 돌려줍니다. 결과는 from_core로 변환해 읽습니다.
LineModel
job_ids | 라인의 작업을 순서대로 나열한 목록입니다. 배정의 기준은 이 목록입니다. |
operator_id | 풀이 후의 작업자입니다. |
JobModel
line_id | 작업이 배정된 라인입니다. |
start_cleaning_date_time | 청소를 시작하는 시각입니다. |
start_production_date_time | 생산을 시작하는 시각입니다. |
end_date_time | 작업이 끝나는 시각입니다. |
PackagingScheduleModel
score_hard | 필수 점수입니다. 0이면 가능한 해입니다. |
score_medium | 중간 점수입니다. 미배정 작업과 이상적 종료 시각 초과를 나타냅니다. |
score_soft | 선호 점수입니다. |
제약
optimaru.food_packaging.constraints에서 클래스를 가져와 MaxEndTimeCost()처럼 호출해서 넘깁니다. 넘긴 제약만 점수에 들어가고, 아무것도 넘기지 않으면 기본 제약을 씁니다. 목록에 같은 제약을 여러 번 넣을 수 있으며, 넣은 것마다 따로 점수에 들어갑니다. 제약마다 다음 파라미터를 받습니다.
중간 제약의 기본 가중치는 중간 수준 단위로 1입니다. 탐색은 중간 값에 1e10을 곱해 선호 값과 합친 점수로 해를 비교하므로, 선호 합계가 1e10보다 작은 동안에는 중간 수준이 선호보다 우선합니다. level로 어느 제약이든 세 수준 가운데 하나로 옮길 수 있습니다. level="medium"이면 중간으로 옮기고, "hard"나 "soft"면 중간 제약을 필수나 선호로 옮깁니다. 가중치는 적용되는 수준의 단위로 셉니다.
위의 최소 실행 예제에 이어서 실행합니다.
from optimaru.food_packaging.constraints import (
AfterIdealEndTimeCost, MaxEndTimeCost, SquaredMakespanCost, UnassignedJobsCost,
get_constraints,
)
picked = get_constraints([
MaxEndTimeCost(), UnassignedJobsCost(), AfterIdealEndTimeCost(), SquaredMakespanCost(weight=2),
])
custom = fp.solve(problem.to_core(), time_limit_seconds=2.0, random_seed=42, constraints=picked)
print(custom.score)
제약 사용법 →
기존 해에서 시작하기
최신 버전이 필요합니다. pip install -U optimaru로 올려 주세요.
with_initial_solution(problem, solution)은 라인의 job_ids와 operator_id를 해의 값으로 바꾼 새 문제를 돌려줍니다.
위의 최소 실행 예제에 이어서 실행합니다.
seeded = fp.with_initial_solution(problem, solved) # solved.model_dump(mode="json")도 됩니다
better = fp.solve(seeded.to_core(), time_limit_seconds=2.0, random_seed=42)
print(better.score)
- 완전한 해를 넣으면 구성 단계를 건너뛰고 그 해에서 바로 탐색을 시작합니다. 일부만 넣으면 넣은 부분은 그대로 두고, 빠진 작업만 새로 구성합니다.
- 고정된 작업은 문제에 있는 그대로 둡니다. 해의 라인은 고정된 작업으로 시작해야 합니다.
- 모르는 id, 두 번 나온 작업, 옮겨진 고정 작업, 제품 사이의 청소 시간이 없는 이웃 작업은
ValueError를 냅니다. constraints= 없이 푸는 기본 풀이에서는 넣어 준 작업자도 바뀔 수 있습니다. improve_operator_assignment가 탐색 시간이 없어도 탐색 뒤에 실행되어, 청소 충돌이 확실히 줄어들면 작업자를 바꿉니다.- 기본 풀이에서 시간 제한이 1ms 이하면 구성 단계 전에 돌아오므로 빠진 작업은 미배정으로 남습니다. 완전한 해는 넣은 그대로 돌아오지만, 일부만 있는 해를 채우려면 시간을 더 줘야 합니다.
step_count_limit = 0은 제한이 없다는 뜻입니다.
예외와 제한
min_start_time과 priority는 작업에 저장되지만 이를 읽는 제약은 없습니다.- 이웃할 수 있는 제품 쌍은 같은 제품끼리를 포함해
cleaning_durations에 모두 적어 주세요. 값이 없는 쌍이 이웃하게 되면 풀이 중에 ValueError가 납니다. - 작업자는 탐색 구간 사이에 언덕 오르기로 다시 배정되며, 기본 풀이에서만 그렇습니다.
직접 해 보기