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

Simplify multi-pickup or multi-delivery shipments #261

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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Implementation of `vehicle_trips` relation: the routes can be successive or with a minimum duration `lapse` in between [#123](https://github.com/Mapotempo/optimizer-api/pull/123)
- CSV headers adapts to the language provided through HTTP_ACCEPT_LANGUAGE header to facilitate import in Mapotempo-Web [#196](https://github.com/Mapotempo/optimizer-api/pull/196)
- Return route day/date and visits' index in result [#196](https://github.com/Mapotempo/optimizer-api/pull/196)
- Treat complex shipments (multi-pickup-single-delivery and single-pickup-multi-delivery) as multiple simple shipments internally to increase performance [#261](https://github.com/Mapotempo/optimizer-api/pull/261)

### Changed

Expand Down
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ gem 'grape-swagger-entity'
gem 'grape_logging'

gem 'actionpack'
gem 'active_hash', github: 'Mapotempo/active_hash', branch: 'mapo'
gem 'active_hash', github: 'mapotempo/active_hash', branch: 'dev' # waiting for the following PRs to get merged and "released!" https://github.com/zilkey/active_hash/pull/231 and https://github.com/zilkey/active_hash/pull/233
gem 'activemodel'

gem 'charlock_holmes'
Expand Down
16 changes: 8 additions & 8 deletions Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
GIT
remote: https://github.com/Mapotempo/active_hash.git
revision: e2b18fedb32aec0cc03f0c747dbc682cbb8fc488
branch: mapo
specs:
active_hash (3.1.0)
activesupport (>= 5.0.0)

GIT
remote: https://github.com/Mapotempo/balanced_vrp_clustering.git
revision: 4b2b48732604731e1759788eab85f7ac6268835d
Expand All @@ -16,6 +8,14 @@ GIT
color-generator
geojson2image

GIT
remote: https://github.com/mapotempo/active_hash.git
revision: d04b246227675806f040be09e7bf0dfaa8ffc590
branch: dev
specs:
active_hash (3.1.0)
activesupport (>= 5.0.0)

GIT
remote: https://github.com/senhalil/rack.git
revision: a23188bb32972dde155977655f57dea035eee6ea
Expand Down
2 changes: 1 addition & 1 deletion lib/filters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def self.merge_timewindows(vrp)
# latest_day_index = day_indices.include?(nil) ? nil : day_indices.max
earliest_start = starts.include?(nil) ? nil : starts.min
latest_end = ends.include?(nil) ? nil : ends.max
new_timewindows << Models::Timewindow.new(start: earliest_start, end: latest_end, day_index: earliest_day_index)
new_timewindows << Models::Timewindow.create(start: earliest_start, end: latest_end, day_index: earliest_day_index)
}
service.activity.timewindows = new_timewindows
}
Expand Down
76 changes: 76 additions & 0 deletions lib/helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,82 @@ def self.euclidean_distance(loc_a, loc_b)
111321 * Math.sqrt(delta_lat**2 + delta_lon**2) # 111321 is the length of a degree (of lon and lat) in meters
end

def self.deep_copy(original, override: {}, shallow_copy: [])
# TODO: after testing move the logic to base.rb as a clone function (initialize_copy)

# To override keys (and sub-keys) with values instead of using the originals
# (i.e., original[:key1], original[:other][:key2] etc) do
# override: { key1: value1, key2: value2 }
# To prevent deep copy and use the original objects and sub-objects
# (i.e., original[:key1], original[other][:key2]) do
# shallow_copy: [:key1, :key2 ]

# Assigning nil to a key in the override hash, skips the key and key_id of the objects

# WARNING: custom fields will be missing from the objects! because if a key/method doesn't exist
# in the Models::Class definition, it cannot be duplicated

case original
when Array # has_many
original.collect{ |val| deep_copy(val, override: override, shallow_copy: shallow_copy) }
when Models::Base # belongs_to
raise 'Keys cannot be both overridden and shallow copied' if (override.keys & shallow_copy).any?

# if an option doesn't exist for the current object level, pass it to lower levels
unused_override = override.select{ |key, _value|
[
key, "#{key}_id", "#{key[0..-2]}_ids", "#{key[0..-4]}y_ids", key[0..-4], "#{key[0..-6]}ies", "#{key[0..-5]}s"
].none?{ |k| original.class.method_defined?(k.to_sym) }
}

unused_shallow_copy = shallow_copy.select{ |key|
[
key, "#{key}_id", "#{key[0..-2]}_ids", "#{key[0..-4]}y_ids", key[0..-4], "#{key[0..-6]}ies", "#{key[0..-5]}s"
].none?{ |k| original.class.method_defined?(k.to_sym) }
}

# if a non-"id" version of the key exists, then prefer the hash of the non-id method (i.e., skip x_id(s))
# so that the objects are generated from scratch instead of re-used.
# Unless the key or key_id is marked as shallow_copy (then use the object) or the key or key_id is given in override.
keys = original.attributes.keys.flat_map{ |key|
[
key[0..-4].to_sym, "#{key[0..-6]}ies".to_sym, "#{key[0..-5]}s".to_sym, key
].find{ |k| original.class.method_defined?(k) }
}.uniq - [:id] # cannot duplicate the object with the same id

# To reuse the same sub-members, the key needs to be given in the shallow_copy
# (which forces the duplication to use the original key object)
keys.map!{ |key|
if override.key?(key)
next unless override[key].nil?

[
"#{key}_id".to_sym, "#{key[0..-2]}_ids".to_sym, "#{key[0..-4]}y_ids".to_sym
].find{ |k| original.class.method_defined?(k) && (!override.key?(k) || override[k]) }
elsif ["#{key}_id", "#{key[0..-2]}_ids", "#{key[0..-4]}y_ids"].any?{ |k| override[k.to_sym] }
next
else
key
end
}.compact!

# if a key is supplied in the override manually as nil, this means removing the key
# pass unused_override and unused_shallow_copy to the lower levels only
keys |= override.keys.select{ |k| override[k] && original.class.method_defined?(k) }
keys |= shallow_copy.select{ |k| original.class.method_defined?(k) }

# prefer the option if supplied
original.class.create(keys.each_with_object({}) { |key, data|
data[key] = override[key] ||
(shallow_copy.include?(key) ? original.send(key) : deep_copy(original.send(key),
override: unused_override,
shallow_copy: unused_shallow_copy))
})
else
original.dup
end
end

def self.merge_results(results, merge_unassigned = true)
results.flatten!
results.compact!
Expand Down
22 changes: 10 additions & 12 deletions lib/heuristics/dichotomious_approach.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,15 @@ def self.dichotomious_heuristic(service_vrp, job = nil, &block)
if sub_service_vrp[:vrp].resolution_minimum_duration
sub_service_vrp[:vrp].resolution_minimum_duration *= sub_service_vrp[:vrp].services.size / service_vrp[:vrp].services.size.to_f * 2
end
matrix_indices = sub_service_vrp[:vrp].points.map{ |point|
service_vrp[:vrp].points.find{ |r_point| point.id == r_point.id }.matrix_index
}
SplitClustering.update_matrix_index(sub_service_vrp[:vrp])
SplitClustering.update_matrix(service_vrp[:vrp].matrices, sub_service_vrp[:vrp], matrix_indices)
result = OptimizerWrapper.define_process(sub_service_vrp, job, &block)
if index.zero? && result
transfer_unused_vehicles(result, sub_service_vrps)
matrix_indices = sub_service_vrps[1][:vrp].points.map{ |point|
service_vrp[:vrp].points.find{ |r_point| point.id == r_point.id }.matrix_index
}
SplitClustering.update_matrix_index(sub_service_vrps[1][:vrp])
SplitClustering.update_matrix(service_vrp[:vrp].matrices, sub_service_vrps[1][:vrp], matrix_indices)
end

transfer_unused_vehicles(result, sub_service_vrps) if index.zero? && result

result
}
service_vrp[:vrp].resolution_split_number = sub_service_vrps[1][:vrp].resolution_split_number
Expand Down Expand Up @@ -211,10 +211,8 @@ def self.build_initial_routes(results)
mission_ids = route[:activities].map{ |activity| activity[:service_id] }.compact
next if mission_ids.empty?

