Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move consistency checkers to a class (2) #192

Merged
merged 1 commit into from
May 31, 2021
Merged
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
265 changes: 265 additions & 0 deletions models/concerns/validate_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,265 @@
# Copyright © Mapotempo, 2021
#
# This file is part of Mapotempo.
#
# Mapotempo is free software. You can redistribute it and/or
# modify since you respect the terms of the GNU Affero General
# Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
#
# Mapotempo is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the Licenses for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with Mapotempo. If not, see:
# <http://www.gnu.org/licenses/agpl.html>
#
require 'active_support/concern'

# Expands provided data
module ValidateData
extend ActiveSupport::Concern

def check_consistency(hash)
hash[:services] ||= []
hash[:shipments] ||= []
@hash = hash

ensure_uniq_ids
ensure_no_conflicting_skills

configuration = @hash[:configuration]
schedule = configuration[:schedule] if configuration
periodic_heuristic = configuration &&
configuration[:preprocessing] &&
configuration[:preprocessing][:first_solution_strategy].to_a.include?('periodic')
check_matrices
check_vehicles
check_relations(periodic_heuristic)
# TODO : this should be replaced by schedule when max_split does not use visits_number > 1 without schedule anymore
# indeed, no configuration implies no schedule and there should be no visits_number > 1 in this case
# check_services_and_shipments(schedule)
check_services_and_shipments(configuration, schedule)
check_shipments_specificities

check_routes(periodic_heuristic)
check_configuration(configuration, periodic_heuristic) if configuration
end

def ensure_uniq_ids
# TODO: Active Hash should be checking this
[:matrices, :units, :points, :rests, :zones, :timewindows,
:vehicles, :services, :shipments, :subtours].each{ |key|
next if @hash[key]&.collect{ |v| v[:id] }&.uniq!.nil?

raise OptimizerWrapper::DiscordantProblemError.new("#{key} IDs should be unique")
}
end

def ensure_no_conflicting_skills
all_skills = (@hash[:vehicles].to_a + @hash[:services].to_a + @hash[:shipments].to_a).flat_map{ |mission|
mission[:skills]
}.compact.uniq

return unless ['vehicle_partition_', 'work_day_partition_'].any?{ |str|
all_skills.any?{ |skill| skill.to_s.start_with?(str) }
}

raise OptimizerWrapper::UnsupportedProblemError.new(
"There are vehicles or services with 'vehicle_partition_*', 'work_day_partition_*' skills. " \
'These skill patterns are reserved for internal use and they would lead to unexpected behaviour.'
)
end

def check_matrices
# matrix_index consistency
if @hash[:matrices].nil? || @hash[:matrices].empty?
if @hash[:points]&.any?{ |p| p[:matrix_index] }
raise OptimizerWrapper::DiscordantProblemError.new(
'There is a point with point[:matrix_index] defined but there is no matrix'
)
end
else
max_matrix_index = @hash[:points].max{ |p| p[:matrix_index] || -1 }[:matrix_index] || -1
matrix_not_big_enough = @hash[:matrices].any?{ |matrix_group|
Models::Matrix.field_names.any?{ |dimension|
matrix_group[dimension] &&
(matrix_group[dimension].size <= max_matrix_index ||
matrix_group[dimension].any?{ |col| col.size <= max_matrix_index })
}
}
if matrix_not_big_enough
raise OptimizerWrapper::DiscordantProblemError.new(
'All matrices should have at least maximum(point[:matrix_index]) number of rows and columns'
)
end
end
end

def check_vehicles
@hash[:vehicles]&.each{ |v|
# vehicle time cost consistency
if v[:cost_waiting_time_multiplier].to_f > (v[:cost_time_multiplier] || 1)
raise OptimizerWrapper::DiscordantProblemError.new(
'Cost_waiting_time_multiplier cannot be greater than cost_time_multiplier'
)
end

# matrix_id consistency
if v[:matrix_id] && (@hash[:matrices].nil? || @hash[:matrices].none?{ |m| m[:id] == v[:matrix_id] })
raise OptimizerWrapper::DiscordantProblemError.new('There is no matrix with id vehicle[:matrix_id]')
end
}
end

def check_services_and_shipments(configuration, schedule)
(@hash[:services].to_a + @hash[:shipments].to_a).each{ |mission|
if (mission[:minimum_lapse] || 0) > (mission[:maximum_lapse] || 2**32)
raise OptimizerWrapper::DiscordantProblemError.new('Minimum lapse can not be bigger than maximum lapse')
end

# TODO : this should be replaced next line when max_split does not use visits_number > 1 without schedule anymore
# next if schedule && schedule[:range_indices] || mission[:visits_number].to_i <= 1
next if configuration.nil? || schedule && schedule[:range_indices] || mission[:visits_number].to_i <= 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@braktar @senhalil I changed here : for now if we have no configuration (coming from max_split in general) then we do not do this check.

PR is ready to make this check more general after we addit max split : #193


raise OptimizerWrapper::DiscordantProblemError.new('There can not be more than one visit without schedule')
}
end

def check_shipments_specificities
forbidden_position_pairs = [
[:always_middle, :always_first],
[:always_last, :always_middle],
[:always_last, :always_first]
]
@hash[:shipments]&.each{ |shipment|
return unless forbidden_position_pairs.include?([shipment[:pickup][:position], shipment[:delivery][:position]])

raise OptimizerWrapper::DiscordantProblemError.new('Unconsistent positions in shipments.')
}
end

