dabke 0.82.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +22 -24
- package/dist/client.d.ts +39 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/client.schemas.d.ts +118 -18
- package/dist/client.schemas.d.ts.map +1 -1
- package/dist/client.schemas.js +110 -6
- package/dist/client.schemas.js.map +1 -1
- package/dist/client.types.d.ts +63 -7
- package/dist/client.types.d.ts.map +1 -1
- package/dist/client.types.js.map +1 -1
- package/dist/cpsat/cost.d.ts +5 -2
- package/dist/cpsat/cost.d.ts.map +1 -1
- package/dist/cpsat/cost.js +13 -6
- package/dist/cpsat/cost.js.map +1 -1
- package/dist/cpsat/model-builder.d.ts +42 -61
- package/dist/cpsat/model-builder.d.ts.map +1 -1
- package/dist/cpsat/model-builder.js +451 -56
- package/dist/cpsat/model-builder.js.map +1 -1
- package/dist/cpsat/response.d.ts +19 -7
- package/dist/cpsat/response.d.ts.map +1 -1
- package/dist/cpsat/response.js +48 -9
- package/dist/cpsat/response.js.map +1 -1
- package/dist/cpsat/rule-descriptor.d.ts +250 -0
- package/dist/cpsat/rule-descriptor.d.ts.map +1 -0
- package/dist/cpsat/rule-descriptor.js +51 -0
- package/dist/cpsat/rule-descriptor.js.map +1 -0
- package/dist/cpsat/rules/artifacts.d.ts +9 -0
- package/dist/cpsat/rules/artifacts.d.ts.map +1 -0
- package/dist/cpsat/rules/artifacts.js +31 -0
- package/dist/cpsat/rules/artifacts.js.map +1 -0
- package/dist/cpsat/rules/assign-together.d.ts +18 -20
- package/dist/cpsat/rules/assign-together.d.ts.map +1 -1
- package/dist/cpsat/rules/assign-together.js +90 -54
- package/dist/cpsat/rules/assign-together.js.map +1 -1
- package/dist/cpsat/rules/assigned-day.d.ts +12 -0
- package/dist/cpsat/rules/assigned-day.d.ts.map +1 -0
- package/dist/cpsat/rules/assigned-day.js +43 -0
- package/dist/cpsat/rules/assigned-day.js.map +1 -0
- package/dist/cpsat/rules/assignment-priority.d.ts +15 -29
- package/dist/cpsat/rules/assignment-priority.d.ts.map +1 -1
- package/dist/cpsat/rules/assignment-priority.js +47 -50
- package/dist/cpsat/rules/assignment-priority.js.map +1 -1
- package/dist/cpsat/rules/assignment-terms.d.ts +6 -0
- package/dist/cpsat/rules/assignment-terms.d.ts.map +1 -0
- package/dist/cpsat/rules/assignment-terms.js +19 -0
- package/dist/cpsat/rules/assignment-terms.js.map +1 -0
- package/dist/cpsat/rules/concurrent-intervals.d.ts +19 -0
- package/dist/cpsat/rules/concurrent-intervals.d.ts.map +1 -0
- package/dist/cpsat/rules/concurrent-intervals.js +57 -0
- package/dist/cpsat/rules/concurrent-intervals.js.map +1 -0
- package/dist/cpsat/rules/cost-utils.d.ts +1 -3
- package/dist/cpsat/rules/cost-utils.d.ts.map +1 -1
- package/dist/cpsat/rules/cost-utils.js +0 -7
- package/dist/cpsat/rules/cost-utils.js.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.d.ts +10 -10
- package/dist/cpsat/rules/day-cost-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-multiplier.js +88 -77
- package/dist/cpsat/rules/day-cost-multiplier.js.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.d.ts +10 -10
- package/dist/cpsat/rules/day-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/day-cost-surcharge.js +76 -65
- package/dist/cpsat/rules/day-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/index.d.ts +30 -21
- package/dist/cpsat/rules/index.d.ts.map +1 -1
- package/dist/cpsat/rules/index.js +30 -21
- package/dist/cpsat/rules/index.js.map +1 -1
- package/dist/cpsat/rules/location-preference.d.ts +14 -21
- package/dist/cpsat/rules/location-preference.d.ts.map +1 -1
- package/dist/cpsat/rules/location-preference.js +45 -45
- package/dist/cpsat/rules/location-preference.js.map +1 -1
- package/dist/cpsat/rules/max-concurrent-assignments.d.ts +46 -0
- package/dist/cpsat/rules/max-concurrent-assignments.d.ts.map +1 -0
- package/dist/cpsat/rules/max-concurrent-assignments.js +121 -0
- package/dist/cpsat/rules/max-concurrent-assignments.js.map +1 -0
- package/dist/cpsat/rules/max-consecutive-days.d.ts +18 -17
- package/dist/cpsat/rules/max-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/max-consecutive-days.js +67 -66
- package/dist/cpsat/rules/max-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/max-days-of-week-per-period.d.ts +45 -0
- package/dist/cpsat/rules/max-days-of-week-per-period.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-of-week-per-period.js +95 -0
- package/dist/cpsat/rules/max-days-of-week-per-period.js.map +1 -0
- package/dist/cpsat/rules/max-days-week.d.ts +30 -0
- package/dist/cpsat/rules/max-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/max-days-week.js +83 -0
- package/dist/cpsat/rules/max-days-week.js.map +1 -0
- package/dist/cpsat/rules/max-hours-day.d.ts +14 -31
- package/dist/cpsat/rules/max-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-day.js +56 -72
- package/dist/cpsat/rules/max-hours-day.js.map +1 -1
- package/dist/cpsat/rules/max-hours-week.d.ts +15 -31
- package/dist/cpsat/rules/max-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/max-hours-week.js +60 -75
- package/dist/cpsat/rules/max-hours-week.js.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.d.ts +17 -33
- package/dist/cpsat/rules/max-shifts-day.d.ts.map +1 -1
- package/dist/cpsat/rules/max-shifts-day.js +57 -73
- package/dist/cpsat/rules/max-shifts-day.js.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.d.ts +18 -18
- package/dist/cpsat/rules/min-consecutive-days.d.ts.map +1 -1
- package/dist/cpsat/rules/min-consecutive-days.js +123 -94
- package/dist/cpsat/rules/min-consecutive-days.js.map +1 -1
- package/dist/cpsat/rules/min-days-of-week-per-period.d.ts +35 -0
- package/dist/cpsat/rules/min-days-of-week-per-period.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-of-week-per-period.js +108 -0
- package/dist/cpsat/rules/min-days-of-week-per-period.js.map +1 -0
- package/dist/cpsat/rules/min-days-week.d.ts +21 -0
- package/dist/cpsat/rules/min-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/min-days-week.js +95 -0
- package/dist/cpsat/rules/min-days-week.js.map +1 -0
- package/dist/cpsat/rules/min-hours-day.d.ts +14 -17
- package/dist/cpsat/rules/min-hours-day.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-day.js +72 -54
- package/dist/cpsat/rules/min-hours-day.js.map +1 -1
- package/dist/cpsat/rules/min-hours-week.d.ts +15 -18
- package/dist/cpsat/rules/min-hours-week.d.ts.map +1 -1
- package/dist/cpsat/rules/min-hours-week.js +75 -57
- package/dist/cpsat/rules/min-hours-week.js.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts +14 -17
- package/dist/cpsat/rules/min-rest-between-shifts.d.ts.map +1 -1
- package/dist/cpsat/rules/min-rest-between-shifts.js +148 -138
- package/dist/cpsat/rules/min-rest-between-shifts.js.map +1 -1
- package/dist/cpsat/rules/minimize-cost.d.ts +2 -18
- package/dist/cpsat/rules/minimize-cost.d.ts.map +1 -1
- package/dist/cpsat/rules/minimize-cost.js +151 -163
- package/dist/cpsat/rules/minimize-cost.js.map +1 -1
- package/dist/cpsat/rules/must-assign.d.ts +12 -0
- package/dist/cpsat/rules/must-assign.d.ts.map +1 -0
- package/dist/cpsat/rules/must-assign.js +54 -0
- package/dist/cpsat/rules/must-assign.js.map +1 -0
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts +6 -10
- package/dist/cpsat/rules/overtime-daily-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-multiplier.js +118 -112
- package/dist/cpsat/rules/overtime-daily-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts +6 -11
- package/dist/cpsat/rules/overtime-daily-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-daily-surcharge.js +104 -99
- package/dist/cpsat/rules/overtime-daily-surcharge.js.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts +9 -21
- package/dist/cpsat/rules/overtime-tiered-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-tiered-multiplier.js +181 -199
- package/dist/cpsat/rules/overtime-tiered-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts +13 -12
- package/dist/cpsat/rules/overtime-weekly-multiplier.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-multiplier.js +143 -143
- package/dist/cpsat/rules/overtime-weekly-multiplier.js.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts +7 -11
- package/dist/cpsat/rules/overtime-weekly-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/overtime-weekly-surcharge.js +124 -119
- package/dist/cpsat/rules/overtime-weekly-surcharge.js.map +1 -1
- package/dist/cpsat/rules/pattern-eligibility.d.ts +6 -0
- package/dist/cpsat/rules/pattern-eligibility.d.ts.map +1 -0
- package/dist/cpsat/rules/pattern-eligibility.js +16 -0
- package/dist/cpsat/rules/pattern-eligibility.js.map +1 -0
- package/dist/cpsat/rules/pattern-time.d.ts +5 -0
- package/dist/cpsat/rules/pattern-time.d.ts.map +1 -0
- package/dist/cpsat/rules/pattern-time.js +20 -0
- package/dist/cpsat/rules/pattern-time.js.map +1 -0
- package/dist/cpsat/rules/registry.d.ts +217 -27
- package/dist/cpsat/rules/registry.d.ts.map +1 -1
- package/dist/cpsat/rules/registry.js +98 -31
- package/dist/cpsat/rules/registry.js.map +1 -1
- package/dist/cpsat/rules/resolver.d.ts +9 -7
- package/dist/cpsat/rules/resolver.d.ts.map +1 -1
- package/dist/cpsat/rules/resolver.js +19 -18
- package/dist/cpsat/rules/resolver.js.map +1 -1
- package/dist/cpsat/rules/role-preference.d.ts +24 -0
- package/dist/cpsat/rules/role-preference.d.ts.map +1 -0
- package/dist/cpsat/rules/role-preference.js +72 -0
- package/dist/cpsat/rules/role-preference.js.map +1 -0
- package/dist/cpsat/rules/rules.types.d.ts +93 -17
- package/dist/cpsat/rules/rules.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.d.ts +25 -12
- package/dist/cpsat/rules/scope.types.d.ts.map +1 -1
- package/dist/cpsat/rules/scope.types.js +14 -19
- package/dist/cpsat/rules/scope.types.js.map +1 -1
- package/dist/cpsat/rules/target-days-week.d.ts +31 -0
- package/dist/cpsat/rules/target-days-week.d.ts.map +1 -0
- package/dist/cpsat/rules/target-days-week.js +80 -0
- package/dist/cpsat/rules/target-days-week.js.map +1 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts +33 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.d.ts.map +1 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.js +146 -0
- package/dist/cpsat/rules/target-peak-concurrent-assignments.js.map +1 -0
- package/dist/cpsat/rules/time-cost-surcharge.d.ts +15 -14
- package/dist/cpsat/rules/time-cost-surcharge.d.ts.map +1 -1
- package/dist/cpsat/rules/time-cost-surcharge.js +82 -96
- package/dist/cpsat/rules/time-cost-surcharge.js.map +1 -1
- package/dist/cpsat/rules/time-off.d.ts +21 -57
- package/dist/cpsat/rules/time-off.d.ts.map +1 -1
- package/dist/cpsat/rules/time-off.js +114 -127
- package/dist/cpsat/rules/time-off.js.map +1 -1
- package/dist/cpsat/rules/variables.d.ts +4 -0
- package/dist/cpsat/rules/variables.d.ts.map +1 -0
- package/dist/cpsat/rules/variables.js +10 -0
- package/dist/cpsat/rules/variables.js.map +1 -0
- package/dist/cpsat/rules.d.ts +20 -3
- package/dist/cpsat/rules.d.ts.map +1 -1
- package/dist/cpsat/rules.js +19 -2
- package/dist/cpsat/rules.js.map +1 -1
- package/dist/cpsat/semantic-time.d.ts +7 -6
- package/dist/cpsat/semantic-time.d.ts.map +1 -1
- package/dist/cpsat/semantic-time.js +15 -28
- package/dist/cpsat/semantic-time.js.map +1 -1
- package/dist/cpsat/types.d.ts +47 -4
- package/dist/cpsat/types.d.ts.map +1 -1
- package/dist/cpsat/types.js +24 -1
- package/dist/cpsat/types.js.map +1 -1
- package/dist/cpsat/utils.d.ts +23 -6
- package/dist/cpsat/utils.d.ts.map +1 -1
- package/dist/cpsat/utils.js +76 -15
- package/dist/cpsat/utils.js.map +1 -1
- package/dist/cpsat/validation-reporter.d.ts +6 -2
- package/dist/cpsat/validation-reporter.d.ts.map +1 -1
- package/dist/cpsat/validation-reporter.js +122 -10
- package/dist/cpsat/validation-reporter.js.map +1 -1
- package/dist/cpsat/validation.types.d.ts +21 -10
- package/dist/cpsat/validation.types.d.ts.map +1 -1
- package/dist/datetime.utils.d.ts +7 -13
- package/dist/datetime.utils.d.ts.map +1 -1
- package/dist/datetime.utils.js +29 -36
- package/dist/datetime.utils.js.map +1 -1
- package/dist/index.d.ts +15 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/schedule/cost.d.ts +192 -0
- package/dist/schedule/cost.d.ts.map +1 -0
- package/dist/schedule/cost.js +151 -0
- package/dist/schedule/cost.js.map +1 -0
- package/dist/schedule/coverage.d.ts +114 -0
- package/dist/schedule/coverage.d.ts.map +1 -0
- package/dist/schedule/coverage.js +56 -0
- package/dist/schedule/coverage.js.map +1 -0
- package/dist/schedule/definition.d.ts +346 -0
- package/dist/schedule/definition.d.ts.map +1 -0
- package/dist/schedule/definition.js +668 -0
- package/dist/schedule/definition.js.map +1 -0
- package/dist/schedule/index.d.ts +67 -0
- package/dist/schedule/index.d.ts.map +1 -0
- package/dist/schedule/index.js +69 -0
- package/dist/schedule/index.js.map +1 -0
- package/dist/schedule/rules.d.ts +678 -0
- package/dist/schedule/rules.d.ts.map +1 -0
- package/dist/schedule/rules.js +554 -0
- package/dist/schedule/rules.js.map +1 -0
- package/dist/schedule/shift-patterns.d.ts +17 -0
- package/dist/schedule/shift-patterns.d.ts.map +1 -0
- package/dist/schedule/shift-patterns.js +24 -0
- package/dist/schedule/shift-patterns.js.map +1 -0
- package/dist/schedule/time-periods.d.ts +79 -0
- package/dist/schedule/time-periods.d.ts.map +1 -0
- package/dist/schedule/time-periods.js +101 -0
- package/dist/schedule/time-periods.js.map +1 -0
- package/dist/types.d.ts +67 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +64 -0
- package/dist/types.js.map +1 -1
- package/package.json +8 -14
- package/solver/src/solver/models.py +41 -4
- package/solver/src/solver/solver.py +546 -157
- package/solver/tests/test_solver.py +363 -7
- package/src/client.schemas.ts +127 -10
- package/src/client.ts +3 -0
- package/src/client.types.ts +73 -7
- package/src/cpsat/cost.ts +17 -7
- package/src/cpsat/model-builder.ts +614 -129
- package/src/cpsat/response.ts +57 -13
- package/src/cpsat/rule-descriptor.ts +311 -0
- package/src/cpsat/rules/artifacts.ts +56 -0
- package/src/cpsat/rules/assign-together.ts +104 -69
- package/src/cpsat/rules/assigned-day.ts +70 -0
- package/src/cpsat/rules/assignment-priority.ts +54 -58
- package/src/cpsat/rules/assignment-terms.ts +43 -0
- package/src/cpsat/rules/concurrent-intervals.ts +93 -0
- package/src/cpsat/rules/cost-utils.ts +1 -9
- package/src/cpsat/rules/day-cost-multiplier.ts +101 -81
- package/src/cpsat/rules/day-cost-surcharge.ts +91 -70
- package/src/cpsat/rules/index.ts +30 -21
- package/src/cpsat/rules/location-preference.ts +47 -52
- package/src/cpsat/rules/max-concurrent-assignments.ts +170 -0
- package/src/cpsat/rules/max-consecutive-days.ts +74 -91
- package/src/cpsat/rules/max-days-of-week-per-period.ts +127 -0
- package/src/cpsat/rules/max-days-week.ts +117 -0
- package/src/cpsat/rules/max-hours-day.ts +64 -84
- package/src/cpsat/rules/max-hours-week.ts +74 -90
- package/src/cpsat/rules/max-shifts-day.ts +65 -85
- package/src/cpsat/rules/min-consecutive-days.ts +161 -127
- package/src/cpsat/rules/min-days-of-week-per-period.ts +144 -0
- package/src/cpsat/rules/min-days-week.ts +122 -0
- package/src/cpsat/rules/min-hours-day.ts +79 -58
- package/src/cpsat/rules/min-hours-week.ts +87 -62
- package/src/cpsat/rules/min-rest-between-shifts.ts +191 -178
- package/src/cpsat/rules/minimize-cost.ts +175 -188
- package/src/cpsat/rules/must-assign.ts +71 -0
- package/src/cpsat/rules/overtime-daily-multiplier.ts +158 -140
- package/src/cpsat/rules/overtime-daily-surcharge.ts +141 -126
- package/src/cpsat/rules/overtime-tiered-multiplier.ts +234 -253
- package/src/cpsat/rules/overtime-weekly-multiplier.ts +183 -175
- package/src/cpsat/rules/overtime-weekly-surcharge.ts +161 -150
- package/src/cpsat/rules/pattern-eligibility.ts +26 -0
- package/src/cpsat/rules/pattern-time.ts +30 -0
- package/src/cpsat/rules/registry.ts +140 -56
- package/src/cpsat/rules/resolver.ts +58 -30
- package/src/cpsat/rules/role-preference.ts +92 -0
- package/src/cpsat/rules/rules.types.ts +92 -17
- package/src/cpsat/rules/scope.types.ts +32 -27
- package/src/cpsat/rules/target-days-week.ts +105 -0
- package/src/cpsat/rules/target-peak-concurrent-assignments.ts +212 -0
- package/src/cpsat/rules/time-cost-surcharge.ts +116 -122
- package/src/cpsat/rules/time-off.ts +154 -163
- package/src/cpsat/rules/variables.ts +11 -0
- package/src/cpsat/rules.ts +43 -17
- package/src/cpsat/semantic-time.ts +29 -43
- package/src/cpsat/types.ts +53 -4
- package/src/cpsat/utils.ts +92 -20
- package/src/cpsat/validation-reporter.ts +170 -19
- package/src/cpsat/validation.types.ts +22 -11
- package/src/datetime.utils.ts +45 -44
- package/src/index.ts +72 -15
- package/src/schedule/cost.ts +221 -0
- package/src/schedule/coverage.ts +164 -0
- package/src/schedule/definition.ts +1447 -0
- package/src/schedule/index.ts +132 -0
- package/src/schedule/rules.ts +1028 -0
- package/src/schedule/shift-patterns.ts +29 -0
- package/src/schedule/time-periods.ts +120 -0
- package/src/types.ts +114 -2
- package/dist/llms.d.ts +0 -2
- package/dist/llms.d.ts.map +0 -1
- package/dist/llms.js +0 -3
- package/dist/llms.js.map +0 -1
- package/dist/schedule.d.ts +0 -917
- package/dist/schedule.d.ts.map +0 -1
- package/dist/schedule.js +0 -1323
- package/dist/schedule.js.map +0 -1
- package/llms.txt +0 -758
- package/src/llms.ts +0 -3
- package/src/schedule.ts +0 -1960
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
+
from time import perf_counter
|
|
6
7
|
from typing import Iterable
|
|
7
8
|
|
|
8
9
|
from ortools.sat.python import cp_model
|
|
@@ -11,13 +12,27 @@ from .models import (
|
|
|
11
12
|
Constraint,
|
|
12
13
|
Objective,
|
|
13
14
|
Options,
|
|
15
|
+
SolverObjectiveStage,
|
|
14
16
|
SolverRequest,
|
|
15
17
|
SolverResponse,
|
|
16
|
-
|
|
18
|
+
SolverHardConstraintConflict,
|
|
19
|
+
SolverSoftConstraintViolation,
|
|
20
|
+
SolverStageResult,
|
|
21
|
+
SolverStatus,
|
|
17
22
|
Term,
|
|
18
23
|
Variable,
|
|
19
24
|
)
|
|
20
25
|
|
|
26
|
+
_SUCCESSFUL_STATUSES = (cp_model.OPTIMAL, cp_model.FEASIBLE)
|
|
27
|
+
_STATUS_MAP: dict[int, str] = {
|
|
28
|
+
int(cp_model.OPTIMAL): "OPTIMAL",
|
|
29
|
+
int(cp_model.FEASIBLE): "FEASIBLE",
|
|
30
|
+
int(cp_model.INFEASIBLE): "INFEASIBLE",
|
|
31
|
+
int(cp_model.MODEL_INVALID): "ERROR",
|
|
32
|
+
int(cp_model.UNKNOWN): "TIMEOUT",
|
|
33
|
+
}
|
|
34
|
+
_ObjectiveLike = Objective | SolverObjectiveStage
|
|
35
|
+
|
|
21
36
|
|
|
22
37
|
@dataclass
|
|
23
38
|
class _VariableBounds:
|
|
@@ -38,6 +53,26 @@ class _TrackedSoftConstraint:
|
|
|
38
53
|
terms: list[Term]
|
|
39
54
|
|
|
40
55
|
|
|
56
|
+
@dataclass
|
|
57
|
+
class _TrackedHardConstraint:
|
|
58
|
+
"""Tracks hard constraints guarded by CP-SAT assumptions."""
|
|
59
|
+
|
|
60
|
+
constraint_id: str
|
|
61
|
+
assumption_var: cp_model.IntVar
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class _BuiltModel:
|
|
66
|
+
"""CP-SAT model and translation metadata."""
|
|
67
|
+
|
|
68
|
+
model: cp_model.CpModel
|
|
69
|
+
vars_map: dict[str, cp_model.IntVar]
|
|
70
|
+
penalty_terms: list[cp_model.LinearExpr]
|
|
71
|
+
penalty_terms_by_stage: dict[str, list[cp_model.LinearExpr]]
|
|
72
|
+
tracked_soft_constraints: list[_TrackedSoftConstraint]
|
|
73
|
+
tracked_hard_constraints: list[_TrackedHardConstraint]
|
|
74
|
+
|
|
75
|
+
|
|
41
76
|
def _collect_bounds(variables: Iterable[Variable]) -> dict[str, _VariableBounds]:
|
|
42
77
|
bounds: dict[str, _VariableBounds] = {}
|
|
43
78
|
for var in variables:
|
|
@@ -83,20 +118,40 @@ def _sum_expr(exprs: list[cp_model.LinearExpr]) -> cp_model.LinearExpr | None:
|
|
|
83
118
|
return total
|
|
84
119
|
|
|
85
120
|
|
|
86
|
-
def _add_linear_constraint(
|
|
121
|
+
def _add_linear_constraint(
|
|
122
|
+
model: cp_model.CpModel,
|
|
123
|
+
constraint: Constraint,
|
|
124
|
+
vars_map: dict[str, cp_model.IntVar],
|
|
125
|
+
tracked_hard_constraints: list[_TrackedHardConstraint],
|
|
126
|
+
constraint_index: int,
|
|
127
|
+
diagnose_hard_conflicts: bool,
|
|
128
|
+
) -> None:
|
|
87
129
|
if not constraint.terms or constraint.op is None or constraint.rhs is None:
|
|
88
130
|
raise ValueError("Linear constraint requires terms, op, and rhs")
|
|
89
131
|
expr = sum(vars_map[t.var] * t.coeff for t in constraint.terms)
|
|
132
|
+
constraint_builder: cp_model.Constraint
|
|
90
133
|
match constraint.op:
|
|
91
134
|
case "<=":
|
|
92
|
-
model.Add(expr <= constraint.rhs)
|
|
135
|
+
constraint_builder = model.Add(expr <= constraint.rhs)
|
|
93
136
|
case ">=":
|
|
94
|
-
model.Add(expr >= constraint.rhs)
|
|
137
|
+
constraint_builder = model.Add(expr >= constraint.rhs)
|
|
95
138
|
case "==":
|
|
96
|
-
model.Add(expr == constraint.rhs)
|
|
139
|
+
constraint_builder = model.Add(expr == constraint.rhs)
|
|
97
140
|
case _:
|
|
98
141
|
raise ValueError(f"Unsupported linear operator {constraint.op}")
|
|
99
142
|
|
|
143
|
+
if not diagnose_hard_conflicts:
|
|
144
|
+
return
|
|
145
|
+
if constraint.id is None:
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
assumption = model.NewBoolVar(f"assume_{constraint_index}_{constraint.id}")
|
|
149
|
+
constraint_builder.OnlyEnforceIf(assumption)
|
|
150
|
+
model.AddAssumption(assumption)
|
|
151
|
+
tracked_hard_constraints.append(
|
|
152
|
+
_TrackedHardConstraint(constraint_id=constraint.id, assumption_var=assumption)
|
|
153
|
+
)
|
|
154
|
+
|
|
100
155
|
|
|
101
156
|
def _add_soft_linear_constraint(
|
|
102
157
|
model: cp_model.CpModel,
|
|
@@ -104,13 +159,10 @@ def _add_soft_linear_constraint(
|
|
|
104
159
|
vars_map: dict[str, cp_model.IntVar],
|
|
105
160
|
bounds: dict[str, _VariableBounds],
|
|
106
161
|
penalty_terms: list[cp_model.LinearExpr],
|
|
107
|
-
|
|
162
|
+
tracked_soft_constraints: list[_TrackedSoftConstraint],
|
|
108
163
|
constraint_index: int,
|
|
109
164
|
) -> None:
|
|
110
|
-
|
|
111
|
-
raise ValueError("Soft linear constraint requires terms, op, rhs, and penalty")
|
|
112
|
-
|
|
113
|
-
min_expr, max_expr = _expression_range(constraint.terms, bounds)
|
|
165
|
+
min_expr, max_expr = _validate_soft_linear_constraint(constraint, bounds)
|
|
114
166
|
max_violation = 0
|
|
115
167
|
violation: cp_model.IntVar | None = None
|
|
116
168
|
expr = sum(vars_map[t.var] * t.coeff for t in constraint.terms)
|
|
@@ -133,7 +185,7 @@ def _add_soft_linear_constraint(
|
|
|
133
185
|
if max_violation > 0:
|
|
134
186
|
penalty_terms.append(violation * constraint.penalty)
|
|
135
187
|
if constraint.id is not None:
|
|
136
|
-
|
|
188
|
+
tracked_soft_constraints.append(
|
|
137
189
|
_TrackedSoftConstraint(
|
|
138
190
|
constraint_id=constraint_id,
|
|
139
191
|
violation_var=violation,
|
|
@@ -144,6 +196,17 @@ def _add_soft_linear_constraint(
|
|
|
144
196
|
)
|
|
145
197
|
|
|
146
198
|
|
|
199
|
+
def _validate_soft_linear_constraint(
|
|
200
|
+
constraint: Constraint,
|
|
201
|
+
bounds: dict[str, _VariableBounds],
|
|
202
|
+
) -> tuple[int, int]:
|
|
203
|
+
if not constraint.terms or constraint.op is None or constraint.rhs is None or constraint.penalty is None:
|
|
204
|
+
raise ValueError("Soft linear constraint requires terms, op, rhs, and penalty")
|
|
205
|
+
if constraint.op not in ("<=", ">="):
|
|
206
|
+
raise ValueError(f"Unsupported soft linear operator {constraint.op}")
|
|
207
|
+
return _expression_range(constraint.terms, bounds)
|
|
208
|
+
|
|
209
|
+
|
|
147
210
|
def _add_exactly_one(model: cp_model.CpModel, constraint: Constraint, vars_map: dict[str, cp_model.IntVar]) -> None:
|
|
148
211
|
if not constraint.vars:
|
|
149
212
|
raise ValueError("Exactly one constraint requires vars")
|
|
@@ -189,13 +252,122 @@ def _add_no_overlap(
|
|
|
189
252
|
model.AddNoOverlap(intervals)
|
|
190
253
|
|
|
191
254
|
|
|
192
|
-
def
|
|
193
|
-
|
|
194
|
-
|
|
255
|
+
def _build_model(
|
|
256
|
+
request: SolverRequest,
|
|
257
|
+
*,
|
|
258
|
+
ignore_soft_constraints: bool = False,
|
|
259
|
+
diagnose_hard_conflicts: bool = False,
|
|
260
|
+
stage_ids: set[str] | None = None,
|
|
261
|
+
) -> _BuiltModel:
|
|
262
|
+
model = cp_model.CpModel()
|
|
263
|
+
|
|
264
|
+
var_bounds = _collect_bounds(request.variables)
|
|
265
|
+
vars_map: dict[str, cp_model.IntVar] = {}
|
|
266
|
+
intervals_map: dict[str, cp_model.IntervalVar] = {}
|
|
267
|
+
|
|
268
|
+
for var in request.variables:
|
|
269
|
+
if var.type == "bool":
|
|
270
|
+
vars_map[var.name] = model.NewBoolVar(var.name)
|
|
271
|
+
elif var.type == "int":
|
|
272
|
+
if var.min is None or var.max is None:
|
|
273
|
+
raise ValueError(f"Int variable {var.name} requires min and max")
|
|
274
|
+
vars_map[var.name] = model.NewIntVar(var.min, var.max, var.name)
|
|
275
|
+
elif var.type == "interval":
|
|
276
|
+
if var.start is None or var.end is None or var.size is None:
|
|
277
|
+
raise ValueError(f"Interval variable {var.name} requires start, end, and size")
|
|
278
|
+
if var.end - var.start != var.size:
|
|
279
|
+
raise ValueError(
|
|
280
|
+
f"Interval variable {var.name} inconsistent: end-start={var.end - var.start} != size={var.size}"
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
start = model.NewConstant(var.start)
|
|
284
|
+
end = model.NewConstant(var.end)
|
|
285
|
+
size = var.size
|
|
286
|
+
|
|
287
|
+
if var.presenceVar is None:
|
|
288
|
+
intervals_map[var.name] = model.NewIntervalVar(start, size, end, var.name)
|
|
289
|
+
else:
|
|
290
|
+
if var.presenceVar not in vars_map:
|
|
291
|
+
raise ValueError(f"Interval variable {var.name} references unknown presenceVar {var.presenceVar}")
|
|
292
|
+
presence = vars_map[var.presenceVar]
|
|
293
|
+
intervals_map[var.name] = model.NewOptionalIntervalVar(start, size, end, presence, var.name)
|
|
294
|
+
else:
|
|
295
|
+
raise ValueError(f"Unsupported variable type {var.type}")
|
|
296
|
+
|
|
297
|
+
penalty_terms: list[cp_model.LinearExpr] = []
|
|
298
|
+
penalty_terms_by_stage: dict[str, list[cp_model.LinearExpr]] = (
|
|
299
|
+
{stage_id: [] for stage_id in stage_ids} if stage_ids is not None else {}
|
|
300
|
+
)
|
|
301
|
+
tracked_soft_constraints: list[_TrackedSoftConstraint] = []
|
|
302
|
+
tracked_hard_constraints: list[_TrackedHardConstraint] = []
|
|
303
|
+
constraint_index = 0
|
|
304
|
+
|
|
305
|
+
for constraint in request.constraints:
|
|
306
|
+
match constraint.type:
|
|
307
|
+
case "linear":
|
|
308
|
+
_add_linear_constraint(
|
|
309
|
+
model,
|
|
310
|
+
constraint,
|
|
311
|
+
vars_map,
|
|
312
|
+
tracked_hard_constraints,
|
|
313
|
+
constraint_index,
|
|
314
|
+
diagnose_hard_conflicts,
|
|
315
|
+
)
|
|
316
|
+
constraint_index += 1
|
|
317
|
+
case "soft_linear":
|
|
318
|
+
if ignore_soft_constraints:
|
|
319
|
+
_validate_soft_linear_constraint(constraint, var_bounds)
|
|
320
|
+
constraint_index += 1
|
|
321
|
+
continue
|
|
322
|
+
if stage_ids is not None:
|
|
323
|
+
if constraint.stage is None:
|
|
324
|
+
raise ValueError("Soft linear constraint requires stage when objectiveStages are used")
|
|
325
|
+
if constraint.stage not in stage_ids:
|
|
326
|
+
raise ValueError(f"Soft linear constraint stage {constraint.stage} is not declared")
|
|
327
|
+
target_penalty_terms = penalty_terms_by_stage[constraint.stage]
|
|
328
|
+
else:
|
|
329
|
+
target_penalty_terms = penalty_terms
|
|
330
|
+
_add_soft_linear_constraint(
|
|
331
|
+
model,
|
|
332
|
+
constraint,
|
|
333
|
+
vars_map,
|
|
334
|
+
var_bounds,
|
|
335
|
+
target_penalty_terms,
|
|
336
|
+
tracked_soft_constraints,
|
|
337
|
+
constraint_index,
|
|
338
|
+
)
|
|
339
|
+
constraint_index += 1
|
|
340
|
+
case "exactly_one":
|
|
341
|
+
_add_exactly_one(model, constraint, vars_map)
|
|
342
|
+
case "at_most_one":
|
|
343
|
+
_add_at_most_one(model, constraint, vars_map)
|
|
344
|
+
case "implication":
|
|
345
|
+
_add_implication(model, constraint, vars_map)
|
|
346
|
+
case "bool_or":
|
|
347
|
+
_add_bool_or(model, constraint, vars_map)
|
|
348
|
+
case "bool_and":
|
|
349
|
+
_add_bool_and(model, constraint, vars_map)
|
|
350
|
+
case "no_overlap":
|
|
351
|
+
_add_no_overlap(model, constraint, intervals_map)
|
|
352
|
+
case _:
|
|
353
|
+
raise ValueError(f"Unsupported constraint type {constraint.type}")
|
|
354
|
+
|
|
355
|
+
return _BuiltModel(
|
|
356
|
+
model=model,
|
|
357
|
+
vars_map=vars_map,
|
|
358
|
+
penalty_terms=penalty_terms,
|
|
359
|
+
penalty_terms_by_stage=penalty_terms_by_stage,
|
|
360
|
+
tracked_soft_constraints=tracked_soft_constraints,
|
|
361
|
+
tracked_hard_constraints=tracked_hard_constraints,
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _compose_objective_expression(
|
|
366
|
+
objective: _ObjectiveLike | None,
|
|
195
367
|
vars_map: dict[str, cp_model.IntVar],
|
|
196
368
|
penalty_terms: list[cp_model.LinearExpr],
|
|
197
|
-
) ->
|
|
198
|
-
penalty_expr
|
|
369
|
+
) -> cp_model.LinearExpr | None:
|
|
370
|
+
penalty_expr = _sum_expr(penalty_terms)
|
|
199
371
|
|
|
200
372
|
if objective:
|
|
201
373
|
objective_terms: list[cp_model.LinearExpr] = (
|
|
@@ -203,160 +375,377 @@ def _build_objective(
|
|
|
203
375
|
)
|
|
204
376
|
obj_expr: cp_model.LinearExpr | None = _sum_expr(objective_terms)
|
|
205
377
|
if penalty_expr is not None and obj_expr is not None:
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return False
|
|
378
|
+
return obj_expr + penalty_expr if objective.sense == "minimize" else obj_expr - penalty_expr
|
|
379
|
+
if obj_expr is not None:
|
|
380
|
+
return obj_expr
|
|
381
|
+
if penalty_expr is not None:
|
|
382
|
+
return penalty_expr if objective.sense == "minimize" else -penalty_expr
|
|
383
|
+
return None
|
|
213
384
|
|
|
214
|
-
|
|
215
|
-
model.Maximize(expr)
|
|
216
|
-
else:
|
|
217
|
-
model.Minimize(expr)
|
|
218
|
-
return True
|
|
385
|
+
return penalty_expr
|
|
219
386
|
|
|
220
|
-
if penalty_expr is not None:
|
|
221
|
-
model.Minimize(penalty_expr)
|
|
222
|
-
return True
|
|
223
387
|
|
|
224
|
-
|
|
388
|
+
def _build_objective(
|
|
389
|
+
model: cp_model.CpModel,
|
|
390
|
+
objective: _ObjectiveLike | None,
|
|
391
|
+
vars_map: dict[str, cp_model.IntVar],
|
|
392
|
+
penalty_terms: list[cp_model.LinearExpr],
|
|
393
|
+
) -> tuple[bool, cp_model.LinearExpr | None]:
|
|
394
|
+
expr = _compose_objective_expression(objective, vars_map, penalty_terms)
|
|
395
|
+
if expr is None:
|
|
396
|
+
return False, None
|
|
225
397
|
|
|
398
|
+
if objective is not None and objective.sense == "maximize":
|
|
399
|
+
model.Maximize(expr)
|
|
400
|
+
else:
|
|
401
|
+
model.Minimize(expr)
|
|
402
|
+
return True, expr
|
|
226
403
|
|
|
227
|
-
def solve_request(request: SolverRequest) -> SolverResponse:
|
|
228
|
-
"""Solve a scheduling request."""
|
|
229
|
-
try:
|
|
230
|
-
model = cp_model.CpModel()
|
|
231
|
-
|
|
232
|
-
var_bounds = _collect_bounds(request.variables)
|
|
233
|
-
vars_map: dict[str, cp_model.IntVar] = {}
|
|
234
|
-
intervals_map: dict[str, cp_model.IntervalVar] = {}
|
|
235
|
-
|
|
236
|
-
# Variable creation
|
|
237
|
-
for var in request.variables:
|
|
238
|
-
if var.type == "bool":
|
|
239
|
-
vars_map[var.name] = model.NewBoolVar(var.name)
|
|
240
|
-
elif var.type == "int":
|
|
241
|
-
if var.min is None or var.max is None:
|
|
242
|
-
raise ValueError(f"Int variable {var.name} requires min and max")
|
|
243
|
-
vars_map[var.name] = model.NewIntVar(var.min, var.max, var.name)
|
|
244
|
-
elif var.type == "interval":
|
|
245
|
-
if var.start is None or var.end is None or var.size is None:
|
|
246
|
-
raise ValueError(f"Interval variable {var.name} requires start, end, and size")
|
|
247
|
-
if var.end - var.start != var.size:
|
|
248
|
-
raise ValueError(
|
|
249
|
-
f"Interval variable {var.name} inconsistent: end-start={var.end - var.start} != size={var.size}"
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
start = model.NewConstant(var.start)
|
|
253
|
-
end = model.NewConstant(var.end)
|
|
254
|
-
size = var.size
|
|
255
|
-
|
|
256
|
-
if var.presenceVar is None:
|
|
257
|
-
intervals_map[var.name] = model.NewIntervalVar(start, size, end, var.name)
|
|
258
|
-
else:
|
|
259
|
-
if var.presenceVar not in vars_map:
|
|
260
|
-
raise ValueError(
|
|
261
|
-
f"Interval variable {var.name} references unknown presenceVar {var.presenceVar}"
|
|
262
|
-
)
|
|
263
|
-
presence = vars_map[var.presenceVar]
|
|
264
|
-
intervals_map[var.name] = model.NewOptionalIntervalVar(start, size, end, presence, var.name)
|
|
265
|
-
else:
|
|
266
|
-
raise ValueError(f"Unsupported variable type {var.type}")
|
|
267
|
-
|
|
268
|
-
penalty_terms: list[cp_model.LinearExpr] = []
|
|
269
|
-
tracked_constraints: list[_TrackedSoftConstraint] = []
|
|
270
|
-
constraint_index = 0
|
|
271
|
-
|
|
272
|
-
for constraint in request.constraints:
|
|
273
|
-
match constraint.type:
|
|
274
|
-
case "linear":
|
|
275
|
-
_add_linear_constraint(model, constraint, vars_map)
|
|
276
|
-
case "soft_linear":
|
|
277
|
-
_add_soft_linear_constraint(
|
|
278
|
-
model,
|
|
279
|
-
constraint,
|
|
280
|
-
vars_map,
|
|
281
|
-
var_bounds,
|
|
282
|
-
penalty_terms,
|
|
283
|
-
tracked_constraints,
|
|
284
|
-
constraint_index,
|
|
285
|
-
)
|
|
286
|
-
constraint_index += 1
|
|
287
|
-
case "exactly_one":
|
|
288
|
-
_add_exactly_one(model, constraint, vars_map)
|
|
289
|
-
case "at_most_one":
|
|
290
|
-
_add_at_most_one(model, constraint, vars_map)
|
|
291
|
-
case "implication":
|
|
292
|
-
_add_implication(model, constraint, vars_map)
|
|
293
|
-
case "bool_or":
|
|
294
|
-
_add_bool_or(model, constraint, vars_map)
|
|
295
|
-
case "bool_and":
|
|
296
|
-
_add_bool_and(model, constraint, vars_map)
|
|
297
|
-
case "no_overlap":
|
|
298
|
-
_add_no_overlap(model, constraint, intervals_map)
|
|
299
|
-
case _:
|
|
300
|
-
raise ValueError(f"Unsupported constraint type {constraint.type}")
|
|
301
|
-
|
|
302
|
-
has_objective = _build_objective(model, request.objective, vars_map, penalty_terms)
|
|
303
404
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
solver.parameters.max_time_in_seconds = options.timeLimitSeconds
|
|
307
|
-
if options.solutionLimit == 1:
|
|
308
|
-
solver.parameters.stop_after_first_solution = True
|
|
309
|
-
|
|
310
|
-
status = solver.Solve(model)
|
|
311
|
-
|
|
312
|
-
status_map: dict[int, str] = {
|
|
313
|
-
cp_model.OPTIMAL: "OPTIMAL",
|
|
314
|
-
cp_model.FEASIBLE: "FEASIBLE",
|
|
315
|
-
cp_model.INFEASIBLE: "INFEASIBLE",
|
|
316
|
-
cp_model.MODEL_INVALID: "ERROR",
|
|
317
|
-
cp_model.UNKNOWN: "TIMEOUT",
|
|
318
|
-
}
|
|
319
|
-
status_text = status_map.get(int(status), "ERROR")
|
|
320
|
-
|
|
321
|
-
if status not in (cp_model.OPTIMAL, cp_model.FEASIBLE):
|
|
322
|
-
solution_info = solver.response_proto.solution_info
|
|
323
|
-
return SolverResponse(
|
|
324
|
-
status=status_text,
|
|
325
|
-
**({"solutionInfo": solution_info} if solution_info else {}),
|
|
326
|
-
)
|
|
405
|
+
def _status_text(status: int) -> str:
|
|
406
|
+
return _STATUS_MAP.get(int(status), "ERROR")
|
|
327
407
|
|
|
328
|
-
statistics: dict[str, int | float] = {
|
|
329
|
-
"solveTimeMs": int(solver.wall_time * 1000),
|
|
330
|
-
"conflicts": solver.num_conflicts,
|
|
331
|
-
"branches": solver.num_branches,
|
|
332
|
-
}
|
|
333
408
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
409
|
+
def _new_solver(options: Options, time_limit_seconds: float) -> cp_model.CpSolver:
|
|
410
|
+
solver = cp_model.CpSolver()
|
|
411
|
+
solver.parameters.max_time_in_seconds = time_limit_seconds
|
|
412
|
+
if options.solutionLimit == 1:
|
|
413
|
+
solver.parameters.stop_after_first_solution = True
|
|
414
|
+
return solver
|
|
337
415
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
416
|
+
|
|
417
|
+
def _solver_statistics(solver: cp_model.CpSolver, has_objective: bool) -> dict[str, int | float]:
|
|
418
|
+
statistics: dict[str, int | float] = {
|
|
419
|
+
"solveTimeMs": int(solver.wall_time * 1000),
|
|
420
|
+
"conflicts": solver.num_conflicts,
|
|
421
|
+
"branches": solver.num_branches,
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if has_objective:
|
|
425
|
+
statistics["objectiveValue"] = solver.objective_value
|
|
426
|
+
statistics["bestObjectiveBound"] = solver.best_objective_bound
|
|
427
|
+
|
|
428
|
+
return statistics
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _solution_values(vars_map: dict[str, cp_model.IntVar], solver: cp_model.CpSolver) -> dict[str, int]:
|
|
432
|
+
return {name: int(solver.value(var)) for name, var in vars_map.items()}
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _collect_soft_constraint_violations(
|
|
436
|
+
tracked_soft_constraints: list[_TrackedSoftConstraint],
|
|
437
|
+
vars_map: dict[str, cp_model.IntVar],
|
|
438
|
+
solver: cp_model.CpSolver,
|
|
439
|
+
) -> list[SolverSoftConstraintViolation]:
|
|
440
|
+
soft_constraint_violations: list[SolverSoftConstraintViolation] = []
|
|
441
|
+
for tracked in tracked_soft_constraints:
|
|
442
|
+
violation_amount = int(solver.value(tracked.violation_var))
|
|
443
|
+
if violation_amount <= 0:
|
|
444
|
+
continue
|
|
445
|
+
actual_value = sum(int(solver.value(vars_map[t.var])) * t.coeff for t in tracked.terms)
|
|
446
|
+
soft_constraint_violations.append(
|
|
447
|
+
SolverSoftConstraintViolation(
|
|
448
|
+
constraintId=tracked.constraint_id,
|
|
449
|
+
violationAmount=violation_amount,
|
|
450
|
+
targetValue=tracked.target_value,
|
|
451
|
+
actualValue=actual_value,
|
|
351
452
|
)
|
|
453
|
+
)
|
|
454
|
+
return soft_constraint_violations
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _collect_hard_constraint_conflicts(
|
|
458
|
+
tracked_hard_constraints: list[_TrackedHardConstraint],
|
|
459
|
+
solver: cp_model.CpSolver,
|
|
460
|
+
) -> list[SolverHardConstraintConflict]:
|
|
461
|
+
assumption_index_to_id = {
|
|
462
|
+
tracked.assumption_var.Index(): tracked.constraint_id for tracked in tracked_hard_constraints
|
|
463
|
+
}
|
|
464
|
+
return [
|
|
465
|
+
SolverHardConstraintConflict(constraintId=constraint_id)
|
|
466
|
+
for assumption_index in solver.SufficientAssumptionsForInfeasibility()
|
|
467
|
+
if (constraint_id := assumption_index_to_id.get(assumption_index)) is not None
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _hard_constraint_conflicts_for_status(
|
|
472
|
+
status: int,
|
|
473
|
+
built: _BuiltModel,
|
|
474
|
+
solver: cp_model.CpSolver,
|
|
475
|
+
) -> list[SolverHardConstraintConflict]:
|
|
476
|
+
if status != cp_model.INFEASIBLE:
|
|
477
|
+
return []
|
|
478
|
+
return _collect_hard_constraint_conflicts(built.tracked_hard_constraints, solver)
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _solution_info_kwargs(solver: cp_model.CpSolver) -> dict[str, str]:
|
|
482
|
+
solution_info = solver.response_proto.solution_info
|
|
483
|
+
return {"solutionInfo": solution_info} if solution_info else {}
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _values_kwargs(values: dict[str, int] | None) -> dict[str, dict[str, int]]:
|
|
487
|
+
return {"values": values} if values is not None else {}
|
|
488
|
+
|
|
352
489
|
|
|
353
|
-
|
|
490
|
+
def _hard_constraint_conflicts_kwargs(
|
|
491
|
+
conflicts: list[SolverHardConstraintConflict],
|
|
492
|
+
) -> dict[str, list[SolverHardConstraintConflict]]:
|
|
493
|
+
return {"hardConstraintConflicts": conflicts} if conflicts else {}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def _response_from_solver(
|
|
497
|
+
status: int,
|
|
498
|
+
solver: cp_model.CpSolver,
|
|
499
|
+
built: _BuiltModel,
|
|
500
|
+
has_objective: bool,
|
|
501
|
+
hard_constraint_conflicts: list[SolverHardConstraintConflict] | None = None,
|
|
502
|
+
) -> SolverResponse:
|
|
503
|
+
status_text = _status_text(status)
|
|
504
|
+
|
|
505
|
+
if status not in _SUCCESSFUL_STATUSES:
|
|
354
506
|
return SolverResponse(
|
|
355
507
|
status=status_text,
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
508
|
+
**_solution_info_kwargs(solver),
|
|
509
|
+
**_hard_constraint_conflicts_kwargs(hard_constraint_conflicts or []),
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
return SolverResponse(
|
|
513
|
+
status=status_text,
|
|
514
|
+
values=_solution_values(built.vars_map, solver),
|
|
515
|
+
statistics=_solver_statistics(solver, has_objective),
|
|
516
|
+
**_solution_info_kwargs(solver),
|
|
517
|
+
softConstraintViolations=_collect_soft_constraint_violations(
|
|
518
|
+
built.tracked_soft_constraints,
|
|
519
|
+
built.vars_map,
|
|
520
|
+
solver,
|
|
521
|
+
),
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _validate_objective_stages(request: SolverRequest) -> list[SolverObjectiveStage]:
|
|
526
|
+
stages = request.objectiveStages
|
|
527
|
+
if stages is None:
|
|
528
|
+
raise ValueError("objectiveStages are required for staged solving")
|
|
529
|
+
if not stages:
|
|
530
|
+
raise ValueError("objectiveStages cannot be empty")
|
|
531
|
+
|
|
532
|
+
seen_stage_ids: set[str] = set()
|
|
533
|
+
for stage in stages:
|
|
534
|
+
if stage.id in seen_stage_ids:
|
|
535
|
+
raise ValueError(f"Duplicate objective stage id {stage.id}")
|
|
536
|
+
seen_stage_ids.add(stage.id)
|
|
537
|
+
|
|
538
|
+
options = request.options or Options()
|
|
539
|
+
if options.solutionLimit == 1:
|
|
540
|
+
raise ValueError("objectiveStages cannot be used with solutionLimit=1")
|
|
541
|
+
|
|
542
|
+
return stages
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _stage_result_from_solver(
|
|
546
|
+
stage: SolverObjectiveStage,
|
|
547
|
+
status: int,
|
|
548
|
+
solver: cp_model.CpSolver,
|
|
549
|
+
has_objective: bool,
|
|
550
|
+
) -> SolverStageResult:
|
|
551
|
+
result_fields: dict[str, str | int | float] = {
|
|
552
|
+
"id": stage.id,
|
|
553
|
+
"status": _status_text(status),
|
|
554
|
+
"solveTimeMs": int(solver.wall_time * 1000),
|
|
555
|
+
}
|
|
556
|
+
if has_objective and status in _SUCCESSFUL_STATUSES:
|
|
557
|
+
result_fields["objectiveValue"] = solver.objective_value
|
|
558
|
+
result_fields["bestObjectiveBound"] = solver.best_objective_bound
|
|
559
|
+
return SolverStageResult(**result_fields)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _aggregate_stage_status(stage_results: list[SolverStageResult]) -> SolverStatus:
|
|
563
|
+
if not stage_results:
|
|
564
|
+
raise ValueError("Cannot aggregate empty stage results")
|
|
565
|
+
|
|
566
|
+
aggregate_status: SolverStatus = "OPTIMAL"
|
|
567
|
+
for stage_result in stage_results:
|
|
568
|
+
match stage_result.status:
|
|
569
|
+
case "OPTIMAL":
|
|
570
|
+
continue
|
|
571
|
+
case "FEASIBLE":
|
|
572
|
+
aggregate_status = "FEASIBLE"
|
|
573
|
+
case "INFEASIBLE" | "TIMEOUT" | "ERROR":
|
|
574
|
+
return stage_result.status
|
|
575
|
+
|
|
576
|
+
return aggregate_status
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _staged_statistics(
|
|
580
|
+
stage_results: list[SolverStageResult],
|
|
581
|
+
conflicts: int,
|
|
582
|
+
branches: int,
|
|
583
|
+
) -> dict[str, int | float]:
|
|
584
|
+
statistics: dict[str, int | float] = {
|
|
585
|
+
"solveTimeMs": sum(stage.solveTimeMs for stage in stage_results),
|
|
586
|
+
"conflicts": conflicts,
|
|
587
|
+
"branches": branches,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
last_successful_stage = next(
|
|
591
|
+
(stage for stage in reversed(stage_results) if stage.status in ("OPTIMAL", "FEASIBLE")),
|
|
592
|
+
None,
|
|
593
|
+
)
|
|
594
|
+
if last_successful_stage is not None and last_successful_stage.objectiveValue is not None:
|
|
595
|
+
statistics["objectiveValue"] = last_successful_stage.objectiveValue
|
|
596
|
+
if last_successful_stage is not None and last_successful_stage.bestObjectiveBound is not None:
|
|
597
|
+
statistics["bestObjectiveBound"] = last_successful_stage.bestObjectiveBound
|
|
598
|
+
|
|
599
|
+
return statistics
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _solve_single_objective(request: SolverRequest) -> SolverResponse:
|
|
603
|
+
options: Options = request.options or Options()
|
|
604
|
+
built = _build_model(request, diagnose_hard_conflicts=options.diagnostics == "hard")
|
|
605
|
+
has_objective, _ = _build_objective(
|
|
606
|
+
built.model,
|
|
607
|
+
request.objective,
|
|
608
|
+
built.vars_map,
|
|
609
|
+
built.penalty_terms,
|
|
610
|
+
)
|
|
611
|
+
|
|
612
|
+
solver = _new_solver(options, options.timeLimitSeconds)
|
|
613
|
+
status = solver.Solve(built.model)
|
|
614
|
+
hard_constraint_conflicts = _hard_constraint_conflicts_for_status(
|
|
615
|
+
status,
|
|
616
|
+
built,
|
|
617
|
+
solver,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return _response_from_solver(status, solver, built, has_objective, hard_constraint_conflicts)
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def _solve_satisfy(request: SolverRequest) -> SolverResponse:
|
|
624
|
+
options: Options = request.options or Options()
|
|
625
|
+
built = _build_model(
|
|
626
|
+
request,
|
|
627
|
+
ignore_soft_constraints=True,
|
|
628
|
+
diagnose_hard_conflicts=options.diagnostics == "hard",
|
|
629
|
+
)
|
|
630
|
+
solver = _new_solver(options, options.timeLimitSeconds)
|
|
631
|
+
status = solver.Solve(built.model)
|
|
632
|
+
hard_constraint_conflicts = _hard_constraint_conflicts_for_status(
|
|
633
|
+
status,
|
|
634
|
+
built,
|
|
635
|
+
solver,
|
|
636
|
+
)
|
|
637
|
+
|
|
638
|
+
return _response_from_solver(
|
|
639
|
+
status,
|
|
640
|
+
solver,
|
|
641
|
+
built,
|
|
642
|
+
has_objective=False,
|
|
643
|
+
hard_constraint_conflicts=hard_constraint_conflicts,
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _solve_staged(request: SolverRequest) -> SolverResponse:
|
|
648
|
+
stages = _validate_objective_stages(request)
|
|
649
|
+
stage_ids = {stage.id for stage in stages}
|
|
650
|
+
options: Options = request.options or Options()
|
|
651
|
+
built = _build_model(
|
|
652
|
+
request,
|
|
653
|
+
stage_ids=stage_ids,
|
|
654
|
+
diagnose_hard_conflicts=options.diagnostics == "hard",
|
|
655
|
+
)
|
|
656
|
+
started_at = perf_counter()
|
|
657
|
+
|
|
658
|
+
stage_results: list[SolverStageResult] = []
|
|
659
|
+
last_solver: cp_model.CpSolver | None = None
|
|
660
|
+
last_values: dict[str, int] | None = None
|
|
661
|
+
total_conflicts = 0
|
|
662
|
+
total_branches = 0
|
|
663
|
+
|
|
664
|
+
for stage in stages:
|
|
665
|
+
remaining_time = options.timeLimitSeconds - (perf_counter() - started_at)
|
|
666
|
+
if remaining_time <= 0:
|
|
667
|
+
stage_results.append(SolverStageResult(id=stage.id, status="TIMEOUT", solveTimeMs=0))
|
|
668
|
+
return SolverResponse(
|
|
669
|
+
status=_aggregate_stage_status(stage_results),
|
|
670
|
+
**_values_kwargs(last_values),
|
|
671
|
+
statistics=_staged_statistics(stage_results, total_conflicts, total_branches),
|
|
672
|
+
stageResults=stage_results,
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
built.model.ClearObjective()
|
|
676
|
+
has_objective, objective_expr = _build_objective(
|
|
677
|
+
built.model,
|
|
678
|
+
stage,
|
|
679
|
+
built.vars_map,
|
|
680
|
+
built.penalty_terms_by_stage[stage.id],
|
|
681
|
+
)
|
|
682
|
+
solver = cp_model.CpSolver()
|
|
683
|
+
solver.parameters.max_time_in_seconds = remaining_time
|
|
684
|
+
status = solver.Solve(built.model)
|
|
685
|
+
total_conflicts += solver.num_conflicts
|
|
686
|
+
total_branches += solver.num_branches
|
|
687
|
+
|
|
688
|
+
stage_result = _stage_result_from_solver(stage, status, solver, has_objective)
|
|
689
|
+
stage_results.append(stage_result)
|
|
690
|
+
|
|
691
|
+
if status not in _SUCCESSFUL_STATUSES:
|
|
692
|
+
hard_constraint_conflicts = _hard_constraint_conflicts_for_status(
|
|
693
|
+
status,
|
|
694
|
+
built,
|
|
695
|
+
solver,
|
|
696
|
+
)
|
|
697
|
+
return SolverResponse(
|
|
698
|
+
status=_aggregate_stage_status(stage_results),
|
|
699
|
+
**_values_kwargs(last_values),
|
|
700
|
+
statistics=_staged_statistics(stage_results, total_conflicts, total_branches),
|
|
701
|
+
**_solution_info_kwargs(solver),
|
|
702
|
+
**_hard_constraint_conflicts_kwargs(hard_constraint_conflicts),
|
|
703
|
+
stageResults=stage_results,
|
|
704
|
+
)
|
|
705
|
+
|
|
706
|
+
last_solver = solver
|
|
707
|
+
last_values = _solution_values(built.vars_map, solver)
|
|
708
|
+
|
|
709
|
+
if has_objective and objective_expr is not None:
|
|
710
|
+
achieved_value = int(solver.value(objective_expr))
|
|
711
|
+
if stage.sense == "maximize":
|
|
712
|
+
built.model.Add(objective_expr >= achieved_value)
|
|
713
|
+
else:
|
|
714
|
+
built.model.Add(objective_expr <= achieved_value)
|
|
715
|
+
|
|
716
|
+
return SolverResponse(
|
|
717
|
+
status=_aggregate_stage_status(stage_results),
|
|
718
|
+
**_values_kwargs(last_values),
|
|
719
|
+
statistics=_staged_statistics(stage_results, total_conflicts, total_branches),
|
|
720
|
+
stageResults=stage_results,
|
|
721
|
+
softConstraintViolations=_collect_soft_constraint_violations(
|
|
722
|
+
built.tracked_soft_constraints,
|
|
723
|
+
built.vars_map,
|
|
724
|
+
last_solver,
|
|
360
725
|
)
|
|
726
|
+
if last_solver is not None
|
|
727
|
+
else [],
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def solve_request(request: SolverRequest) -> SolverResponse:
|
|
732
|
+
"""Solve a scheduling request."""
|
|
733
|
+
try:
|
|
734
|
+
mode = request.mode or "optimize"
|
|
735
|
+
|
|
736
|
+
if mode == "satisfy":
|
|
737
|
+
if request.objective is not None:
|
|
738
|
+
raise ValueError('mode "satisfy" cannot be used with objective')
|
|
739
|
+
if request.objectiveStages is not None:
|
|
740
|
+
raise ValueError('mode "satisfy" cannot be used with objectiveStages')
|
|
741
|
+
return _solve_satisfy(request)
|
|
742
|
+
|
|
743
|
+
if request.objective is not None and request.objectiveStages is not None:
|
|
744
|
+
raise ValueError("objective and objectiveStages cannot both be present")
|
|
745
|
+
|
|
746
|
+
if request.objectiveStages is not None:
|
|
747
|
+
return _solve_staged(request)
|
|
748
|
+
|
|
749
|
+
return _solve_single_objective(request)
|
|
361
750
|
except Exception as exc: # pragma: no cover - surfaced in response
|
|
362
751
|
return SolverResponse(status="ERROR", error=str(exc))
|