Models::Route.new(
vehicle: {
id: route[:vehicle_id]
},
Models::Route.create(
vehicle_id: route[:vehicle_id],
mission_ids: mission_ids
)
}
Expand Down
4 changes: 2 additions & 2 deletions lib/heuristics/periodic_heuristic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1167,7 +1167,7 @@ def prepare_output_and_collect_routes(vrp)
computed_activities, start_time, end_time = get_activities(day, route_data, vrp, vrp_vehicle)

routes << {
vehicle: { id: vrp_vehicle.id },
vehicle_id: vrp_vehicle.id,
mission_ids: computed_activities.collect{ |stop| stop[:service_id] }.compact
}

Expand All @@ -1184,7 +1184,7 @@ def prepare_output_and_collect_routes(vrp)
unassigned = collect_unassigned
vrp[:preprocessing_heuristic_result] = {
cost: @cost,
cost_details: Models::CostDetails.new({}), # TODO: fulfill with solution costs
cost_details: Models::CostDetails.create({}), # TODO: fulfill with solution costs
solvers: ['heuristic'],
iterations: 0,
routes: solution,
Expand Down
2 changes: 1 addition & 1 deletion lib/interpreters/compute_several_solutions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ def self.find_best_heuristic(service_vrp)
}.compact
next if mission_ids.empty?

Models::Route.new(vehicle: vrp.vehicles.find{ |v| v[:id] == route[:vehicle_id] }, mission_ids: mission_ids)
Models::Route.create(vehicle: vrp.vehicles.find{ |v| v[:id] == route[:vehicle_id] }, mission_ids: mission_ids)
}.compact
end

Expand Down
6 changes: 3 additions & 3 deletions lib/interpreters/multi_modal.rb
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ def generate_subproblems(patterns)
sub_vrp.vehicles += (1..duplicate_vehicles).collect{ |index|
if vehicle_skills && !vehicle_skills.empty?
vehicle_skills.collect{ |alternative|
Models::Vehicle.new(
Models::Vehicle.create(
id: "subtour_#{alternative.join('-')}_#{transmodal_id}_#{index}",
router_mode: sub_tour.router_mode,
router_dimension: sub_tour.router_dimension,
Expand All @@ -158,7 +158,7 @@ def generate_subproblems(patterns)
)
}
else
Models::Vehicle.new(
Models::Vehicle.create(
id: "subtour_#{transmodal_id}_#{index}",
router_mode: sub_tour.router_mode,
router_dimension: sub_tour.router_dimension,
Expand Down Expand Up @@ -227,7 +227,7 @@ def override_original_vrp(subresults)
subresult[:routes].collect{ |route|
next unless route[:activities].size > 2

service = Models::Service.new(
service = Models::Service.create(
id: route[:vehicle_id],
activity: {
point: @original_vrp.points.find{ |point| point.id == route[:activities].first[:point_id] },
Expand Down
Loading