20
20
from dwave .optimization .symbols import BinaryVariable
21
21
from dwave .system import LeapHybridCQMSampler , LeapHybridNLSampler
22
22
23
- from utils import DAYS , FULL_TIME_SHIFTS , SHIFTS , validate_nl_schedule
23
+ from src . utils import DAYS , FULL_TIME_SHIFTS , SHIFTS , validate_nl_schedule
24
24
25
25
26
26
MSGS = {
42
42
}
43
43
44
44
45
- def build_cqm (# params: ModelParams
45
+ def build_cqm ( # params: ModelParams
46
46
availability : dict [str , list [int ]],
47
47
shifts : list [str ],
48
48
min_shifts : int ,
@@ -118,7 +118,7 @@ def build_cqm(#params: ModelParams
118
118
)
119
119
120
120
for employee in employees_ft :
121
- # Schedule employees for at most max_shifts
121
+ # Schedule full time employees for all their shifts
122
122
cqm .add_constraint (
123
123
quicksum (x [employee , shift ] for shift in shifts ) <= FULL_TIME_SHIFTS ,
124
124
label = f"overtime,{ employee } ," ,
@@ -129,7 +129,7 @@ def build_cqm(#params: ModelParams
129
129
label = f"insufficient,{ employee } ," ,
130
130
)
131
131
132
- # Every shift needs shift_min and shift_max employees working
132
+ # Every shift needs shift_forecast employees working
133
133
for i , shift in enumerate (shifts ):
134
134
cqm .add_constraint (
135
135
sum (x [employee , shift ] for employee in employees ) >= shift_forecast [i ],
@@ -234,16 +234,15 @@ def run_cqm(cqm: ConstrainedQuadraticModel):
234
234
return feasible_sampleset , None
235
235
236
236
237
- def build_nl (
237
+ def build_nl ( # params: ModelParams
238
238
availability : dict [str , list [int ]],
239
239
shifts : list [str ],
240
240
min_shifts : int ,
241
241
max_shifts : int ,
242
- shift_min : int ,
243
- shift_max : int ,
244
- requires_manager : bool ,
242
+ shift_forecast : list ,
245
243
allow_isolated_days_off : bool ,
246
244
max_consecutive_shifts : int ,
245
+ num_full_time : int ,
247
246
) -> tuple [Model , BinaryVariable ]:
248
247
"""Builds an employee scheduling nonlinear model.
249
248
@@ -252,11 +251,10 @@ def build_nl(
252
251
shifts (list[str]): Shift labels.
253
252
min_shifts (int): Minimum shifts per employee.
254
253
max_shifts (int): Maximum shifts per employee.
255
- shift_min (int): Minimum employees per shift.
256
- shift_max (int): Maximum employees per shift.
257
- requires_manager (bool): Whether to require exactly one manager on every shift.
254
+ shift_forecast (list[int]): A list of the number of expected employees needed per shift.
258
255
allow_isolated_days_off (bool): Whether to allow isolated days off.
259
256
max_consecutive_shifts (int): Maximum consecutive shifts per employee.
257
+ num_full_time (int): The number of full time employees.
260
258
261
259
Returns:
262
260
tuple[Model, BinaryVariable]: the NL model and assignments decision variable
@@ -268,6 +266,7 @@ def build_nl(
268
266
# Create a binary symbol representing the assignment of employees to shifts
269
267
# i.e. assignments[employee][shift] = 1 if assigned, else 0
270
268
num_employees = len (employees )
269
+ num_part_time = num_employees - num_full_time
271
270
num_shifts = len (shifts )
272
271
assignments = model .binary ((num_employees , num_shifts ))
273
272
@@ -281,8 +280,8 @@ def build_nl(
281
280
# Initialize model constants
282
281
min_shifts_constant = model .constant (min_shifts )
283
282
max_shifts_constant = model .constant (max_shifts )
284
- shift_min_constant = model .constant (shift_min )
285
- shift_max_constant = model .constant (shift_max )
283
+ full_time_shifts_constant = model .constant (FULL_TIME_SHIFTS )
284
+ shift_forecast_constant = model .constant (shift_forecast )
286
285
max_consecutive_shifts_c = model .constant (max_consecutive_shifts )
287
286
one_c = model .constant (1 )
288
287
@@ -292,28 +291,36 @@ def build_nl(
292
291
293
292
# Objective: for infeasible solutions, focus on right number of shifts for employees
294
293
target_shifts = model .constant ((min_shifts + max_shifts ) / 2 )
295
- shift_difference_list = [
296
- (assignments [e , :].sum () - target_shifts ) ** 2 for e in range (num_employees )
294
+ shift_difference_list_pt = [
295
+ (assignments [e , :].sum () - target_shifts ) ** 2 for e in range (num_part_time )
296
+ ]
297
+ shift_difference_list_ft = [
298
+ (assignments [e , :].sum () - full_time_shifts_constant ) ** 2 for e in range (num_full_time )
297
299
]
298
- obj += add (* shift_difference_list )
300
+ obj += add (* shift_difference_list_pt , * shift_difference_list_ft )
299
301
300
302
model .minimize (- obj )
301
303
302
304
# CONSTRAINTS:
303
305
# Only schedule employees when they're available
304
306
model .add_constraint ((availability_const >= assignments ).all ())
305
307
306
- for e in range (len ( employees ) ):
308
+ for e in range (num_part_time ):
307
309
# Schedule employees for at most max_shifts
308
310
model .add_constraint (assignments [e , :].sum () <= max_shifts_constant )
309
311
310
312
# Schedule employees for at least min_shifts
311
313
model .add_constraint (assignments [e , :].sum () >= min_shifts_constant )
312
314
313
- # Every shift needs shift_min and shift_max employees working
314
- for s in range (num_shifts ):
315
- model .add_constraint (assignments [:, s ].sum () <= shift_max_constant )
316
- model .add_constraint (assignments [:, s ].sum () >= shift_min_constant )
315
+ for e in range (num_full_time ):
316
+ # Schedule full time employees for all their shifts
317
+ model .add_constraint (assignments [e , :].sum () == full_time_shifts_constant )
318
+
319
+ # Every shift needs shift_forecast employees working
320
+ # model.add_constraint(([assignments[:, s].sum() for s in range(num_shifts)] == shift_forecast_constant).all())
321
+
322
+ # shft_fcst = model.constant(shift_forecast)
323
+ model .add_constraint ((assignments .sum (axis = 0 ) == shift_forecast_constant ).all ())
317
324
318
325
managers_c = model .constant (
319
326
[employees .index (e ) for e in employees if e [- 3 :] == "Mgr" ]
@@ -326,7 +333,7 @@ def build_nl(
326
333
negthree_c = model .constant (- 3 )
327
334
zero_c = model .constant (0 )
328
335
# Adding many small constraints greatly improves feasibility
329
- for e in range (len ( employees ) ):
336
+ for e in range (num_part_time ):
330
337
for s1 in range (len (shifts ) - 2 ):
331
338
s2 , s3 = s1 + 1 , s1 + 2
332
339
model .add_constraint (
@@ -337,12 +344,11 @@ def build_nl(
337
344
<= zero_c
338
345
)
339
346
340
- if requires_manager :
341
- for shift in range (len (shifts )):
342
- model .add_constraint (assignments [managers_c ][:, shift ].sum () == one_c )
347
+ for shift in range (len (shifts )):
348
+ model .add_constraint (assignments [managers_c ][:, shift ].sum () >= one_c )
343
349
344
350
# Don't exceed max_consecutive_shifts
345
- for e in range (num_employees ):
351
+ for e in range (num_part_time ):
346
352
for s in range (num_shifts - max_consecutive_shifts + 1 ):
347
353
s_window = s + max_consecutive_shifts + 1
348
354
model .add_constraint (
@@ -368,12 +374,11 @@ def run_nl(
368
374
shifts : list [str ],
369
375
min_shifts : int ,
370
376
max_shifts : int ,
371
- shift_min : int ,
372
- shift_max : int ,
373
- requires_manager : bool ,
377
+ shift_forecast : list [int ],
374
378
allow_isolated_days_off : bool ,
375
379
max_consecutive_shifts : int ,
376
- time_limit : int | None = None ,
380
+ num_full_time : int ,
381
+ time_limit : Optional [int ] = None ,
377
382
msgs : dict [str , tuple [str , str ]] = MSGS ,
378
383
) -> Optional [defaultdict [str , list [str ]]]:
379
384
"""Solves the NL scheduling model and detects any errors.
@@ -395,11 +400,10 @@ def run_nl(
395
400
shifts ,
396
401
min_shifts ,
397
402
max_shifts ,
398
- shift_min ,
399
- shift_max ,
400
- requires_manager ,
403
+ shift_forecast ,
401
404
allow_isolated_days_off ,
402
405
max_consecutive_shifts ,
406
+ num_full_time ,
403
407
)
404
408
405
409
# Return errors if any error message list is populated
0 commit comments