|
25 | 25 | from processscheduler.resource import Worker, CumulativeWorker, SelectWorkers
|
26 | 26 | from processscheduler.constraint import ResourceConstraint
|
27 | 27 | from processscheduler.util import sort_no_duplicates
|
| 28 | +from processscheduler.task import VariableDurationTask |
28 | 29 |
|
29 | 30 |
|
30 | 31 | class WorkLoad(ResourceConstraint):
|
@@ -197,6 +198,333 @@ def __init__(self, **data) -> None:
|
197 | 198 | )
|
198 | 199 |
|
199 | 200 |
|
| 201 | +class ResourcePeriodicallyUnavailable(ResourceConstraint): |
| 202 | + """ |
| 203 | + This constraint sets the unavailability of a resource in terms of time intervals in one period. |
| 204 | +
|
| 205 | + Parameters: |
| 206 | + - resource: The resource for which unavailability is defined. |
| 207 | + - list_of_time_intervals: A list of time intervals *in one period* during which the resource is unavailable for any task. |
| 208 | + For example, [(0, 2), (5, 8)] represents time intervals from 0 to 2 and from 5 to 8 in one period. |
| 209 | + - period: The length of one period after which to repeat the list of time intervals. |
| 210 | + For example, setting this to 5 with [(2, 4)] gives unavailabilities at (2, 4), (7, 9), (12, 14), ... |
| 211 | + - start: The start after which repeating the list of time intervals is active (default is 0). |
| 212 | + - offset: The shift of the repeated list of time intervals (default is 0). |
| 213 | + It might be desired to set also the start parameter to the same value, as otherwise the pattern shifts in from the left or the right into the schedule. |
| 214 | + - end: The end until which repeating the list of time intervals is activate (default is None). |
| 215 | + - optional (bool, optional): Whether the constraint is optional (default is False). |
| 216 | + """ |
| 217 | + |
| 218 | + resource: Union[Worker, CumulativeWorker] |
| 219 | + list_of_time_intervals: List[Tuple[int, int]] |
| 220 | + period: int |
| 221 | + start: int = 0 |
| 222 | + offset: int = 0 |
| 223 | + end: Union[int, None] = None |
| 224 | + |
| 225 | + |
| 226 | + def __init__(self, **data): |
| 227 | + """ |
| 228 | + Initialize a ResourceUnavailable constraint. |
| 229 | +
|
| 230 | + :param resource: The resource for which unavailability is defined. |
| 231 | + :param list_of_time_intervals: A list of time intervals *in one period* during which the resource is unavailable for any task. |
| 232 | + For example, [(0, 2), (5, 8)] represents time intervals from 0 to 2 and from 5 to 8. |
| 233 | + :param period: The length of one period after which to repeat the list of time intervals. |
| 234 | + For example, setting this to 5 with [(2, 4)] gives unavailabilities at (2, 4), (7, 9), (12, 14), ... |
| 235 | + :param start: The start after which repeating the list of time intervals is active (default is 0). |
| 236 | + :param offset: The shift of the repeated list of time intervals (default is 0). |
| 237 | + It might be desired to set also the start parameter to the same value, as otherwise the pattern shifts in from the left or the right into the schedule. |
| 238 | + :param end: The end until which repeating the list of time intervals is activate (default is None). |
| 239 | + :param optional: Whether the constraint is optional (default is False). |
| 240 | + """ |
| 241 | + super().__init__(**data) |
| 242 | + |
| 243 | + if isinstance(self.resource, Worker): |
| 244 | + workers = [self.resource] |
| 245 | + elif isinstance(self.resource, CumulativeWorker): |
| 246 | + workers = self.resource.cumulative_workers |
| 247 | + |
| 248 | + resource_assigned = False |
| 249 | + |
| 250 | + for interval_lower_bound, interval_upper_bound in self.list_of_time_intervals: |
| 251 | + for worker in workers: |
| 252 | + for start_task_i, end_task_i in worker.get_busy_intervals(): |
| 253 | + resource_assigned = True |
| 254 | + duration = end_task_i - start_task_i |
| 255 | + conds = [ |
| 256 | + z3.Xor( |
| 257 | + (start_task_i - self.offset) % self.period >= interval_upper_bound, |
| 258 | + (start_task_i - self.offset) % self.period + duration <= interval_lower_bound |
| 259 | + ) |
| 260 | + ] |
| 261 | + |
| 262 | + if self.start > 0: |
| 263 | + conds.append(end_task_i <= self.start) |
| 264 | + if self.end is not None: |
| 265 | + conds.append(start_task_i >= self.end) |
| 266 | + |
| 267 | + if len(conds) > 1: |
| 268 | + self.set_z3_assertions(z3.Or(*conds)) |
| 269 | + else: |
| 270 | + self.set_z3_assertions(*conds) |
| 271 | + |
| 272 | + if not resource_assigned: |
| 273 | + raise AssertionError( |
| 274 | + "The resource is not assigned to any task. Please first assign the resource to one or more tasks, and then add the ResourcePeriodicallyUnavailable constraint." |
| 275 | + ) |
| 276 | + |
| 277 | + |
| 278 | +class ResourceInterrupted(ResourceConstraint): |
| 279 | + """ |
| 280 | + This constraint sets the interrupts of a resource in terms of time intervals. |
| 281 | +
|
| 282 | + Parameters: |
| 283 | + - resource: The resource for which interrupts are defined. |
| 284 | + - list_of_time_intervals: A list of time intervals during which the resource is interrupting any task. |
| 285 | + For example, [(0, 2), (5, 8)] represents time intervals from 0 to 2 and from 5 to 8. |
| 286 | + - optional (bool, optional): Whether the constraint is optional (default is False). |
| 287 | + """ |
| 288 | + |
| 289 | + resource: Union[Worker, CumulativeWorker] |
| 290 | + list_of_time_intervals: List[Tuple[int, int]] |
| 291 | + |
| 292 | + def __init__(self, **data) -> None: |
| 293 | + """ |
| 294 | + Initialize a ResourceInterrupted constraint. |
| 295 | +
|
| 296 | + :param resource: The resource for which interrupts are defined. |
| 297 | + :param list_of_time_intervals: A list of time intervals during which the resource is interrupting any task. |
| 298 | + For example, [(0, 2), (5, 8)] represents time intervals from 0 to 2 and from 5 to 8. |
| 299 | + :param optional: Whether the constraint is optional (default is False). |
| 300 | + """ |
| 301 | + super().__init__(**data) |
| 302 | + |
| 303 | + if isinstance(self.resource, Worker): |
| 304 | + workers = [self.resource] |
| 305 | + elif isinstance(self.resource, CumulativeWorker): |
| 306 | + workers = self.resource.cumulative_workers |
| 307 | + |
| 308 | + resource_assigned = False |
| 309 | + |
| 310 | + for worker in workers: |
| 311 | + conds = [] |
| 312 | + for task, (start_task_i, end_task_i) in worker._busy_intervals.items(): |
| 313 | + resource_assigned = True |
| 314 | + overlaps = [] |
| 315 | + |
| 316 | + is_interruptible = isinstance(task, VariableDurationTask) |
| 317 | + |
| 318 | + for interval_lower_bound, interval_upper_bound in self.list_of_time_intervals: |
| 319 | + overlap_condition = z3.Not(z3.Xor( |
| 320 | + start_task_i >= interval_upper_bound, |
| 321 | + end_task_i <= interval_lower_bound |
| 322 | + )) |
| 323 | + overlap = z3.If( |
| 324 | + overlap_condition, |
| 325 | + interval_upper_bound - interval_lower_bound, |
| 326 | + 0 |
| 327 | + ) |
| 328 | + overlaps.append(overlap) |
| 329 | + |
| 330 | + if is_interruptible: |
| 331 | + # just make sure that the task does not start or end within the time interval... |
| 332 | + # TODO: account for zero-duration? |
| 333 | + conds.extend([ |
| 334 | + z3.Xor( |
| 335 | + start_task_i <= interval_lower_bound, |
| 336 | + start_task_i >= interval_upper_bound |
| 337 | + ), |
| 338 | + z3.Xor( |
| 339 | + end_task_i <= interval_lower_bound, |
| 340 | + end_task_i >= interval_upper_bound |
| 341 | + ) |
| 342 | + ]) |
| 343 | + else: |
| 344 | + # ...otherwise make sure the task does not overlap with the time interval |
| 345 | + conds.append( |
| 346 | + z3.Xor( |
| 347 | + start_task_i >= interval_upper_bound, |
| 348 | + end_task_i <= interval_lower_bound |
| 349 | + ) |
| 350 | + ) |
| 351 | + |
| 352 | + if is_interruptible: |
| 353 | + # add assertions for task duration based on the total count of overlapped periods |
| 354 | + total_overlap = z3.Sum(*overlaps) |
| 355 | + conds.append( |
| 356 | + task._duration >= task.min_duration + total_overlap |
| 357 | + ) |
| 358 | + if task.max_duration is not None: |
| 359 | + conds.append( |
| 360 | + task._duration <= task.max_duration + total_overlap |
| 361 | + ) |
| 362 | + |
| 363 | + # TODO: remove AND, as the solver does that anyways? |
| 364 | + self.set_z3_assertions(z3.And(*conds)) |
| 365 | + |
| 366 | + if not resource_assigned: |
| 367 | + raise AssertionError( |
| 368 | + "The resource is not assigned to any task. Please first assign the resource to one or more tasks, and then add the ResourceInterrupted constraint." |
| 369 | + ) |
| 370 | + |
| 371 | + |
| 372 | +class ResourcePeriodicallyInterrupted(ResourceConstraint): |
| 373 | + """ |
| 374 | + This constraint sets the interrupts of a resource in terms of time intervals. |
| 375 | +
|
| 376 | + Parameters: |
| 377 | + - resource: The resource for which interrupts are defined. |
| 378 | + - list_of_time_intervals: A list of time intervals during which the resource is interrupting any task. |
| 379 | + For example, [(0, 2), (5, 8)] represents time intervals from 0 to 2 and from 5 to 8. |
| 380 | + - optional (bool, optional): Whether the constraint is optional (default is False). |
| 381 | + - period: The length of one period after which to repeat the list of time intervals. |
| 382 | + For example, setting this to 5 with [(2, 4)] gives unavailabilities at (2, 4), (7, 9), (12, 14), ... |
| 383 | + - start: The start after which repeating the list of time intervals is active (default is 0). |
| 384 | + - offset: The shift of the repeated list of time intervals (default is 0). |
| 385 | + It might be desired to set also the start parameter to the same value, as otherwise the pattern shifts in from the left or the right into the schedule. |
| 386 | + - end: The end until which repeating the list of time intervals is activate (default is None). |
| 387 | + - optional (bool, optional): Whether the constraint is optional (default is False). |
| 388 | + """ |
| 389 | + |
| 390 | + resource: Union[Worker, CumulativeWorker] |
| 391 | + list_of_time_intervals: List[Tuple[int, int]] |
| 392 | + period: int |
| 393 | + start: int = 0 |
| 394 | + offset: int = 0 |
| 395 | + end: Union[int, None] = None |
| 396 | + |
| 397 | + def __init__(self, **data) -> None: |
| 398 | + """ |
| 399 | + Initialize a ResourceInterrupted constraint. |
| 400 | +
|
| 401 | + :param resource: The resource for which interrupts are defined. |
| 402 | + :param list_of_time_intervals: A list of time intervals during which the resource is interrupting any task. |
| 403 | + For example, [(0, 2), (5, 8)] represents time intervals from 0 to 2 and from 5 to 8. |
| 404 | + :param optional: Whether the constraint is optional (default is False). |
| 405 | + :param period: The length of one period after which to repeat the list of time intervals. |
| 406 | + For example, setting this to 5 with [(2, 4)] gives unavailabilities at (2, 4), (7, 9), (12, 14), ... |
| 407 | + :param start: The start after which repeating the list of time intervals is active (default is 0). |
| 408 | + :param offset: The shift of the repeated list of time intervals (default is 0). |
| 409 | + It might be desired to set also the start parameter to the same value, as otherwise the pattern shifts in from the left or the right into the schedule. |
| 410 | + :param end: The end until which repeating the list of time intervals is activate (default is None). |
| 411 | + :param optional: Whether the constraint is optional (default is False). |
| 412 | + """ |
| 413 | + super().__init__(**data) |
| 414 | + |
| 415 | + if isinstance(self.resource, Worker): |
| 416 | + workers = [self.resource] |
| 417 | + elif isinstance(self.resource, CumulativeWorker): |
| 418 | + workers = self.resource.cumulative_workers |
| 419 | + |
| 420 | + resource_assigned = False |
| 421 | + |
| 422 | + for worker in workers: |
| 423 | + conds = [] |
| 424 | + for task, (start_task_i, end_task_i) in worker._busy_intervals.items(): |
| 425 | + resource_assigned = True |
| 426 | + overlaps = [] |
| 427 | + |
| 428 | + # check if the task allows variable duration |
| 429 | + is_interruptible = isinstance(task, VariableDurationTask) |
| 430 | + |
| 431 | + duration = end_task_i - start_task_i |
| 432 | + folded_start_task_i = (start_task_i - self.offset) % self.period |
| 433 | + folded_end_task_i = (end_task_i - self.offset) % self.period |
| 434 | + |
| 435 | + for interval_lower_bound, interval_upper_bound in self.list_of_time_intervals: |
| 436 | + # intervals need to be defined in one period |
| 437 | + if interval_upper_bound > self.period: |
| 438 | + raise AssertionError(f"interval ({interval_lower_bound}, {interval_upper_bound}) exceeds period {self.period}") |
| 439 | + |
| 440 | + # if true, the folded task overlaps with the time interval in the first period |
| 441 | + crossing_condition = z3.Not(z3.Xor( |
| 442 | + # folded task is completely before the first time interval |
| 443 | + z3.And( |
| 444 | + folded_start_task_i <= interval_lower_bound, |
| 445 | + folded_start_task_i + duration % self.period <= interval_lower_bound, |
| 446 | + ), |
| 447 | + # folded task is completely between the first and second time interval |
| 448 | + z3.And( |
| 449 | + folded_start_task_i >= interval_upper_bound, |
| 450 | + folded_start_task_i + duration % self.period <= interval_lower_bound + self.period, |
| 451 | + ) |
| 452 | + )) |
| 453 | + |
| 454 | + # if true, the task overlaps with at least one time interval |
| 455 | + overlap_condition = z3.Or( |
| 456 | + crossing_condition, |
| 457 | + # task does not fit between two intervals |
| 458 | + duration > interval_lower_bound + self.period - interval_upper_bound |
| 459 | + ) |
| 460 | + |
| 461 | + # adjust the number of crossed time intervals |
| 462 | + crossings = z3.If( |
| 463 | + crossing_condition, |
| 464 | + duration / self.period + 1, |
| 465 | + duration / self.period |
| 466 | + ) |
| 467 | + # calculate the total overlap for this particular time interval |
| 468 | + overlap = z3.If( |
| 469 | + overlap_condition, |
| 470 | + (interval_upper_bound - interval_lower_bound) * crossings, |
| 471 | + 0 |
| 472 | + ) |
| 473 | + overlaps.append(overlap) |
| 474 | + |
| 475 | + if is_interruptible: |
| 476 | + # just make sure that the task does not start or end within one of the time intervals... |
| 477 | + # TODO: account for zero-duration? |
| 478 | + conds.extend([ |
| 479 | + z3.Xor( |
| 480 | + folded_start_task_i <= interval_lower_bound, |
| 481 | + folded_start_task_i >= interval_upper_bound |
| 482 | + ), |
| 483 | + z3.Xor( |
| 484 | + folded_end_task_i <= interval_lower_bound, |
| 485 | + folded_end_task_i >= interval_upper_bound |
| 486 | + ) |
| 487 | + ]) |
| 488 | + else: |
| 489 | + # ...otherwise make sure the task does not overlap with any of time intervals |
| 490 | + conds.append( |
| 491 | + z3.Xor( |
| 492 | + folded_start_task_i >= interval_upper_bound, |
| 493 | + folded_start_task_i + duration <= interval_lower_bound |
| 494 | + ) |
| 495 | + ) |
| 496 | + |
| 497 | + if is_interruptible: |
| 498 | + # add assertions for task duration based on the total count of overlapped periods |
| 499 | + total_overlap = z3.Sum(*overlaps) |
| 500 | + conds.append( |
| 501 | + task._duration >= task.min_duration + total_overlap |
| 502 | + ) |
| 503 | + if task.max_duration is not None: |
| 504 | + conds.append( |
| 505 | + task._duration <= task.max_duration + total_overlap |
| 506 | + ) |
| 507 | + |
| 508 | + # TODO: add AND only of mask is set? |
| 509 | + core = z3.And(*conds) |
| 510 | + |
| 511 | + mask = [core] |
| 512 | + if self.start > 0: |
| 513 | + mask.append(end_task_i <= self.start) |
| 514 | + if self.end is not None: |
| 515 | + mask.append(start_task_i >= self.end) |
| 516 | + |
| 517 | + if len(mask) > 1: |
| 518 | + self.set_z3_assertions(z3.Or(*mask)) |
| 519 | + else: |
| 520 | + self.set_z3_assertions(*mask) |
| 521 | + |
| 522 | + if not resource_assigned: |
| 523 | + raise AssertionError( |
| 524 | + "The resource is not assigned to any task. Please first assign the resource to one or more tasks, and then add the ResourcePeriodicallyInterrupted constraint." |
| 525 | + ) |
| 526 | + |
| 527 | + |
200 | 528 | class ResourceNonDelay(ResourceConstraint):
|
201 | 529 | """All tasks processed by the resource are contiguous, there's no idle while an operation
|
202 | 530 | if waiting for processing"""
|
|
0 commit comments