Spaces:
Runtime error
Runtime error
File size: 6,769 Bytes
50f82a1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 | from solverforge_legacy.solver.score import (
constraint_provider,
HardSoftScore,
Joiners,
ConstraintFactory,
Constraint,
)
from .domain import Job
# Constraint names
CREW_CONFLICT = "Crew conflict"
MIN_START_DATE = "Min start date"
MAX_END_DATE = "Max end date"
BEFORE_IDEAL_END_DATE = "Before ideal end date"
AFTER_IDEAL_END_DATE = "After ideal end date"
TAG_CONFLICT = "Tag conflict"
@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory):
"""
Defines all constraints for the maintenance scheduling problem.
Args:
constraint_factory: The constraint factory.
Returns:
List of all defined constraints.
"""
return [
# Hard constraints
crew_conflict(constraint_factory),
min_start_date(constraint_factory),
max_end_date(constraint_factory),
# Soft constraints
before_ideal_end_date(constraint_factory),
after_ideal_end_date(constraint_factory),
tag_conflict(constraint_factory),
]
# ************************************************************************
# Hard constraints
# ************************************************************************
def crew_conflict(constraint_factory: ConstraintFactory) -> Constraint:
"""
A crew can do at most one maintenance job at the same time.
Penalizes overlapping jobs assigned to the same crew, proportional to the
number of overlapping days.
"""
return (
constraint_factory.for_each_unique_pair(
Job,
Joiners.equal(lambda job: job.crew),
Joiners.overlapping(
lambda job: job.start_date,
lambda job: job.get_end_date(),
),
)
.filter(lambda job1, job2: job1.crew is not None)
.penalize(
HardSoftScore.ONE_HARD,
lambda job1, job2: _calculate_overlap_days(job1, job2),
)
.as_constraint(CREW_CONFLICT)
)
def min_start_date(constraint_factory: ConstraintFactory) -> Constraint:
"""
Don't start a maintenance job before it's ready to start.
Penalizes jobs that start before their minimum start date, proportional
to the number of days early.
"""
return (
constraint_factory.for_each(Job)
.filter(
lambda job: job.start_date is not None
and job.min_start_date is not None
and job.start_date < job.min_start_date
)
.penalize(
HardSoftScore.ONE_HARD,
lambda job: (job.min_start_date - job.start_date).days,
)
.as_constraint(MIN_START_DATE)
)
def max_end_date(constraint_factory: ConstraintFactory) -> Constraint:
"""
Don't end a maintenance job after it's due.
Penalizes jobs that end after their maximum end date, proportional
to the number of days late.
"""
return (
constraint_factory.for_each(Job)
.filter(
lambda job: job.get_end_date() is not None
and job.max_end_date is not None
and job.get_end_date() > job.max_end_date
)
.penalize(
HardSoftScore.ONE_HARD,
lambda job: (job.get_end_date() - job.max_end_date).days,
)
.as_constraint(MAX_END_DATE)
)
# ************************************************************************
# Soft constraints
# ************************************************************************
def before_ideal_end_date(constraint_factory: ConstraintFactory) -> Constraint:
"""
Early maintenance is expensive because maintenance cycles restart sooner.
Penalizes jobs that finish before their ideal end date.
Weight: 1 point per day early (lowest priority soft constraint).
"""
return (
constraint_factory.for_each(Job)
.filter(
lambda job: job.get_end_date() is not None
and job.ideal_end_date is not None
and job.get_end_date() < job.ideal_end_date
)
.penalize(
HardSoftScore.ONE_SOFT,
lambda job: (job.ideal_end_date - job.get_end_date()).days,
)
.as_constraint(BEFORE_IDEAL_END_DATE)
)
def after_ideal_end_date(constraint_factory: ConstraintFactory) -> Constraint:
"""
Late maintenance is risky because delays can push it over the due date.
Penalizes jobs that finish after their ideal end date.
Weight: 1,000,000 points per day late (high priority soft constraint).
"""
return (
constraint_factory.for_each(Job)
.filter(
lambda job: job.get_end_date() is not None
and job.ideal_end_date is not None
and job.get_end_date() > job.ideal_end_date
)
.penalize(
HardSoftScore.of_soft(1_000_000),
lambda job: (job.get_end_date() - job.ideal_end_date).days,
)
.as_constraint(AFTER_IDEAL_END_DATE)
)
def tag_conflict(constraint_factory: ConstraintFactory) -> Constraint:
"""
Avoid overlapping maintenance jobs with the same tag.
For example, road maintenance in the same area should not overlap.
Penalizes overlapping jobs that share tags, proportional to the number
of shared tags and the overlap duration.
Weight: 1,000 points per tag per day of overlap.
"""
return (
constraint_factory.for_each_unique_pair(
Job,
Joiners.overlapping(
lambda job: job.start_date,
lambda job: job.get_end_date(),
),
)
.filter(lambda job1, job2: len(job1.get_common_tags(job2)) > 0)
.penalize(
HardSoftScore.of_soft(1_000),
lambda job1, job2: len(job1.get_common_tags(job2))
* _calculate_overlap_days(job1, job2),
)
.as_constraint(TAG_CONFLICT)
)
# ************************************************************************
# Helper functions
# ************************************************************************
def _calculate_overlap_days(job1: Job, job2: Job) -> int:
"""
Calculate the number of overlapping calendar days between two jobs.
Args:
job1: First job
job2: Second job
Returns:
Number of overlapping days (calendar days, not working days)
"""
if job1.start_date is None or job2.start_date is None:
return 0
end1 = job1.get_end_date()
end2 = job2.get_end_date()
if end1 is None or end2 is None:
return 0
# Calculate overlap range
overlap_start = max(job1.start_date, job2.start_date)
overlap_end = min(end1, end2)
if overlap_start >= overlap_end:
return 0
return (overlap_end - overlap_start).days
|