anujkum0x commited on
Commit
d577361
·
verified ·
1 Parent(s): 7481eb1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +95 -41
app.py CHANGED
@@ -74,15 +74,16 @@ def optimize_staffing(
74
 
75
  # Parameters
76
  BEDS_PER_STAFF = float(beds_per_staff)
77
- STANDARD_PERIOD_DAYS = 30 # Standard 4-week period
78
 
79
- # Scale MAX_HOURS_PER_STAFF based on the ratio of actual days to standard period
80
- BASE_MAX_HOURS = float(max_hours_per_staff) # This is for a 28-day period
81
  MAX_HOURS_PER_STAFF = BASE_MAX_HOURS * (num_days / STANDARD_PERIOD_DAYS)
82
 
83
  # Log the adjustment for transparency
84
- original_results = f"Input max hours per staff (28-day period): {BASE_MAX_HOURS}\n"
85
- original_results += f"Adjusted max hours for {num_days}-day period: {MAX_HOURS_PER_STAFF:.1f}\n\n"
 
86
 
87
  HOURS_PER_CYCLE = float(hours_per_cycle)
88
  REST_DAYS_PER_WEEK = int(rest_days_per_week)
@@ -224,13 +225,15 @@ def optimize_staffing(
224
 
225
  # Use exact_staff_count if provided, otherwise estimate
226
  if exact_staff_count is not None and exact_staff_count > 0:
227
- # When exact staff count is provided, only create that many staff in the model
 
 
228
  estimated_staff = exact_staff_count
229
- num_staff_to_create = exact_staff_count # Only create exactly this many staff
230
  else:
231
  # Add some buffer for constraints like rest days and shift changes
232
  estimated_staff = max(min_staff_estimate, max_staff_needed + 1)
233
- num_staff_to_create = int(estimated_staff) # Create the estimated number of staff
234
 
235
  def optimize_schedule(num_staff, time_limit=600):
236
  try:
@@ -250,35 +253,59 @@ def optimize_staffing(
250
  # Total hours worked by all staff
251
  total_hours = pl.LpVariable("total_hours", lowBound=0)
252
 
253
- # CRITICAL CHANGE: Remove coverage violation variables - make coverage a hard constraint
254
- # CRITICAL CHANGE: Remove overtime variables - make overtime a hard constraint
255
 
256
- # Objective function now only focuses on minimizing staff count and total hours
257
- model += (
258
- 10**10 * pl.lpSum(staff_used[s] for s in range(1, num_staff+1)) +
259
- 1 * total_hours
260
- )
 
 
 
 
 
 
 
 
261
 
262
- # Link total_hours to the sum of all hours worked
263
- model += total_hours == pl.lpSum(x[(s, d, shift['id'])] * shift['duration']
264
- for s in range(1, num_staff+1)
265
- for d in range(1, num_days+1)
266
- for shift in possible_shifts)
267
-
268
- # Link staff_used variable with shift assignments
269
  for s in range(1, num_staff+1):
270
- model += pl.lpSum(x[(s, d, shift['id'])]
271
- for d in range(1, num_days+1)
272
- for shift in possible_shifts) <= num_days * staff_used[s]
273
-
274
- # If staff is used, they must work at least one shift
275
- model += pl.lpSum(x[(s, d, shift['id'])]
276
- for d in range(1, num_days+1)
277
- for shift in possible_shifts) >= staff_used[s]
278
 
279
- # Maintain staff ordering (to avoid symmetrical solutions)
280
- for s in range(1, num_staff):
281
- model += staff_used[s] >= staff_used[s+1]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
  # Each staff works at most one shift per day
284
  for s in range(1, num_staff+1):
@@ -305,12 +332,12 @@ def optimize_staffing(
305
  # HARD CONSTRAINT: No overtime allowed - strict limit at MAX_HOURS_PER_STAFF
306
  for s in range(1, num_staff+1):
307
  # Calculate total hours worked by this staff
308
- staff_hours = pl.lpSum(x[(s, d, shift['id'])] * shift['duration']
309
  for d in range(1, num_days+1)
310
  for shift in possible_shifts)
311
 
312
  # STRICT constraint: No overtime allowed
313
- model += staff_hours <= MAX_HOURS_PER_STAFF
314
 
315
  # HARD CONSTRAINT: Full coverage required
316
  for d in range(1, num_days+1):
@@ -388,21 +415,28 @@ def optimize_staffing(
388
  if exact_staff_count is not None and exact_staff_count > 0:
389
  # If exact staff count is specified, only try with that count
390
  staff_count = int(exact_staff_count)
391
- results = f"Using exactly {staff_count} staff as specified...\n"
 
 
 
 
392
 
393
  # Try to solve with exactly this many staff
394
  schedule, objective = optimize_schedule(staff_count)
395
 
396
  if schedule is None:
397
  results += f"Failed to find a feasible solution with exactly {staff_count} staff.\n"
398
- results += "Try increasing the staff count.\n"
 
 
399
  return results, None, None, None, None
400
  else:
401
  # Start from theoretical minimum and work up
402
  min_staff = max(1, int(theoretical_min_staff)) # Start from theoretical minimum
403
  max_staff = int(min_staff_estimate) + 5 # Allow some buffer
404
 
405
- results = f"Theoretical minimum staff needed: {theoretical_min_staff:.1f}\n"
 
406
  results += f"Searching for minimum staff count starting from {min_staff}...\n"
407
 
408
  # Try each staff count from min to max
@@ -435,19 +469,39 @@ def optimize_staffing(
435
  total_hours = staff_shifts['duration'].sum()
436
  staff_hours[s] = total_hours
437
 
438
- # After calculating staff hours, filter out staff with 0 hours before displaying
439
- active_staff_hours = {s: hours for s, hours in staff_hours.items() if hours > 0}
 
 
 
 
 
440
 
441
  results += "\nStaff Hours:\n"
 
 
 
442
  for staff_id, hours in active_staff_hours.items():
443
  utilization = (hours / MAX_HOURS_PER_STAFF) * 100
444
- results += f"Staff {staff_id}: {hours} hours ({utilization:.1f}% utilization)\n"
 
 
 
 
 
445
  # Add overtime information
446
  if hours > MAX_HOURS_PER_STAFF:
447
  overtime = hours - MAX_HOURS_PER_STAFF
448
  overtime_percent = (overtime / MAX_HOURS_PER_STAFF) * 100
449
  results += f" Overtime: {overtime:.1f} hours ({overtime_percent:.1f}%)\n"
450
 
 
 
 
 
 
 
 
451
  # Use active_staff_hours for average utilization calculation
452
  active_staff_count = len(active_staff_hours)
453
  avg_utilization = sum(active_staff_hours.values()) / (active_staff_count * MAX_HOURS_PER_STAFF) * 100
 
74
 
75
  # Parameters
76
  BEDS_PER_STAFF = float(beds_per_staff)
77
+ STANDARD_PERIOD_DAYS = 30 # Standard month period (changed from 28 to 30)
78
 
79
+ # Scale MAX_HOURS_PER_STAFF based on the ratio of actual days to standard month
80
+ BASE_MAX_HOURS = float(max_hours_per_staff) # This is for a 30-day period
81
  MAX_HOURS_PER_STAFF = BASE_MAX_HOURS * (num_days / STANDARD_PERIOD_DAYS)
82
 
83
  # Log the adjustment for transparency
84
+ original_results = f"Input max hours per staff (30-day period): {BASE_MAX_HOURS}\n"
85
+ original_results += f"Adjusted max hours for {num_days}-day period: {MAX_HOURS_PER_STAFF:.1f}\n"
86
+ original_results += f"(Adjustment ratio: {num_days}/{STANDARD_PERIOD_DAYS} = {(num_days/STANDARD_PERIOD_DAYS):.2f})\n\n"
87
 
88
  HOURS_PER_CYCLE = float(hours_per_cycle)
89
  REST_DAYS_PER_WEEK = int(rest_days_per_week)
 
225
 
226
  # Use exact_staff_count if provided, otherwise estimate
227
  if exact_staff_count is not None and exact_staff_count > 0:
228
+ # When exact staff count is provided, use it regardless of minimum required
229
+ if exact_staff_count < min_staff_estimate:
230
+ original_results += f"\nWarning: Provided staff count ({exact_staff_count}) is below estimated minimum ({min_staff_estimate:.1f}). Solution may not be feasible.\n"
231
  estimated_staff = exact_staff_count
232
+ num_staff_to_create = exact_staff_count
233
  else:
234
  # Add some buffer for constraints like rest days and shift changes
235
  estimated_staff = max(min_staff_estimate, max_staff_needed + 1)
236
+ num_staff_to_create = int(estimated_staff)
237
 
238
  def optimize_schedule(num_staff, time_limit=600):
239
  try:
 
253
  # Total hours worked by all staff
254
  total_hours = pl.LpVariable("total_hours", lowBound=0)
255
 
256
+ # Individual staff hours variables for balancing
257
+ staff_hours = pl.LpVariable.dicts("staff_hours", range(1, num_staff+1), lowBound=0)
258
 
259
+ # Objective function modification for exact staff count
260
+ if exact_staff_count is not None:
261
+ # When exact staff count is specified, focus on balancing hours between staff
262
+ avg_hours = total_staff_hours / num_staff
263
+ model += pl.lpSum(staff_hours[s] for s in range(1, num_staff+1))
264
+
265
+ # Add penalty for deviation from average
266
+ for s in range(1, num_staff+1):
267
+ model += staff_hours[s] >= avg_hours * 0.8 # Each staff must get at least 80% of average hours
268
+ model += staff_hours[s] <= avg_hours * 1.2 # Each staff must not exceed 120% of average hours
269
+ else:
270
+ # Original objective for minimizing staff and total hours
271
+ model += 10**10 * pl.lpSum(staff_used[s] for s in range(1, num_staff+1)) + total_hours
272
 
273
+ # Link staff_hours to actual hours worked
 
 
 
 
 
 
274
  for s in range(1, num_staff+1):
275
+ model += staff_hours[s] == pl.lpSum(x[(s, d, shift['id'])] * shift['duration']
276
+ for d in range(1, num_days+1)
277
+ for shift in possible_shifts)
 
 
 
 
 
278
 
279
+ # Link total_hours to sum of staff_hours
280
+ model += total_hours == pl.lpSum(staff_hours[s] for s in range(1, num_staff+1))
281
+
282
+ # When exact staff count is provided, ensure all staff are used
283
+ if exact_staff_count is not None:
284
+ for s in range(1, num_staff+1):
285
+ # Ensure each staff works at least some minimum shifts
286
+ min_shifts = max(1, int(num_days / (num_staff * 2))) # At least this many shifts per staff
287
+ model += pl.lpSum(x[(s, d, shift['id'])]
288
+ for d in range(1, num_days+1)
289
+ for shift in possible_shifts) >= min_shifts
290
+
291
+ # Maximum shifts per staff (to prevent overloading)
292
+ max_shifts = int(num_days * 0.8) # At most 80% of days
293
+ model += pl.lpSum(x[(s, d, shift['id'])]
294
+ for d in range(1, num_days+1)
295
+ for shift in possible_shifts) <= max_shifts
296
+ else:
297
+ # Original staff usage constraints
298
+ for s in range(1, num_staff+1):
299
+ model += pl.lpSum(x[(s, d, shift['id'])]
300
+ for d in range(1, num_days+1)
301
+ for shift in possible_shifts) <= num_days * staff_used[s]
302
+ model += pl.lpSum(x[(s, d, shift['id'])]
303
+ for d in range(1, num_days+1)
304
+ for shift in possible_shifts) >= staff_used[s]
305
+
306
+ # Maintain staff ordering only when not using exact staff count
307
+ for s in range(1, num_staff):
308
+ model += staff_used[s] >= staff_used[s+1]
309
 
310
  # Each staff works at most one shift per day
311
  for s in range(1, num_staff+1):
 
332
  # HARD CONSTRAINT: No overtime allowed - strict limit at MAX_HOURS_PER_STAFF
333
  for s in range(1, num_staff+1):
334
  # Calculate total hours worked by this staff
335
+ staff_hours_value = pl.lpSum(x[(s, d, shift['id'])] * shift['duration']
336
  for d in range(1, num_days+1)
337
  for shift in possible_shifts)
338
 
339
  # STRICT constraint: No overtime allowed
340
+ model += staff_hours_value <= MAX_HOURS_PER_STAFF
341
 
342
  # HARD CONSTRAINT: Full coverage required
343
  for d in range(1, num_days+1):
 
415
  if exact_staff_count is not None and exact_staff_count > 0:
416
  # If exact staff count is specified, only try with that count
417
  staff_count = int(exact_staff_count)
418
+ results = original_results # Include the hours adjustment information
419
+ results += f"\nUsing exactly {staff_count} staff as specified"
420
+ if staff_count < min_staff_estimate:
421
+ results += f" (Warning: This is below estimated minimum of {min_staff_estimate:.1f})"
422
+ results += "...\n"
423
 
424
  # Try to solve with exactly this many staff
425
  schedule, objective = optimize_schedule(staff_count)
426
 
427
  if schedule is None:
428
  results += f"Failed to find a feasible solution with exactly {staff_count} staff.\n"
429
+ if staff_count < min_staff_estimate:
430
+ results += f"This is likely because the staff count is below the estimated minimum of {min_staff_estimate:.1f}.\n"
431
+ results += "Try increasing the staff count or adjusting other parameters.\n"
432
  return results, None, None, None, None
433
  else:
434
  # Start from theoretical minimum and work up
435
  min_staff = max(1, int(theoretical_min_staff)) # Start from theoretical minimum
436
  max_staff = int(min_staff_estimate) + 5 # Allow some buffer
437
 
438
+ results = original_results # Include the hours adjustment information
439
+ results += f"Theoretical minimum staff needed: {theoretical_min_staff:.1f}\n"
440
  results += f"Searching for minimum staff count starting from {min_staff}...\n"
441
 
442
  # Try each staff count from min to max
 
469
  total_hours = staff_shifts['duration'].sum()
470
  staff_hours[s] = total_hours
471
 
472
+ # Handle staff hours display based on whether exact count was specified
473
+ if exact_staff_count is not None:
474
+ # When exact count is specified, show all staff including those with 0 hours
475
+ active_staff_hours = staff_hours
476
+ else:
477
+ # Otherwise, only show active staff
478
+ active_staff_hours = {s: hours for s, hours in staff_hours.items() if hours > 0}
479
 
480
  results += "\nStaff Hours:\n"
481
+ total_active_hours = sum(active_staff_hours.values())
482
+ avg_hours = total_active_hours / len(active_staff_hours) if active_staff_hours else 0
483
+
484
  for staff_id, hours in active_staff_hours.items():
485
  utilization = (hours / MAX_HOURS_PER_STAFF) * 100
486
+ deviation_from_avg = ((hours - avg_hours) / avg_hours * 100) if avg_hours > 0 else 0
487
+ results += f"Staff {staff_id}: {hours:.1f} hours ({utilization:.1f}% utilization)"
488
+ if exact_staff_count is not None:
489
+ results += f" [Deviation from avg: {deviation_from_avg:+.1f}%]"
490
+ results += "\n"
491
+
492
  # Add overtime information
493
  if hours > MAX_HOURS_PER_STAFF:
494
  overtime = hours - MAX_HOURS_PER_STAFF
495
  overtime_percent = (overtime / MAX_HOURS_PER_STAFF) * 100
496
  results += f" Overtime: {overtime:.1f} hours ({overtime_percent:.1f}%)\n"
497
 
498
+ if exact_staff_count is not None:
499
+ results += f"\nWorkload Distribution Stats:\n"
500
+ results += f"Average hours per staff: {avg_hours:.1f}\n"
501
+ if active_staff_hours:
502
+ max_deviation = max(abs((hours - avg_hours) / avg_hours * 100) for hours in active_staff_hours.values()) if avg_hours > 0 else 0
503
+ results += f"Maximum deviation from average: {max_deviation:.1f}%\n"
504
+
505
  # Use active_staff_hours for average utilization calculation
506
  active_staff_count = len(active_staff_hours)
507
  avg_utilization = sum(active_staff_hours.values()) / (active_staff_count * MAX_HOURS_PER_STAFF) * 100