Spaces:
Sleeping
Sleeping
Update partial_optimizer.py
Browse files- partial_optimizer.py +59 -294
partial_optimizer.py
CHANGED
|
@@ -71,61 +71,22 @@ class PartialOptimizer:
|
|
| 71 |
self.tasks_by_subject[t.subject.subject_code].append(t)
|
| 72 |
|
| 73 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 74 |
-
#
|
| 75 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
'SUBJECT_PREFERRED_TIME', 'HEAVY_SUBJECT_MORNING',
|
| 91 |
-
'LAB_MUST_CONSECUTIVE', 'NO_BACK_TO_BACK_SUBJECTS',
|
| 92 |
-
'DISTRIBUTE_SUBJECTS_EVENLY', 'SUBJECT_SPACING',
|
| 93 |
-
'NO_FREE_PERIOD',
|
| 94 |
-
}
|
| 95 |
-
|
| 96 |
-
# Type aliases β map SLM variants to canonical types
|
| 97 |
-
ALIASES = {
|
| 98 |
-
'SUBJECT_FREE_DAY': 'CANCEL_CLASS',
|
| 99 |
-
'FACULTY_LEAVE': 'FACULTY_SUBSTITUTION',
|
| 100 |
-
'BLOCK_SLOT': 'SECTION_FREE_SLOT',
|
| 101 |
-
'REMOVE_CLASS': 'CANCEL_CLASS',
|
| 102 |
-
'DELETE_CLASS': 'CANCEL_CLASS',
|
| 103 |
-
'CLASS_CANCELLED': 'CANCEL_CLASS',
|
| 104 |
-
'SHIFT_CLASS': 'MOVE_CLASS',
|
| 105 |
-
'RELOCATE_CLASS': 'MOVE_CLASS',
|
| 106 |
-
'TEACHER_SUBSTITUTION': 'FACULTY_SUBSTITUTION',
|
| 107 |
-
'REPLACE_FACULTY': 'FACULTY_SUBSTITUTION',
|
| 108 |
-
'SWAP_FACULTY': 'FACULTY_SUBSTITUTION',
|
| 109 |
-
'SUBJECT_UNAVAILABLE': 'CANCEL_CLASS',
|
| 110 |
-
'NO_CLASS': 'CANCEL_CLASS',
|
| 111 |
-
'HOLIDAY': 'MARK_HOLIDAY',
|
| 112 |
-
'MOVE_LAB': 'RESCHEDULE_LAB',
|
| 113 |
-
'SHIFT_LAB': 'RESCHEDULE_LAB',
|
| 114 |
-
'LOCK_SLOT': 'FREEZE_SLOT',
|
| 115 |
-
'PIN_SLOT': 'FREEZE_SLOT',
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
def _resolve_alias(self, constraint: Dict[str, Any]) -> Dict[str, Any]:
|
| 119 |
-
"""Resolve type aliases to canonical constraint types."""
|
| 120 |
-
ctype = constraint.get('type', '').upper()
|
| 121 |
-
if ctype in self.ALIASES:
|
| 122 |
-
constraint = dict(constraint)
|
| 123 |
-
constraint['type'] = self.ALIASES[ctype]
|
| 124 |
-
return constraint
|
| 125 |
-
|
| 126 |
-
def _get_direct_handler(self, ctype: str):
|
| 127 |
-
"""Return the direct-mutation handler function for a constraint type."""
|
| 128 |
-
handlers = {
|
| 129 |
'FACULTY_SUBSTITUTION': self._op_faculty_substitution,
|
| 130 |
'MOVE_CLASS': self._op_move_class,
|
| 131 |
'SWAP_CLASSES': self._op_swap_classes,
|
|
@@ -137,254 +98,58 @@ class PartialOptimizer:
|
|
| 137 |
'CHANGE_FACULTY': self._op_change_faculty,
|
| 138 |
'FREEZE_SLOT': self._op_freeze_slot,
|
| 139 |
}
|
| 140 |
-
return handlers.get(ctype)
|
| 141 |
-
|
| 142 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 143 |
-
# PUBLIC ENTRY POINT (single constraint β legacy)
|
| 144 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
return self._reoptimize(slm_constraint, time_limit)
|
| 163 |
else:
|
| 164 |
return ('NO_CHANGE', self.current_solution, [],
|
| 165 |
f'β οΈ Unknown constraint type: {ctype}')
|
| 166 |
|
| 167 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 168 |
-
# PUBLIC ENTRY POINT (N constraints β batch)
|
| 169 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 170 |
-
|
| 171 |
-
def apply_constraints_batch(
|
| 172 |
-
self,
|
| 173 |
-
constraints: List[Dict[str, Any]],
|
| 174 |
-
time_limit: int = 30,
|
| 175 |
-
) -> Tuple[str, Dict[str, Any], List[str], List[str]]:
|
| 176 |
-
"""
|
| 177 |
-
Apply N constraints together. Direct-mutation constraints are
|
| 178 |
-
applied immediately in sequence; all re-optimization constraints
|
| 179 |
-
are batched into a SINGLE CP-SAT solve so the solver sees them
|
| 180 |
-
simultaneously (avoiding sequential conflicts).
|
| 181 |
-
|
| 182 |
-
Returns: (status, new_solution, affected_task_ids, change_summaries)
|
| 183 |
-
"""
|
| 184 |
-
if not constraints:
|
| 185 |
-
return ('NO_CHANGE', self.current_solution, [], ['No constraints provided.'])
|
| 186 |
-
|
| 187 |
-
# ββ 1. Resolve aliases ββββββββββββββββββββββββββββββββββββββββββ
|
| 188 |
-
resolved = [self._resolve_alias(c) for c in constraints]
|
| 189 |
-
|
| 190 |
-
# ββ 2. Classify into direct vs. reopt βββββββββββββββββββββββββββ
|
| 191 |
-
direct_constraints = []
|
| 192 |
-
reopt_constraints = []
|
| 193 |
-
unknown_types = []
|
| 194 |
-
for c in resolved:
|
| 195 |
-
ctype = c.get('type', '').upper()
|
| 196 |
-
if ctype in self.DIRECT_TYPES:
|
| 197 |
-
direct_constraints.append(c)
|
| 198 |
-
elif ctype in self.REOPT_TYPES:
|
| 199 |
-
reopt_constraints.append(c)
|
| 200 |
-
else:
|
| 201 |
-
unknown_types.append(ctype)
|
| 202 |
-
|
| 203 |
-
all_changes: List[str] = []
|
| 204 |
-
final_solution = self.current_solution
|
| 205 |
-
overall_status = 'NO_CHANGE'
|
| 206 |
-
|
| 207 |
-
# ββ 3. Apply direct-mutation constraints sequentially βββββββββββ
|
| 208 |
-
# (These don't need CP-SAT; they mutate the schedule dict
|
| 209 |
-
# directly. Each sees the result of the previous one.)
|
| 210 |
-
for c in direct_constraints:
|
| 211 |
-
ctype = c.get('type', '').upper()
|
| 212 |
-
handler = self._get_direct_handler(ctype)
|
| 213 |
-
if handler:
|
| 214 |
-
# Update self.current_solution so the handler sees latest state
|
| 215 |
-
self.current_solution = final_solution
|
| 216 |
-
status, new_sol, _, summary = handler(c)
|
| 217 |
-
if status in ('OPTIMAL', 'FEASIBLE'):
|
| 218 |
-
final_solution = new_sol
|
| 219 |
-
overall_status = 'FEASIBLE'
|
| 220 |
-
all_changes.append(summary)
|
| 221 |
-
|
| 222 |
-
# ββ 4. Batch all re-optimization constraints into ONE solve βββββ
|
| 223 |
-
if reopt_constraints:
|
| 224 |
-
# Update solution reference for the reopt phase
|
| 225 |
-
self.current_solution = final_solution
|
| 226 |
-
|
| 227 |
-
# Detect conflicts before solving
|
| 228 |
-
conflicts = self.detect_conflicts(reopt_constraints)
|
| 229 |
-
if conflicts:
|
| 230 |
-
all_changes.append(
|
| 231 |
-
f"β οΈ Potential conflicts detected:\n"
|
| 232 |
-
+ "\n".join(f" β’ {cf}" for cf in conflicts)
|
| 233 |
-
)
|
| 234 |
-
|
| 235 |
-
# Collect affected tasks across ALL reopt constraints
|
| 236 |
-
all_affected = set()
|
| 237 |
-
for c in reopt_constraints:
|
| 238 |
-
affected = self._find_affected_tasks(c)
|
| 239 |
-
all_affected.update(affected)
|
| 240 |
-
|
| 241 |
-
if all_affected:
|
| 242 |
-
# Scale time limit based on constraint count and task count
|
| 243 |
-
scaled_limit = time_limit + (len(reopt_constraints) * 10)
|
| 244 |
-
scaled_limit = min(scaled_limit, 300) # cap at 5 minutes
|
| 245 |
-
|
| 246 |
-
status, partial = self._solve_partial(
|
| 247 |
-
list(all_affected), reopt_constraints, scaled_limit
|
| 248 |
-
)
|
| 249 |
-
|
| 250 |
-
if status in ('OPTIMAL', 'FEASIBLE'):
|
| 251 |
-
final_solution = dict(final_solution)
|
| 252 |
-
final_solution.update(partial)
|
| 253 |
-
overall_status = status
|
| 254 |
-
|
| 255 |
-
# Build change summaries
|
| 256 |
-
changes = []
|
| 257 |
-
for tid in all_affected:
|
| 258 |
-
old = self.current_solution.get(tid, {})
|
| 259 |
-
new = final_solution.get(tid, {})
|
| 260 |
-
if old and new:
|
| 261 |
-
old_slot = f"{const.DAYS[old['day_index']]} P{old['period_index']+1}"
|
| 262 |
-
new_slot = f"{const.DAYS[new['day_index']]} P{new['period_index']+1}"
|
| 263 |
-
if old_slot != new_slot:
|
| 264 |
-
changes.append(
|
| 265 |
-
f"β’ {new.get('subject_code','?').upper()} "
|
| 266 |
-
f"({new.get('section_id','?')}): "
|
| 267 |
-
f"{old_slot} β {new_slot}"
|
| 268 |
-
)
|
| 269 |
-
|
| 270 |
-
summary = (
|
| 271 |
-
f"β
Batch: {len(reopt_constraints)} constraint(s), "
|
| 272 |
-
f"{len(changes)} slot(s) moved:\n" + "\n".join(changes)
|
| 273 |
-
if changes else
|
| 274 |
-
f"β
{len(reopt_constraints)} constraint(s) applied β no slot changes needed."
|
| 275 |
-
)
|
| 276 |
-
all_changes.append(summary)
|
| 277 |
-
else:
|
| 278 |
-
all_changes.append(
|
| 279 |
-
f"β Could not satisfy {len(reopt_constraints)} batched "
|
| 280 |
-
f"constraint(s) affecting {len(all_affected)} task(s). "
|
| 281 |
-
f"Constraints may be too restrictive or mutually conflicting."
|
| 282 |
-
)
|
| 283 |
-
overall_status = status
|
| 284 |
-
else:
|
| 285 |
-
all_changes.append(
|
| 286 |
-
f"βΉοΈ {len(reopt_constraints)} re-optimization constraint(s) "
|
| 287 |
-
f"matched no affected tasks."
|
| 288 |
-
)
|
| 289 |
-
|
| 290 |
-
# ββ 5. Report unknown types βββββββββββββββββββββββββββββββββββββ
|
| 291 |
-
for utype in unknown_types:
|
| 292 |
-
all_changes.append(f"β οΈ Unknown constraint type: {utype}")
|
| 293 |
-
|
| 294 |
-
return (overall_status, final_solution, [], all_changes)
|
| 295 |
-
|
| 296 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 297 |
-
# CONFLICT DETECTION
|
| 298 |
-
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 299 |
-
|
| 300 |
-
def detect_conflicts(
|
| 301 |
-
self, constraints: List[Dict[str, Any]]
|
| 302 |
-
) -> List[str]:
|
| 303 |
-
"""
|
| 304 |
-
Detect obvious conflicts between constraints BEFORE solving.
|
| 305 |
-
Returns a list of human-readable conflict descriptions.
|
| 306 |
-
Does not block solving β just warns the user.
|
| 307 |
-
"""
|
| 308 |
-
conflicts = []
|
| 309 |
-
|
| 310 |
-
# Index constraints by faculty and section for cross-checking
|
| 311 |
-
faculty_constraints = defaultdict(list)
|
| 312 |
-
section_constraints = defaultdict(list)
|
| 313 |
-
day_constraints = defaultdict(list)
|
| 314 |
-
|
| 315 |
-
for i, c in enumerate(constraints):
|
| 316 |
-
ctype = c.get('type', '').upper()
|
| 317 |
-
fid = c.get('faculty_id')
|
| 318 |
-
sid = c.get('section_id')
|
| 319 |
-
days = c.get('days') or []
|
| 320 |
-
|
| 321 |
-
if fid:
|
| 322 |
-
faculty_constraints[fid].append((i, c))
|
| 323 |
-
if sid:
|
| 324 |
-
section_constraints[sid].append((i, c))
|
| 325 |
-
for d in days:
|
| 326 |
-
day_constraints[d].append((i, c))
|
| 327 |
-
|
| 328 |
-
# Check: Faculty made unavailable on a day where another
|
| 329 |
-
# constraint tries to move their class TO that day
|
| 330 |
-
for fid, fac_cs in faculty_constraints.items():
|
| 331 |
-
unavailable_days = set()
|
| 332 |
-
move_to_days = set()
|
| 333 |
-
for _, c in fac_cs:
|
| 334 |
-
ctype = c.get('type', '').upper()
|
| 335 |
-
if ctype == 'FACULTY_UNAVAILABLE':
|
| 336 |
-
for d in (c.get('days') or []):
|
| 337 |
-
unavailable_days.add(d)
|
| 338 |
-
if ctype == 'SUBJECT_PREFERRED_TIME':
|
| 339 |
-
# Check if we're trying to move this faculty's
|
| 340 |
-
# subject when they're also being made unavailable
|
| 341 |
-
move_to_days.add(c.get('subject_code', '?'))
|
| 342 |
-
|
| 343 |
-
if unavailable_days and move_to_days:
|
| 344 |
-
fac_name = self.fac_by_id.get(fid)
|
| 345 |
-
fac_label = fac_name.name if fac_name else fid
|
| 346 |
-
conflicts.append(
|
| 347 |
-
f"Faculty '{fac_label}' is made unavailable on "
|
| 348 |
-
f"{', '.join(unavailable_days)} but other constraints "
|
| 349 |
-
f"affect their subjects ({', '.join(move_to_days)})"
|
| 350 |
-
)
|
| 351 |
-
|
| 352 |
-
# Check: Section has conflicting free-slot and no-free-period
|
| 353 |
-
for sid, sec_cs in section_constraints.items():
|
| 354 |
-
free_slots = set()
|
| 355 |
-
no_free_periods = set()
|
| 356 |
-
for _, c in sec_cs:
|
| 357 |
-
ctype = c.get('type', '').upper()
|
| 358 |
-
if ctype == 'SECTION_FREE_SLOT':
|
| 359 |
-
slot = c.get('slot')
|
| 360 |
-
if slot is not None:
|
| 361 |
-
free_slots.add(slot - 1) # 0-indexed
|
| 362 |
-
if ctype == 'NO_FREE_PERIOD':
|
| 363 |
-
for p in (c.get('periods') or []):
|
| 364 |
-
no_free_periods.add(p)
|
| 365 |
-
|
| 366 |
-
overlap = free_slots & no_free_periods
|
| 367 |
-
if overlap:
|
| 368 |
-
conflicts.append(
|
| 369 |
-
f"Section '{sid}': period(s) {[p+1 for p in overlap]} "
|
| 370 |
-
f"are set as BOTH free-slot AND no-free-period"
|
| 371 |
-
)
|
| 372 |
-
|
| 373 |
-
# Check: Working-day restrictions that eliminate too many days
|
| 374 |
-
for fid, fac_cs in faculty_constraints.items():
|
| 375 |
-
for _, c in fac_cs:
|
| 376 |
-
if c.get('type', '').upper() == 'WORKING_DAYS':
|
| 377 |
-
allowed = c.get('days') or []
|
| 378 |
-
if len(allowed) < 2:
|
| 379 |
-
fac_name = self.fac_by_id.get(fid)
|
| 380 |
-
fac_label = fac_name.name if fac_name else fid
|
| 381 |
-
conflicts.append(
|
| 382 |
-
f"Working-days constraint for '{fac_label}' allows "
|
| 383 |
-
f"only {len(allowed)} day(s) β likely too restrictive"
|
| 384 |
-
)
|
| 385 |
-
|
| 386 |
-
return conflicts
|
| 387 |
-
|
| 388 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 389 |
# DIRECT MUTATION OPERATIONS
|
| 390 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 71 |
self.tasks_by_subject[t.subject.subject_code].append(t)
|
| 72 |
|
| 73 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 74 |
+
# PUBLIC ENTRY POINT
|
| 75 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 76 |
|
| 77 |
+
def apply_constraint_and_reoptimize(
|
| 78 |
+
self,
|
| 79 |
+
slm_constraint: Dict[str, Any],
|
| 80 |
+
time_limit: int = 30
|
| 81 |
+
) -> Tuple[str, Dict[str, Any], List[str], str]:
|
| 82 |
+
"""
|
| 83 |
+
Route to the correct handler based on constraint type.
|
| 84 |
+
Returns: (status, new_solution, affected_task_ids, summary)
|
| 85 |
+
"""
|
| 86 |
+
ctype = slm_constraint.get('type', '').upper()
|
| 87 |
+
|
| 88 |
+
# Direct mutation handlers (no CP-SAT needed)
|
| 89 |
+
direct_handlers = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
'FACULTY_SUBSTITUTION': self._op_faculty_substitution,
|
| 91 |
'MOVE_CLASS': self._op_move_class,
|
| 92 |
'SWAP_CLASSES': self._op_swap_classes,
|
|
|
|
| 98 |
'CHANGE_FACULTY': self._op_change_faculty,
|
| 99 |
'FREEZE_SLOT': self._op_freeze_slot,
|
| 100 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
# Re-optimization handlers (CP-SAT partial solve)
|
| 103 |
+
reopt_handlers = {
|
| 104 |
+
'FACULTY_UNAVAILABLE': True,
|
| 105 |
+
'FACULTY_FREE_DAY': True,
|
| 106 |
+
'FACULTY_MAX_DAILY_HOURS': True,
|
| 107 |
+
'FACULTY_NO_CONSECUTIVE': True,
|
| 108 |
+
'SECTION_FREE_SLOT': True,
|
| 109 |
+
'WORKING_DAYS': True,
|
| 110 |
+
'SUBJECT_PREFERRED_TIME': True,
|
| 111 |
+
'HEAVY_SUBJECT_MORNING': True,
|
| 112 |
+
'LAB_MUST_CONSECUTIVE': True,
|
| 113 |
+
'NO_BACK_TO_BACK_SUBJECTS': True,
|
| 114 |
+
'DISTRIBUTE_SUBJECTS_EVENLY': True,
|
| 115 |
+
'SUBJECT_SPACING': True,
|
| 116 |
+
'NO_FREE_PERIOD': True,
|
| 117 |
+
}
|
| 118 |
|
| 119 |
+
# ββ Type aliases β map SLM variants to canonical types ββββββββββ
|
| 120 |
+
aliases = {
|
| 121 |
+
'SUBJECT_FREE_DAY': 'CANCEL_CLASS',
|
| 122 |
+
'FACULTY_LEAVE': 'FACULTY_SUBSTITUTION',
|
| 123 |
+
'BLOCK_SLOT': 'SECTION_FREE_SLOT',
|
| 124 |
+
'REMOVE_CLASS': 'CANCEL_CLASS',
|
| 125 |
+
'DELETE_CLASS': 'CANCEL_CLASS',
|
| 126 |
+
'CLASS_CANCELLED': 'CANCEL_CLASS',
|
| 127 |
+
'SHIFT_CLASS': 'MOVE_CLASS',
|
| 128 |
+
'RELOCATE_CLASS': 'MOVE_CLASS',
|
| 129 |
+
'TEACHER_SUBSTITUTION': 'FACULTY_SUBSTITUTION',
|
| 130 |
+
'REPLACE_FACULTY': 'FACULTY_SUBSTITUTION',
|
| 131 |
+
'SWAP_FACULTY': 'FACULTY_SUBSTITUTION',
|
| 132 |
+
'SUBJECT_UNAVAILABLE': 'CANCEL_CLASS',
|
| 133 |
+
'NO_CLASS': 'CANCEL_CLASS',
|
| 134 |
+
'HOLIDAY': 'MARK_HOLIDAY',
|
| 135 |
+
'MOVE_LAB': 'RESCHEDULE_LAB',
|
| 136 |
+
'SHIFT_LAB': 'RESCHEDULE_LAB',
|
| 137 |
+
'LOCK_SLOT': 'FREEZE_SLOT',
|
| 138 |
+
'PIN_SLOT': 'FREEZE_SLOT',
|
| 139 |
+
}
|
| 140 |
+
if ctype in aliases:
|
| 141 |
+
slm_constraint = dict(slm_constraint)
|
| 142 |
+
slm_constraint['type'] = aliases[ctype]
|
| 143 |
+
ctype = aliases[ctype]
|
| 144 |
+
|
| 145 |
+
if ctype in direct_handlers:
|
| 146 |
+
return direct_handlers[ctype](slm_constraint)
|
| 147 |
+
elif ctype in reopt_handlers:
|
| 148 |
return self._reoptimize(slm_constraint, time_limit)
|
| 149 |
else:
|
| 150 |
return ('NO_CHANGE', self.current_solution, [],
|
| 151 |
f'β οΈ Unknown constraint type: {ctype}')
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 154 |
# DIRECT MUTATION OPERATIONS
|
| 155 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|