무엇을 정하나요
작업마다 수행 모드와 지연 값을 정합니다. 시작일과 종료일은 이 선택과 작업 간 선후 관계에서 계산됩니다.
최소 실행 예제
from optimaru import project_job_scheduling as pj
from optimaru.project_job_scheduling.models import (
AllocationModel, ExecutionModeModel, JobModel, ProjectJobScheduleModel, ProjectModel,
ResourceModel, ResourceRequirementModel,
)
def needs(amount):
return [ResourceRequirementModel(resource_id="crew", requirement=amount)]
jobs = [
JobModel(id="start", project_id="P1", job_type="SOURCE", successor_job_ids=["dig", "wire"],
execution_modes=[ExecutionModeModel(id="start-0")]),
JobModel(id="dig", project_id="P1", successor_job_ids=["end"], execution_modes=[
ExecutionModeModel(id="dig-fast", duration=2, resource_requirements=needs(3)),
ExecutionModeModel(id="dig-slow", duration=4, resource_requirements=needs(1)),
]),
JobModel(id="wire", project_id="P1", successor_job_ids=["end"], execution_modes=[
ExecutionModeModel(id="wire-0", duration=3, resource_requirements=needs(2)),
]),
JobModel(id="end", project_id="P1", job_type="SINK",
execution_modes=[ExecutionModeModel(id="end-0")]),
]
problem = ProjectJobScheduleModel(
projects=[ProjectModel(id="P1", critical_path_duration=5)],
resources=[ResourceModel(id="crew", capacity=3)],
jobs=jobs,
allocations=[AllocationModel(id=f"A-{job.id}", job_id=job.id) for job in jobs],
)
result = pj.solve(problem, time_limit_seconds=2.0, random_seed=42)
print(result.score_hard, result.score_medium, result.score_soft)
print([(a.job_id, a.execution_mode_id, a.start_date, a.end_date) for a in result.allocations])
같은 유스케이스를 그림과 실행 결과가 담긴 Jupyter 노트북으로도 볼 수 있습니다. examples/project-job-scheduling.ipynb ↗
ProjectModel
id 필수 입력 | 프로젝트 id입니다. |
release_date | 지연을 재는 기준일입니다. 시작일을 강제하지는 않습니다. |
critical_path_duration | 주 공정 길이입니다. 지연은 release_date에 이 값을 더한 날부터 잽니다. |
ResourceModel
id 필수 입력 | 자원 id입니다. |
capacity 필수 입력 | 재생 자원이면 하루 기준, 비재생 자원이면 전체 기간 기준 한도입니다. |
renewable | 한도가 매일 회복되는 자원이면 True입니다. |
project_id | 자원이 속한 프로젝트입니다. None은 전역 자원이며, 재생 자원이어야 합니다. |
ExecutionModeModel
id 필수 입력 | 수행 모드 id입니다. |
duration | 이 모드로 걸리는 일수입니다. |
resource_requirements | 사용하는 자원입니다. resource_id와 requirement의 목록입니다. |
JobModel
id 필수 입력 | 작업 id입니다. |
project_id 필수 입력 | 작업이 속한 프로젝트입니다. |
job_type | SOURCE, STANDARD, SINK 중 하나입니다. |
execution_modes | 모드가 하나 이상 있어야 합니다. SOURCE와 SINK는 기간이 0인 모드 하나를 씁니다. |
successor_job_ids | 이 작업이 끝나야 시작할 수 있는 작업들입니다. |
AllocationModel
id 필수 입력 | 배정 id입니다. |
job_id 필수 입력 | 대상 작업입니다. 배정과 작업은 1:1입니다. |
pinned | 이 배정의 모드와 지연 값을 고정합니다. SOURCE와 SINK는 항상 고정됩니다. |
ProjectJobScheduleModel
projects 필수 입력 | 프로젝트 목록입니다. |
resources 필수 입력 | 자원 목록입니다. |
jobs 필수 입력 | 작업 목록입니다. |
allocations 필수 입력 | 작업마다 하나씩 있는 목록입니다. |
delay_range_size | 지연 값의 범위입니다. 0부터 이 값 미만까지이고, 기본값은 500입니다. |
풀이 파라미터
풀이를 호출할 때 넘기는 값입니다. 시간 제한, 시드, 병렬 수가 여기에 있습니다.
탐색 옵션 전체 (20)
SolverConfig에 값을 넣어 config=로 넘기세요.
돌아오는 결과
solve는 모델을 받아 풀린 모델을 돌려줍니다.
AllocationModel
execution_mode_id | 선택된 수행 모드입니다. |
delay | 선택된 지연 값입니다. |
start_date | 계산된 시작일입니다. |
end_date | 계산된 종료일입니다. |
ProjectJobScheduleModel
score_hard | 필수 점수입니다. 0이면 가능한 해입니다. |
score_medium | 중간 점수입니다. 프로젝트 지연 합계의 음수입니다. |
score_soft | 선호 점수입니다. 가장 늦은 완료일의 음수입니다. |
제약
optimaru.project_job_scheduling.constraints에서 클래스를 가져와 NonRenewableResourceCapacityCost()처럼 호출해서 넘깁니다. 넘긴 제약만 점수에 들어가고, 아무것도 넘기지 않으면 기본 제약을 씁니다. 목록에 같은 제약을 여러 번 넣을 수 있으며, 넣은 것마다 따로 점수에 들어갑니다. 제약마다 다음 파라미터를 받습니다.
중간 제약의 기본 가중치는 중간 수준 단위로 1입니다. 탐색은 중간 값에 1e9을 곱해 선호 값과 합친 점수로 해를 비교하므로, 선호 합계가 1e9보다 작은 동안에는 중간 수준이 선호보다 우선합니다. level로 어느 제약이든 세 수준 가운데 하나로 옮길 수 있습니다. level="medium"이면 중간으로 옮기고, "hard"나 "soft"면 중간 제약을 필수나 선호로 옮깁니다. 가중치는 적용되는 수준의 단위로 셉니다.
위의 최소 실행 예제에 이어서 실행합니다.
from optimaru.project_job_scheduling.constraints import (
MakespanCost, ProjectDelayCost, RenewableResourceCapacityCost, get_constraints,
)
picked = get_constraints([RenewableResourceCapacityCost(), ProjectDelayCost(), MakespanCost(weight=2)])
custom = pj.solve(problem, time_limit_seconds=2.0, random_seed=42, constraints=picked)
print(custom.score_hard, custom.score_medium, custom.score_soft)
제약 사용법 →
기존 해에서 시작하기
최신 버전이 필요합니다. pip install -U optimaru로 올려 주세요.
with_initial_solution(problem, solution)은 solution에서 탐색을 시작하는 problem의 복사본을 돌려줍니다. solution은 solve가 돌려준 모델이나 그 JSON 딕셔너리입니다.
위의 최소 실행 예제에 이어서 실행합니다.
seeded = pj.with_initial_solution(problem, result.model_dump(mode="json"))
again = pj.solve(seeded, time_limit_seconds=2.0, random_seed=43)
print(again.score_hard, again.score_medium, again.score_soft)
- 배정은 id로 맞춥니다.
- 완전한 해를 넣으면 구성 단계를 건너뛰고 그 해에서 바로 탐색을 시작합니다. 일부만 넣으면 넣은 부분은 그대로 두고, 빠진 배정만 새로 구성합니다.
time_limit_seconds=0을 주면 바로 멈추므로 완전한 해는 넣은 그대로 돌아옵니다. step_count_limit = 0은 제한이 없다는 뜻이므로, 바로 멈추려면 시간 제한을 쓰세요.- 고정된 배정은 해의 내용과 상관없이
problem에 있는 값을 유지합니다. SOURCE와 SINK는 항상 여기에 해당합니다. - 모르는 id, 작업에 없는 모드, 모드 없는 지연 값, 범위를 벗어난 지연 값은
ValueError를 냅니다.
예외와 제한
- 프로젝트마다
SOURCE와 SINK 작업을 넣으세요. 지연과 전체 소요 기간을 SINK에서 읽기 때문입니다. 없으면 오류 없이 그 항목이 0으로 계산됩니다. - 배정과 작업은 1:1이어야 합니다.
release_date는 지연을 재는 기준일 뿐, 프로젝트 시작을 늦추지 않습니다.
직접 해 보기