Skip to content
This repository has been archived by the owner on Mar 19, 2024. It is now read-only.

Commit

Permalink
Support for restricted number of vehicles (#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
leonlan authored Sep 19, 2023
1 parent 2b0a355 commit 2330204
Show file tree
Hide file tree
Showing 15 changed files with 183 additions and 14 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ jobs:
- name: Run pre-commit
run: poetry run pre-commit run --all-files
- name: Test run dynamic instance
run: poetry run benchmark instances/ortec/ORTEC-VRPTW-ASYM-01829532-d1-n324-k22.txt --epoch_tlim 1 --strategy_tlim_factor 0.8
run: poetry run benchmark instances/ortec/ORTEC-VRPTW-ASYM-01829532-d1-n324-k22.txt --epoch_tlim 10 --strategy_tlim_factor 0.8 --agent_config_loc configs/icd-test.toml
- name: Test run hindsight instance
run: poetry run benchmark instances/ortec/ORTEC-VRPTW-ASYM-01829532-d1-n324-k22.txt --epoch_tlim 1 --strategy_tlim_factor 0.8 --hindsight
run: poetry run benchmark instances/ortec/ORTEC-VRPTW-ASYM-01829532-d1-n324-k22.txt --epoch_tlim 10 --strategy_tlim_factor 0.8 --hindsight --agent_config_loc configs/icd-test.toml
49 changes: 48 additions & 1 deletion Environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from dataclasses import dataclass
from time import perf_counter
from typing import Optional
from warnings import warn

import numpy as np
Expand Down Expand Up @@ -58,6 +59,9 @@ class StaticInfo:
the routes is `t * epoch_duration + dispatch_margin`.
num_requests_per_epoch
The expected number of revealed requests per epoch.
num_vehicles_per_epoch
The available number of primary vehicles per epoch. If None, then
there is no limit on the number of primary vehicles.
"""

static_instance: VrpInstance
Expand All @@ -67,6 +71,7 @@ class StaticInfo:
epoch_duration: int
dispatch_margin: int
num_requests_per_epoch: list[int]
num_vehicles_per_epoch: Optional[list[int]]


@dataclass(frozen=True)
Expand All @@ -77,7 +82,7 @@ class State:

current_epoch: int
current_time: int
departure_time: float
departure_time: int
epoch_instance: VrpInstance


Expand All @@ -97,6 +102,9 @@ class Environment:
The sampling method to use.
num_requests_per_epoch
The expected number of revealed requests per epoch.
num_vehicles_per_epoch
The available number of primary vehicles per epoch. If None, then
there is no limit on the number of primary vehicles.
start_epoch
The start epoch.
end_epoch
Expand All @@ -118,6 +126,7 @@ def __init__(
start_epoch: int,
end_epoch: int,
num_requests_per_epoch: list[int],
num_vehicles_per_epoch: Optional[list[int]],
epoch_duration: int,
dispatch_margin: int,
):
Expand All @@ -128,6 +137,7 @@ def __init__(
self.start_epoch = start_epoch
self.end_epoch = end_epoch
self.num_requests_per_epoch = num_requests_per_epoch
self.num_vehicles_per_epoch = num_vehicles_per_epoch
self.epoch_duration = epoch_duration
self.dispatch_margin = dispatch_margin

Expand All @@ -139,6 +149,7 @@ def __init__(
epoch_duration=epoch_duration,
dispatch_margin=dispatch_margin,
num_requests_per_epoch=num_requests_per_epoch,
num_vehicles_per_epoch=num_vehicles_per_epoch,
)

self.is_done = True # Requires reset to be called first
Expand All @@ -153,6 +164,7 @@ def euro_neurips(
num_requests: int = 100,
epoch_duration: int = 3600,
dispatch_margin: int = 3600,
num_vehicles_per_epoch: Optional[list[int]] = None,
):
"""
Creates a DDWP environment identical to the one used in [1].
Expand All @@ -175,6 +187,9 @@ def euro_neurips(
The preparation time needed to dispatch a set of routes. That is, when
a set of routes are to be dispatched at epoch t, then the start time of
the routes is `t * epoch_duration + dispatch_margin`.
num_vehicles_per_epoch
The available number of primary vehicles per epoch. If None, then
there is no limit on the number of primary vehicles.
References
----------
Expand All @@ -200,6 +215,7 @@ def euro_neurips(
start_epoch=start_epoch,
end_epoch=end_epoch,
num_requests_per_epoch=num_requests_per_epoch,
num_vehicles_per_epoch=num_vehicles_per_epoch,
epoch_duration=epoch_duration,
dispatch_margin=dispatch_margin,
)
Expand All @@ -211,6 +227,7 @@ def paper(
instance: VrpInstance,
epoch_tlim: float,
sampling_method: SamplingMethod,
num_vehicles_per_epoch: Optional[list[int]] = None,
num_requests_per_epoch: list[int] = [75] * 8,
num_epochs: int = 8,
):
Expand All @@ -228,6 +245,9 @@ def paper(
The epoch time limit.
sampling_method
The sampling method to use.
num_vehicles_per_epoch
The available number of primary vehicles per epoch. If None, then
there is no limit on the number of primary vehicles.
num_requests_per_epoch
The expected number of revealed requests per epoch.
num_epochs
Expand Down Expand Up @@ -270,6 +290,7 @@ def paper(
start_epoch=start_epoch,
end_epoch=end_epoch,
num_requests_per_epoch=num_requests_per_epoch,
num_vehicles_per_epoch=num_vehicles_per_epoch,
epoch_duration=epoch_duration,
dispatch_margin=0,
)
Expand All @@ -287,6 +308,7 @@ def reset(self) -> tuple[State, StaticInfo]:

self.current_epoch = self.start_epoch
self.current_time = self.current_epoch * self.epoch_duration
self.num_vehicles_used: list[int] = []

self.is_done = False
self.final_solutions: dict[int, list] = {}
Expand Down Expand Up @@ -432,6 +454,9 @@ def step(self, action: Action) -> tuple[State, float, bool]:
for route in action:
self.req_is_dispatched[route] = True

# Register how many primary vehicles were used.
self.num_vehicles_used.append(len(action))

self.final_solutions[self.current_epoch] = action
self.final_costs[self.current_epoch] = cost

Expand Down Expand Up @@ -471,6 +496,13 @@ def _next_observation(self) -> State:
must_dispatch_epoch = self.req_must_dispatch_epoch[current_reqs]
must_dispatch = must_dispatch_epoch == self.current_epoch

# Determine the number of primary vehicles available.
if self.num_vehicles_per_epoch is None:
num_available_vehicles = customer_idx.size
else:
total = sum(self.num_vehicles_per_epoch[: self.current_epoch + 1])
num_available_vehicles = total - sum(self.num_vehicles_used)

self.ep_inst = VrpInstance(
is_depot=self.instance.is_depot[customer_idx],
customer_idx=customer_idx,
Expand All @@ -485,6 +517,8 @@ def _next_observation(self) -> State:
],
must_dispatch=must_dispatch,
release_times=self.req_release_time[current_reqs],
num_vehicles=num_available_vehicles,
shift_tw_early=num_available_vehicles * [departure_time],
)

return State(
Expand All @@ -503,6 +537,17 @@ def get_hindsight_problem(self) -> VrpInstance:
"""
customer_idx = self.req_customer_idx

if self.num_vehicles_per_epoch is None:
num_vehicles = customer_idx.size
shift_tw_early = num_vehicles * [0]
else:
shift_tw_early = []
for epoch, num_vehicles in enumerate(self.num_vehicles_per_epoch):
departure = epoch * self.epoch_duration + self.dispatch_margin
shift_tw_early.extend(num_vehicles * [departure])

num_vehicles = sum(self.num_vehicles_per_epoch)

return VrpInstance(
is_depot=self.instance.is_depot[customer_idx],
coords=self.instance.coords[customer_idx],
Expand All @@ -516,6 +561,8 @@ def get_hindsight_problem(self) -> VrpInstance:
np.ix_(customer_idx, customer_idx)
],
release_times=self.req_release_time,
num_vehicles=num_vehicles,
shift_tw_early=shift_tw_early,
)


Expand Down
22 changes: 22 additions & 0 deletions VrpInstance.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class VrpInstance:
Boolean array indicating whether a request must be dispatched.
prizes
Request prizes.
num_vehicles
Number of available primary vehicles.
shift_tw_early
Shift time window early of primary vehicles.
"""

def __init__(
Expand All @@ -55,6 +59,8 @@ def __init__(
dispatch_times: Optional[npt.NDArray[np.int_]] = None,
must_dispatch: Optional[npt.NDArray[np.bool_]] = None,
prizes: Optional[npt.NDArray[np.int_]] = None,
num_vehicles: Optional[int] = None,
shift_tw_early: Optional[list[int]] = None,
):
self._is_depot = is_depot
self._coords = coords
Expand All @@ -79,6 +85,10 @@ def __init__(
self._prizes = _set_if_none(
prizes, np.zeros(self.dimension, dtype=int)
)
self._num_vehicles = _set_if_none(num_vehicles, self.dimension)
self._shift_tw_early = _set_if_none(
shift_tw_early, [0] * self.num_vehicles
)

@property
def is_depot(self) -> npt.NDArray[np.bool_]:
Expand Down Expand Up @@ -132,6 +142,14 @@ def must_dispatch(self) -> npt.NDArray[np.bool_]:
def prizes(self) -> npt.NDArray[np.int_]:
return self._prizes

@property
def num_vehicles(self) -> int:
return self._num_vehicles

@property
def shift_tw_early(self) -> list[int]:
return self._shift_tw_early

@property
def horizon(self) -> int:
return self._time_windows[0, 1]
Expand All @@ -155,6 +173,8 @@ def replace(
dispatch_times: Optional[npt.NDArray[np.int_]] = None,
must_dispatch: Optional[npt.NDArray[np.bool_]] = None,
prizes: Optional[npt.NDArray[np.int_]] = None,
num_vehicles: Optional[int] = None,
shift_tw_early: Optional[list[int]] = None,
) -> "VrpInstance":
return VrpInstance(
is_depot=_copy_if_none(is_depot, self.is_depot),
Expand All @@ -172,6 +192,8 @@ def replace(
dispatch_times=_copy_if_none(dispatch_times, self.dispatch_times),
must_dispatch=_copy_if_none(must_dispatch, self.must_dispatch),
prizes=_copy_if_none(prizes, self.prizes),
num_vehicles=_copy_if_none(num_vehicles, self.num_vehicles),
shift_tw_early=_copy_if_none(shift_tw_early, self.shift_tw_early),
)

def filter(self, mask: npt.NDArray[np.bool_]) -> "VrpInstance":
Expand Down
6 changes: 6 additions & 0 deletions agents/IterativeConditionalDispatch.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import warnings
from functools import partial
from multiprocessing import Pool

Expand Down Expand Up @@ -122,6 +123,7 @@ def _determine_dispatch(self, info: StaticInfo, obs: State) -> np.ndarray:
solutions = pool.map(self._solve_scenario, scenarios)

to_dispatch, to_postpone = self.consensus_func(
info,
list(zip(scenarios, solutions)),
ep_inst,
to_dispatch,
Expand All @@ -139,4 +141,8 @@ def _solve_scenario(self, instance: VrpInstance) -> list[list[int]]:
Solves a single scenario instance, returning the solution.
"""
result = scenario_solver(instance, self.seed, self.scenario_time_limit)

if not result.best.is_feasible():
warnings.warn("Infeasible scenario instance!")

return [route.visits() for route in result.best.get_routes()]
4 changes: 4 additions & 0 deletions agents/consensus/ConsensusFunction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import numpy as np

from Environment import StaticInfo
from VrpInstance import VrpInstance


Expand All @@ -12,6 +13,7 @@ class ConsensusFunction(Protocol):

def __call__(
self,
info: StaticInfo,
scenarios: list[tuple[VrpInstance, list[list[int]]]],
instance: VrpInstance,
to_dispatch: np.ndarray,
Expand All @@ -22,6 +24,8 @@ def __call__(
Parameters
----------
info
The static information about the problem.
scenarios
The set of scenarios and their solutions.
instance
Expand Down
5 changes: 5 additions & 0 deletions agents/consensus/fixed_threshold.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import numpy as np

from Environment import StaticInfo

from .utils import (
select_dispatch_on_threshold,
select_postpone_on_threshold,
Expand All @@ -8,6 +10,7 @@


def fixed_threshold(
info: StaticInfo,
scenarios: list[tuple[dict, list[list[int]]]],
instance: dict,
old_dispatch: np.ndarray,
Expand All @@ -21,6 +24,8 @@ def fixed_threshold(
Parameters
----------
info
Static information about the problem.
scenarios
The list of instances and solutions, one for each scenario.
old_dispatch
Expand Down
3 changes: 3 additions & 0 deletions agents/consensus/hamming_distance.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import numpy as np

from Environment import StaticInfo

from .utils import (
get_dispatch_matrix,
select_postpone_on_threshold,
Expand All @@ -8,6 +10,7 @@


def hamming_distance(
info: StaticInfo,
scenarios: list[tuple[dict, list[list[int]]]],
instance: dict,
old_dispatch: np.ndarray,
Expand Down
2 changes: 2 additions & 0 deletions agents/consensus/prize_collecting.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import numpy as np

from Environment import StaticInfo
from static_solvers import default_solver

from .utils import get_dispatch_count, verify_action


def prize_collecting(
info: StaticInfo,
scenarios: list[tuple[dict, list[list[int]]]],
instance,
old_dispatch: np.ndarray,
Expand Down
Loading

0 comments on commit 2330204

Please sign in to comment.