def check_relations(periodic_heuristic)
return unless @hash[:relations].to_a.any?

# shipment relation consistency
if @hash[:relations]&.any?{ |r| r[:type] == :shipment }
shipment_relations = @hash[:relations].select{ |r| r[:type] == :shipment }

shipments_not_having_exactly_two_linked_ids = shipment_relations.reject{ |r| r[:linked_ids].uniq.size == 2 }
unless shipments_not_having_exactly_two_linked_ids.empty?
raise OptimizerWrapper::DiscordantProblemError.new(
'Shipment relations need to have two services -- a pickup and a delivery. ' \
'Relations of following services does not have exactly two linked_ids: ' \
"#{shipments_not_having_exactly_two_linked_ids.flat_map{ |r| r[:linked_ids] }.uniq.sort.join(', ')}"
)
end

pickups = shipment_relations.map{ |r| r[:linked_ids].first }
deliveries = shipment_relations.map{ |r| r[:linked_ids].last }
services_that_are_both_pickup_and_delivery = pickups & deliveries
unless services_that_are_both_pickup_and_delivery.empty?
raise OptimizerWrapper::UnsupportedProblemError.new(
'A service cannot be both a delivery and a pickup in different relations. '\
'Following services appear in multiple shipment relations both as pickup and delivery: ',
[services_that_are_both_pickup_and_delivery]
)
end
end

incompatible_relation_types = @hash[:relations].collect{ |r| r[:type] }.uniq - %i[force_first never_first force_end]
return unless periodic_heuristic && incompatible_relation_types.any?

raise OptimizerWrapper::UnsupportedProblemError.new(
"#{incompatible_relation_types} relations not available with specified first_solution_strategy"
)
end

def check_routes(periodic_heuristic)
@hash[:routes]&.each{ |route|
route[:mission_ids].each{ |id|
corresponding = @hash[:services]&.find{ |s| s[:id] == id } || @hash[:shipments]&.find{ |s| s[:id] == id }

if corresponding.nil?
raise OptimizerWrapper::DiscordantProblemError.new('Each mission_ids should refer to an existant id')
end

next unless corresponding[:activities] && periodic_heuristic

raise OptimizerWrapper::UnsupportedProblemError.new('Services in routes should have only one activity')
}
}
end

def check_configuration(configuration, periodic_heuristic)
check_clustering_parameters(configuration) if configuration[:preprocessing]
check_schedule_consistency(configuration[:schedule]) if configuration[:schedule]
check_periodic_consistency(configuration) if periodic_heuristic
check_geometry_parameters(configuration) if configuration[:restitution]
end

def check_clustering_parameters(configuration)
return unless configuration[:preprocessing][:partitions]&.any?{ |partition|
partition[:entity].to_sym == :work_day
}

if @hash[:services].any?{ |s|
min_lapse = s[:minimum_lapse]&.floor || 1
max_lapse = s[:maximum_lapse]&.ceil || @hash[:configuration][:schedule][:range_indices][:end]

s[:visits_number].to_i > 1 && (
@hash[:configuration][:schedule][:range_indices][:end] <= 6 ||
(min_lapse..max_lapse).none?{ |intermediate_lapse| (intermediate_lapse % 7).zero? }
)
}

raise OptimizerWrapper::DiscordantProblemError.new(
'Work day partition implies that lapses of all services can be a multiple of 7.
There are services whose minimum and maximum lapse do not permit such lapse'
)
end
end

def check_schedule_consistency(schedule)
if schedule[:range_indices][:start] > 6
raise OptimizerWrapper::DiscordantProblemError.new('Api does not support schedule start index bigger than 6')
# TODO : allow start bigger than 6 and make code consistent with this
end

return unless schedule[:range_indices][:start] > schedule[:range_indices][:end]

raise OptimizerWrapper::DiscordantProblemError.new('Schedule start index should be less than or equal to end')
end

def check_periodic_consistency(configuration)
if @hash[:relations].to_a.any?{ |relation| relation[:type] == 'vehicle_group_duration_on_months' } &&
(!configuration[:schedule] || configuration[:schedule][:range_indice])
raise OptimizerWrapper::DiscordantProblemError.new(
'Vehicle group duration on weeks or months is not available without range_date'
)
end

unless @hash[:shipments].to_a.empty?
raise OptimizerWrapper::UnsupportedProblemError.new('Shipments are not available with periodic heuristic')
end

unless @hash[:vehicles].all?{ |vehicle| vehicle[:rests].to_a.empty? }
raise OptimizerWrapper::UnsupportedProblemError.new('Rests are not available with periodic heuristic')
end

if configuration[:resolution] && configuration[:resolution][:same_point_day] &&
@hash[:services].any?{ |service| service[:activities].to_a.size.positive? }
raise OptimizerWrapper.UnsupportedProblemError.new(
'Same_point_day is not supported if a set has one service with several activities'
)
end
end

def check_geometry_parameters(configuration)
return unless configuration[:restitution][:geometry].any? &&
!@hash[:points].all?{ |pt| pt[:location] }

raise OptimizerWrapper::DiscordantProblemError.new('Geometry is not available if locations are not defined')
end
end
Loading