Class: ORTools::BasicScheduler

Inherits:
Object
  • Object
show all
Defined in:
lib/or_tools/basic_scheduler.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(people:, shifts:) ⇒ BasicScheduler

for time blocks (shifts and availability) could also use time range (starts_at..ends_at) or starts_at + duration keep current format for now for flexibility

Raises:



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
# File 'lib/or_tools/basic_scheduler.rb', line 8

def initialize(people:, shifts:)
  @shifts = shifts

  model = ORTools::CpModel.new

  # create variables
  # a person must be available for the entire shift to be considered for it
  vars = []
  shifts.each_with_index do |shift, i|
    people.each_with_index do |person, j|
      if person[:availability].any? { |a| a[:starts_at] <= shift[:starts_at] && a[:ends_at] >= shift[:ends_at] }
        vars << {shift: i, person: j, var: model.new_bool_var("{shift: #{i}, person: #{j}}")}
      end
    end
  end

  vars_by_shift = vars.group_by { |v| v[:shift] }
  vars_by_person = vars.group_by { |v| v[:person] }

  # one person per shift
  vars_by_shift.each do |j, vs|
    model.add(model.sum(vs.map { |v| v[:var] }) <= 1)
  end

  # one shift per day per person
  # in future, may also want to add option to ensure assigned shifts are N hours apart
  vars_by_person.each do |j, vs|
    vs.group_by { |v| shift_dates[v[:shift]] }.each do |_, vs2|
      model.add(model.sum(vs2.map { |v| v[:var] }) <= 1)
    end
  end

  # max hours per person
  # use seconds since model needs integers
  vars_by_person.each do |j, vs|
    max_hours = people[j][:max_hours]
    if max_hours
      model.add(model.sum(vs.map { |v| v[:var] * shift_duration[v[:shift]] }) <= max_hours * 3600)
    end
  end

  # maximize hours assigned
  # could also include distance from max hours
  model.maximize(model.sum(vars.map { |v| v[:var] * shift_duration[v[:shift]] }))

  # solve
  solver = ORTools::CpSolver.new
  status = solver.solve(model)
  raise Error, "No solution found" unless [:feasible, :optimal].include?(status)

  # read solution
  @assignments = []
  vars.each do |v|
    if solver.value(v[:var])
      @assignments << {
        person: v[:person],
        shift: v[:shift]
      }
    end
  end
  # can calculate manually if objective changes
  @assigned_hours = solver.objective_value / 3600.0
end

Instance Attribute Details

#assigned_hoursObject (readonly)

Returns the value of attribute assigned_hours.



3
4
5
# File 'lib/or_tools/basic_scheduler.rb', line 3

def assigned_hours
  @assigned_hours
end

#assignmentsObject (readonly)

Returns the value of attribute assignments.



3
4
5
# File 'lib/or_tools/basic_scheduler.rb', line 3

def assignments
  @assignments
end

Instance Method Details

#total_hoursObject



72
73
74
# File 'lib/or_tools/basic_scheduler.rb', line 72

def total_hours
  @total_hours ||= shift_duration.sum / 3600.0
end