Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/NurseScheduling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export Schedule,
n_split_nbhd,
perform_random_jumps!,
get_shifts_distance,
Shifts
Shifts,
cmp_workers_worktime

using JSON
using SuperEnum
Expand All @@ -27,9 +28,11 @@ include("schedule.jl")
include("validation.jl")
include("scoring.jl")
include("neighborhood.jl")
include("comparator.jl")

using .ScheduleValidation
using .ScheduleScoring
using .ScheduleComparing
using .NeighborhoodGen

end # NurseSchedules
117 changes: 117 additions & 0 deletions src/comparator.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
module ScheduleComparing

export cmp_workers_worktime

using ..NurseSchedules:
Schedule,
get_shift_options,
get_month_info,
get_shift_length,
get_workers_info,
WEEK_DAYS_NO,
NUM_WORKING_DAYS,
MAX_OVERTIME,
MAX_UNDERTIME,
WORKTIME_DAILY,
W_ID,
SUNDAY_NO,
ErrorCode,
ScheduleShifts,
ScoringResult,
is_working,
get_hours_reduction

"""
Evaluates current workers worktime restrictions based on a schedules from the beginning of the month and at the end
Args:
old_schedule::ScheduleShifts - array containing shcedule from the beginning of the month
new_schedule::ScheduleShifts - array containing shcedule from the end of the month
schedule::Schedule - Schedule metadata for a given month
Returns:
ScoringResult - Evaluated score with errors description

"""
function cmp_workers_worktime(
old_schedule::ScheduleShifts,
new_schedule::ScheduleShifts,
schedule::Schedule
)::ScoringResult
shift_info = get_shift_options(schedule)
month_info = get_month_info(schedule)
workers_info = get_workers_info(schedule)
workers, old_shifts = old_schedule
_, new_shifts = new_schedule

penalty = 0
errors = Vector{Dict{String, Any}}()

num_weeks = ceil(Int, size(old_shifts, 2) / WEEK_DAYS_NO)
num_days = num_weeks * NUM_WORKING_DAYS

max_overtime = num_weeks * MAX_OVERTIME
max_undertime = num_weeks * MAX_UNDERTIME

# Current assumption
# Reduce norm only when working shift is replaced by non-working

holidays_no = length(filter(
day_no -> day_no % WEEK_DAYS_NO != SUNDAY_NO,
get(month_info, "holidays", Int[])
))

for worker_no in axes(old_shifts, 1)
# Catch all hours reduction
negative_hours = sum(
map(
pos -> get_hours_reduction(shift_info[new_shifts[pos]]),
filter(
pos ->
is_working(shift_info[old_shifts[worker_no, pos]]) &&
!is_working(shift_info[new_shifts[worker_no, pos]]) &&
new_shifts[worker_no, pos] != W_ID,
keys(old_shifts[worker_no, :])
)))

hours_per_day::Float32 = workers_info["time"][workers[worker_no]] * WORKTIME_DAILY
req_worktime = ((num_days - holidays_no) * hours_per_day) - negative_hours
act_worktime = sum(map(s -> get_shift_length(shift_info[s]), new_shifts[worker_no, :]))

worktime = act_worktime - req_worktime

if worktime > max_overtime
pen_diff = worktime - max_overtime
@debug "The worker '$(worker_no)' has too much overtime: '$(pen_diff)'"
push!(
errors,
Dict(
"code" => string(ErrorCode.WORKER_OVERTIME_HOURS),
"hours" => pen_diff,
"worker" => worker_no,
))
penalty += pen_diff
elseif worktime < -max_undertime
pen_diff = -(worktime+max_undertime)
@debug "The worker '$(worker_no)' has too much undertime: '$(pen_diff)'"
push!(
errors,
Dict(
"code" => string(ErrorCode.WORKER_UNDERTIME_HOURS),
"hours" => pen_diff,
"worker" => worker_no,
))
penalty += pen_diff
end
end

if penalty > 0
@debug "Total penalty from undertime and overtime: $(penalty)"
@debug "Max overtime hours: '$(max_overtime)'"
@debug "Max undertime hours: '$(max_undertime)"
end
ScoringResult((penalty, errors))
end

end # Module
2 changes: 2 additions & 0 deletions src/constants.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# Scoring
ScoringResult = @NamedTuple{penalty::Int, errors::Vector{Dict{String,Any}}}
ScoringResultOrPenalty = Union{ScoringResult,Int}

# Schedule related
Workers = Vector{String}
Shifts = Matrix{UInt8}
Expand Down Expand Up @@ -52,6 +53,7 @@ const PERIOD_BEGIN = 7

# weekly worktime
const WORKTIME_BASE = 40
const DEFAULT_NORM_SUBSTRACTION = 8

const DAY_HOURS_NO = 24
const WEEK_DAYS_NO = 7
Expand Down
6 changes: 6 additions & 0 deletions src/shifts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

ShiftType = Dict{String, Any}

"""
Access operators
"""
@inline is_working(shift::ShiftType)::Bool = shift["is_working_shift"]
@inline get_hours_reduction(shift::ShiftType)::Int = get(shift, "norm_substraction", DEFAULT_NORM_SUBSTRACTION)

"""
Computes wheter an hour is inside the shift
Args:
Expand Down
41 changes: 41 additions & 0 deletions test/comparator.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.

include("../src/scoring.jl")

using .ScheduleScoring:
ck_workers_worktime

using .NurseSchedules:
cmp_workers_worktime

function check(schedule, old, new; info=false)
l = cmp_workers_worktime(old, new, schedule)
r = ck_workers_worktime(new, schedule)

if l.penalty != r.penalty || keys(l.errors) != keys(r.errors)
if info
@info "Different penalties: " l.penalty, r.penalty
@info "CMP \\ CK " setdiff(keys(l.errors), keys(r.errors))
@info "CK \\ CMP " setdiff(keys(r.errors), keys(l.errors))
end
false
else
if info
@info "Received score: " l.penalty
end
true
end
end

@testset "Basic cmp_workers_worktime case" begin
base = Schedule("schedules/schedule_2016_august_medium.json")
working_type = base.shift_map["PN"]

base_s = get_shifts(base)
working_s = (base_s[1], fill(working_type, size(base_s[2])))

@test check(base, base_s, base_s ) == false
@test check(base, working_s, base_s, info=true) == true
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ using .NurseSchedules:
include("engine_tests.jl")
include("shifts.jl")
include("schedule.jl")
include("comparator.jl")