diff --git a/docs/source/_static/algorithms.puml b/docs/source/_static/algorithms.puml new file mode 100644 index 00000000..5f677308 --- /dev/null +++ b/docs/source/_static/algorithms.puml @@ -0,0 +1,106 @@ +@startuml +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 100 + +hide empty methods +hide empty attributes + +entity Job <> +entity Fleet <> + +entity Result <> + +abstract class Algorithm <> + +class MilpAlgorithm <> + +abstract class LinearModel <> +class ThreeIndexLinearModel <> + +class InsertionAlgorithm <> + +class InsertionIterator <> +class RankingInsertionIterator <> +class StatelessInsertionIterator <> + +class InsertionStrategy <> +class IntensiveInsertionStrategy <> +class SamplingInsertionStrategy <> +class TailInsertionStrategy <> + +class LocalSearchAlgorithm <> + +abstract class LocalSearchStrategy <> +class ReallocationLocalSearchStrategy <> +class OneShiftLocalSearchStrategy <> +class TwoOPTLocalSearchStrategy <> + +class GRASPAlgorithm <> +class IterativeAlgorithm <> +class SequentialAlgorithm <> + + +InsertionAlgorithm -up-|> Algorithm +Algorithm <|-- LocalSearchAlgorithm +Algorithm <|-- GRASPAlgorithm +MilpAlgorithm --|> Algorithm +IterativeAlgorithm --|> Algorithm +SequentialAlgorithm --|> Algorithm + + + +InsertionStrategy <|- IntensiveInsertionStrategy +InsertionStrategy <|-- SamplingInsertionStrategy +InsertionStrategy <|-- TailInsertionStrategy + +RankingInsertionIterator --|> InsertionIterator +StatelessInsertionIterator --|> InsertionIterator + +LinearModel <|-up- ThreeIndexLinearModel + +LocalSearchStrategy <|-- ReallocationLocalSearchStrategy +LocalSearchStrategy <|-- OneShiftLocalSearchStrategy +LocalSearchStrategy <|-- TwoOPTLocalSearchStrategy + + + +InsertionAlgorithm -up- InsertionIterator +InsertionAlgorithm -down- InsertionStrategy + +MilpAlgorithm -up- LinearModel + +LocalSearchAlgorithm -down- LocalSearchStrategy + + +GRASPAlgorithm -down- LocalSearchAlgorithm +GRASPAlgorithm -down- InsertionAlgorithm + + +note "composed of" as N1 +N1 --o IterativeAlgorithm +Algorithm - N1 + +note "composed of" as N2 +N2 --o SequentialAlgorithm +Algorithm - N2 + + +Job "receives "-o Algorithm +Fleet "receives"-o Algorithm +Result "generates"- Algorithm + +Job -down[hidden]- Fleet +Fleet -down[hidden]- Result + + +ReallocationLocalSearchStrategy -down[hidden]- OneShiftLocalSearchStrategy +OneShiftLocalSearchStrategy -down[hidden]- TwoOPTLocalSearchStrategy + +IntensiveInsertionStrategy -down[hidden]- SamplingInsertionStrategy +SamplingInsertionStrategy -down[hidden]- TailInsertionStrategy + +RankingInsertionIterator -down[hidden]- StatelessInsertionIterator + +@enduml + diff --git a/docs/source/_static/data_model.puml b/docs/source/_static/data_model.puml index c22d9f32..8b8b009d 100644 --- a/docs/source/_static/data_model.puml +++ b/docs/source/_static/data_model.puml @@ -5,59 +5,69 @@ skinparam ranksep 100 hide empty methods hide empty attributes -entity Vehicle { +abstract class Algorithm <> + +entity Vehicle <> { identifier: str capacity: float timeout: float } -entity Route { +entity Route <> { uuid: UUID } -entity Trip { +entity Trip <> { identifier: str on_time_bonus: float capacity: float timeout: float } -entity PlannedTrip { +entity PlannedTrip <> { } -entity Planning { +entity Planning <> { computation_time: float } -entity Stop { +entity Result <> { +} + + +entity Stop <> { starting_time?: float } -entity Service { +entity Service <> { earliest: float latest: float duration: float } -entity Job { +entity Job <> { } -entity Fleet { +entity Fleet <> { } -entity Position { +entity Position <> { coordinates: Tuple[float, ...] } -entity Surface { +entity Surface <> { uuid: UUID } + Job o-"*" Trip -Algorithm --o Planning: optimizes > -Algorithm o-- Fleet -Algorithm o-- Job +Algorithm -- Result: generates > +Result --o Planning +Result o-- Fleet +Result o-- Job +Algorithm o-- Fleet: receives < +Algorithm o-- Job: receives < Planning o--"*" Route Trip o- PlannedTrip: planified > Trip o-right-"origin" Service @@ -77,4 +87,4 @@ Vehicle o-left-"origin" Service Vehicle o-left-"destination" Service -@enduml \ No newline at end of file +@enduml diff --git a/docs/source/_static/loaders.puml b/docs/source/_static/loaders.puml new file mode 100644 index 00000000..9af9c3e3 --- /dev/null +++ b/docs/source/_static/loaders.puml @@ -0,0 +1,42 @@ +@startuml +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 100 + +hide empty methods +hide empty attributes + +entity Job <> +entity Fleet <> +entity Surface <> + +abstract class Loader <> { +} + +class FileLoader <> { +} + +abstract class LoaderFormatter <> { +} + +class CordeauLaporteLoaderFormatter <> { +} + +class HashCodeLoaderFormatter <> { +} + + +Loader -up-"loads" Job +Loader -up-"loads" Fleet +Loader -up-"loads" Surface + +LoaderFormatter "uses"-right-o Loader + +Loader <|-down- FileLoader + +LoaderFormatter <|-down- CordeauLaporteLoaderFormatter +LoaderFormatter <|-down- HashCodeLoaderFormatter + + +@enduml + diff --git a/docs/source/_static/modules.puml b/docs/source/_static/modules.puml new file mode 100644 index 00000000..4428ecd5 --- /dev/null +++ b/docs/source/_static/modules.puml @@ -0,0 +1,14 @@ +@startuml + +package "jinete" { + [models] + [loaders] + [algorithms] + [storers] + [dispachers] + [solvers] + [exceptions] + [utils] +} + +@enduml diff --git a/docs/source/_static/storers.puml b/docs/source/_static/storers.puml new file mode 100644 index 00000000..32be0ce8 --- /dev/null +++ b/docs/source/_static/storers.puml @@ -0,0 +1,52 @@ +@startuml +skinparam linetype ortho +skinparam nodesep 100 +skinparam ranksep 100 + +hide empty methods +hide empty attributes + +entity Result <> + +entity Storer <> { +} + +abstract class FileStorer <> { +} +class SetStorer <> { +} +class PromptStorer <> { +} +class GraphPlotStorer <> { +} + +abstract class StorerFormatter <> { +} +class ColumnarStorerFormatter <> { +} +class HashCodeStorerFormatter <> { +} + + +Storer -up-"stores" Result + +StorerFormatter "uses"-right-o Storer + +note "composed of" as N2 +N2 --o SetStorer +N2 - Storer + + +Storer <|-down- FileStorer +Storer <|-down- PromptStorer +Storer <|-down- SetStorer + + +Storer <|-down- GraphPlotStorer + +StorerFormatter <|-down- ColumnarStorerFormatter +StorerFormatter <|-down- HashCodeStorerFormatter + + +@enduml + diff --git a/examples/cordeau/benchmark_launch.py b/examples/cordeau/benchmark_launch.py new file mode 100644 index 00000000..4790415d --- /dev/null +++ b/examples/cordeau/benchmark_launch.py @@ -0,0 +1,45 @@ +import logging +import traceback +from concurrent import ( + futures, +) +from pathlib import ( + Path, +) +from subprocess import ( + STDOUT, + TimeoutExpired, + check_output, +) + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +DATASETS_PATH = Path(__file__).absolute().parents[2] / "res" / "datasets" / "cordeau-laporte" +INSTANCE_TIMEOUT = 7200 + + +def main(): + with futures.ProcessPoolExecutor() as executor: + for file_path in DATASETS_PATH.glob("*.txt"): + executor.submit(run_one, file_path) + + +def run_one(file_path): + try: + command = ["python3", "benchmark_solve.py", str(file_path.absolute())] + print(f'COMMAND: "{" ".join(command)}"') + try: + output = check_output(command, timeout=INSTANCE_TIMEOUT, stderr=STDOUT, cwd=Path(__file__).parent).decode() + except TimeoutExpired as exc: + print(exc) + output = exc.output.decode() + with (file_path.parent / f"{file_path.name}.log").open("w") as file: + file.write(output) + except Exception as exc: + traceback.print_exc() + raise exc + + +if __name__ == "__main__": + main() diff --git a/examples/cordeau/benchmark_solve.py b/examples/cordeau/benchmark_solve.py new file mode 100644 index 00000000..03100469 --- /dev/null +++ b/examples/cordeau/benchmark_solve.py @@ -0,0 +1,42 @@ +import logging +import sys +import traceback +from functools import ( + partial, +) +from pathlib import ( + Path, +) + +import jinete as jit + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +def main(): + logger.info("Starting...") + file_path = Path(sys.argv[1]) + solver = jit.Solver( + loader_kwargs={"file_path": file_path}, + algorithm=jit.GraspAlgorithm, + algorithm_kwargs={"first_solution_kwargs": {"episodes": 1, "randomized_size": 2}, "episodes": 5}, + storer=jit.StorerSet, + storer_kwargs={ + "storer_cls_set": { + jit.PromptStorer, + partial(jit.GraphPlotStorer, file_path=file_path.parent / f"{file_path.name}.png"), + partial(jit.FileStorer, file_path=file_path.parent / f"{file_path.name}.output"), + } + }, + ) + result = solver.solve() # noqa + logger.info("Finished...") + + +if __name__ == "__main__": + try: + main() + except Exception as exc: + traceback.print_exc() + raise exc diff --git a/examples/cordeau/example_grasp.py b/examples/cordeau/example_grasp.py index 4f69d00a..5ef71082 100644 --- a/examples/cordeau/example_grasp.py +++ b/examples/cordeau/example_grasp.py @@ -24,7 +24,7 @@ def main(): solver = jit.Solver( loader_kwargs={"file_path": file_path}, algorithm=jit.GraspAlgorithm, - algorithm_kwargs={"first_solution_kwargs": {"episodes": 1, "strategy_cls": jit.IntensiveInsertionStrategy}}, + algorithm_kwargs={"first_solution_kwargs": {"episodes": 1, "randomized_size": 2}, "episodes": 5}, storer=jit.StorerSet, storer_kwargs={"storer_cls_set": {jit.PromptStorer, jit.GraphPlotStorer}}, ) diff --git a/jinete/_version.py b/jinete/_version.py index 347bd406..7cad94cf 100644 --- a/jinete/_version.py +++ b/jinete/_version.py @@ -1,5 +1,5 @@ """The module to allocate the versioning information.""" -VERSION = (0, 0, 14) +VERSION = (0, 1, 0) __version__ = ".".join(map(str, VERSION)) diff --git a/jinete/algorithms/abc.py b/jinete/algorithms/abc.py index f49bf35f..4ed3f4b9 100644 --- a/jinete/algorithms/abc.py +++ b/jinete/algorithms/abc.py @@ -68,7 +68,7 @@ def optimize(self) -> Result: result = Result(algorithm=self, planning=planning, computation_time=computation_time,) logger.info( f'Optimized with {self.__class__.__name__} obtaining {"" if result.feasible else "non "}' - f"feasible results!" + f"feasible results and {result.optimization_value} cost!" ) return result diff --git a/jinete/algorithms/exacts/models/three_index.py b/jinete/algorithms/exacts/models/three_index.py index 13e40c12..a558f831 100644 --- a/jinete/algorithms/exacts/models/three_index.py +++ b/jinete/algorithms/exacts/models/three_index.py @@ -200,7 +200,7 @@ def _time_window_by_position_idx(self, idx: int) -> Tuple[float, float]: elif position == trip.destination_position: earliest, latest = trip.destination_earliest, trip.destination_latest else: - raise Exception(f"There was a problem related with earliest, latest indices.") + raise Exception("There was a problem related with earliest, latest indices.") return earliest, latest def _capacity_by_position_idx(self, idx: int) -> float: @@ -376,7 +376,7 @@ def _validate(self): assert min(abs(self._x[k][i][j].varValue), abs(self._x[k][i][j].varValue - 1)) <= 0.05 def _solution_to_routes(self): - logger.info(f"Casting solution to a set of routes...") + logger.info("Casting solution to a set of routes...") routes = set() for k in self._routes_indexer: vehicle = self._vehicles[k] diff --git a/jinete/algorithms/heuristics/insertion/iterators/abc.py b/jinete/algorithms/heuristics/insertion/iterators/abc.py index 4563fdab..bbad14f2 100644 --- a/jinete/algorithms/heuristics/insertion/iterators/abc.py +++ b/jinete/algorithms/heuristics/insertion/iterators/abc.py @@ -58,9 +58,9 @@ def __init__( **kwargs, ): if strategy_cls is None: - from ..strategies import TailInsertionStrategy + from ..strategies import SamplingInsertionStrategy - strategy_cls = TailInsertionStrategy + strategy_cls = SamplingInsertionStrategy if criterion_cls is None: from .....models import EarliestLastDepartureTimeRouteCriterion diff --git a/jinete/algorithms/heuristics/insertion/iterators/ranking.py b/jinete/algorithms/heuristics/insertion/iterators/ranking.py index 32b8402c..d8845ae5 100644 --- a/jinete/algorithms/heuristics/insertion/iterators/ranking.py +++ b/jinete/algorithms/heuristics/insertion/iterators/ranking.py @@ -28,7 +28,7 @@ class RankingInsertionIterator(InsertionIterator): ranking: Dict[Vehicle, List[Route]] - def __init__(self, neighborhood_max_size: int = 250, randomized_size: int = 1, seed: int = 56, *args, **kwargs): + def __init__(self, neighborhood_max_size: int = 24, randomized_size: int = 1, seed: int = 56, *args, **kwargs): super().__init__(*args, **kwargs) if neighborhood_max_size is None: @@ -84,7 +84,10 @@ def __next__(self) -> Route: continue for current in sub_ranking: - if any(candidates) and self._criterion.best(candidates[-1], current) == candidates[-1]: + if ( + len(candidates) == self.randomized_size + and self._criterion.best(candidates[-1], current) == candidates[-1] + ): break if self.randomized_size <= len(candidates): diff --git a/jinete/algorithms/heuristics/insertion/strategies/sampling.py b/jinete/algorithms/heuristics/insertion/strategies/sampling.py index 7e997e0b..3ba840a6 100644 --- a/jinete/algorithms/heuristics/insertion/strategies/sampling.py +++ b/jinete/algorithms/heuristics/insertion/strategies/sampling.py @@ -13,6 +13,9 @@ from .....models import ( Trip, ) +from .....utils import ( + sample_index_pairs, +) from .abc import ( InsertionStrategy, ) @@ -34,19 +37,12 @@ def __init__(self, seed: int = 56, *args, **kwargs): self.random = Random(seed) def compute( - self, route: Route, trips: Union[Trip, Iterable[Trip]], count: int = 25, *args, **kwargs + self, route: Route, trips: Union[Trip, Iterable[Trip]], count: int = 128, *args, **kwargs ) -> List[Route]: if not isinstance(trips, Trip): trips = tuple(trips) - indices = set() - for _ in range(count): - sampled_i = self.random.randint(0, len(route.stops) - 2) - sampled_j = self.random.randint(sampled_i + 1, len(route.stops) - 1) - pair = (sampled_i, sampled_j) - indices.add(pair) - routes = list() - for i, j in indices: + for i, j in sample_index_pairs(len(route.stops), count, self.random): routes += super().compute(route, trips, i, j, *args, **kwargs) return routes diff --git a/jinete/algorithms/heuristics/local_search/strategies/plannings/reallocation.py b/jinete/algorithms/heuristics/local_search/strategies/plannings/reallocation.py index bc686387..feb05983 100644 --- a/jinete/algorithms/heuristics/local_search/strategies/plannings/reallocation.py +++ b/jinete/algorithms/heuristics/local_search/strategies/plannings/reallocation.py @@ -1,6 +1,10 @@ import itertools as it import logging +from jinete.utils import ( + sample_index_pairs, +) + from ..abc import ( LocalSearchStrategy, ) @@ -33,7 +37,7 @@ def _improve(self) -> None: partial_cost_origin = partial_cost + self._objective.optimization_function(new_origin)[-1] - for i, j in it.combinations(range(len(destination.stops) - 1), 2): + for i, j in sample_index_pairs(len(destination.stops), 128): destinations = strategy.compute(destination, trip, i, j) if not any(destinations): continue diff --git a/jinete/algorithms/heuristics/local_search/strategies/routes/two_opt.py b/jinete/algorithms/heuristics/local_search/strategies/routes/two_opt.py index d8a5c10b..8a735145 100644 --- a/jinete/algorithms/heuristics/local_search/strategies/routes/two_opt.py +++ b/jinete/algorithms/heuristics/local_search/strategies/routes/two_opt.py @@ -24,10 +24,10 @@ def _improve(self) -> None: j = k + 1 condition = any( any( - delivery in chain.from_iterable(b.pickup_planned_trips for b in route.stops[i:j]) + delivery in chain.from_iterable(b.pickup_planned_trips for b in route.stops[i : j + 1]) for delivery in a.delivery_planned_trips ) - for a in route.stops[i:j] + for a in route.stops[i : j + 1] ) if condition: continue diff --git a/jinete/algorithms/metaheuristics/grasp.py b/jinete/algorithms/metaheuristics/grasp.py index 9e271f8e..2471a855 100644 --- a/jinete/algorithms/metaheuristics/grasp.py +++ b/jinete/algorithms/metaheuristics/grasp.py @@ -13,7 +13,6 @@ ) from ...models import ( - MAX_INT, Planning, ) from ..abc import ( @@ -30,6 +29,8 @@ from typing import ( Dict, Any, + Tuple, + Type, ) logger = logging.getLogger(__name__) @@ -72,40 +73,34 @@ def __init__( self.first_solution_kwargs = first_solution_kwargs self.local_search_kwargs = local_search_kwargs self.random = Random(seed) + self.args = args + self.kwargs = kwargs - def _build_first_solution_algorithm(self, **kwargs) -> Algorithm: + def _build_first_solution_algorithm(self, **kwargs) -> Tuple[Type[Algorithm], Dict]: kwargs.update(self.first_solution_kwargs.copy()) if "fleet" not in kwargs: kwargs["fleet"] = self.fleet if "job" not in kwargs: kwargs["job"] = self.job - if "seed" not in kwargs: - kwargs["seed"] = self.random.randint(0, MAX_INT) - return IterativeAlgorithm(**kwargs) + return IterativeAlgorithm, kwargs - def _build_local_search_algorithm(self, **kwargs) -> Algorithm: + def _build_local_search_algorithm(self, **kwargs) -> Tuple[Type[Algorithm], Dict]: kwargs.update(self.local_search_kwargs.copy()) if "fleet" not in kwargs: kwargs["fleet"] = self.fleet if "job" not in kwargs: kwargs["job"] = self.job - if "seed" not in kwargs: - kwargs["seed"] = self.random.randint(0, MAX_INT) - return SequentialAlgorithm(**kwargs) + kwargs["algorithm_cls"] = SequentialAlgorithm + kwargs["restart_mode"] = False + kwargs["episodes"] = 3 + return IterativeAlgorithm, kwargs def _optimize(self) -> Planning: - iterative = self._build_first_solution_algorithm() - best = iterative.optimize() - - no_improvement_count = 0 - while no_improvement_count < self.no_improvement_threshold: - no_improvement_count += 1 - - current = self._build_local_search_algorithm(initial=best).optimize() - - best = self._objective.best(best, current) - - if best == current: - no_improvement_count = 0 - - return best.planning + algorithm = IterativeAlgorithm( + algorithm_cls=SequentialAlgorithm, + algorithms_cls=[self._build_first_solution_algorithm(), self._build_local_search_algorithm()], + *self.args, + **self.kwargs + ) + result = algorithm.optimize() + return result.planning diff --git a/jinete/algorithms/metaheuristics/iterative.py b/jinete/algorithms/metaheuristics/iterative.py index d5a69994..9e787e26 100644 --- a/jinete/algorithms/metaheuristics/iterative.py +++ b/jinete/algorithms/metaheuristics/iterative.py @@ -40,7 +40,15 @@ class IterativeAlgorithm(Algorithm): for a defined number of episodes. It's mostly used as a component of more complicated metaheuristics. """ - def __init__(self, episodes: int = 3, algorithm_cls: Type[Algorithm] = None, seed: int = 56, *args, **kwargs): + def __init__( + self, + episodes: int = 3, + algorithm_cls: Type[Algorithm] = None, + seed: int = 56, + restart_mode: bool = True, + *args, + **kwargs, + ): """Construct a new instance. :param episodes: The number of episodes to repeat the algorithm. @@ -55,21 +63,25 @@ def __init__(self, episodes: int = 3, algorithm_cls: Type[Algorithm] = None, see self.episodes = episodes self.algorithm_cls = algorithm_cls self.random = Random(seed) - + self.restart_mode = restart_mode self.args = args self.kwargs = kwargs def _build_algorithm(self, *args, **kwargs) -> Algorithm: args = (*self.args, *args) - kwargs.update(self.kwargs) + new_kwargs = self.kwargs.copy() + new_kwargs.update(kwargs) - return self.algorithm_cls(*args, **kwargs) + return self.algorithm_cls(*args, **new_kwargs) def _optimize(self) -> Planning: best: Optional[Result] = None for i in range(self.episodes): seed = self.random.randint(0, MAX_INT) - current = self._build_algorithm(seed=seed).optimize() + kwargs = {"seed": seed} + if not self.restart_mode and best is not None: + kwargs["initial"] = best + current = self._build_algorithm(**kwargs).optimize() best = self._objective.best(best, current) assert best is not None diff --git a/jinete/algorithms/metaheuristics/sequential.py b/jinete/algorithms/metaheuristics/sequential.py index fedc80e5..61b250d8 100644 --- a/jinete/algorithms/metaheuristics/sequential.py +++ b/jinete/algorithms/metaheuristics/sequential.py @@ -42,7 +42,7 @@ class SequentialAlgorithm(Algorithm): def __init__( self, - initial: Result, + initial: Result = None, algorithms_cls: Sequence[Tuple[Type[Algorithm], Dict[str, Any]]] = None, seed: int = 56, *args, @@ -59,15 +59,15 @@ def __init__( super().__init__(*args, **kwargs) if algorithms_cls is None: - from .iterative import IterativeAlgorithm from ..heuristics import ( + InsertionAlgorithm, LocalSearchAlgorithm, ReallocationLocalSearchStrategy, ) algorithms_cls = [ (LocalSearchAlgorithm, {**kwargs, "strategy_cls": ReallocationLocalSearchStrategy}), - (IterativeAlgorithm, kwargs), + (InsertionAlgorithm, kwargs), ] self.random = Random(seed) diff --git a/jinete/loaders/formatters/cordeau_laporte.py b/jinete/loaders/formatters/cordeau_laporte.py index 1b251e4c..8876498b 100644 --- a/jinete/loaders/formatters/cordeau_laporte.py +++ b/jinete/loaders/formatters/cordeau_laporte.py @@ -115,5 +115,5 @@ def surface(self, *args, **kwargs) -> Surface: :return: A surface instance from the loaded instance. """ surface = GeometricSurface(DistanceMetric.EUCLIDEAN) - logger.info(f"Created surface!") + logger.info("Created surface!") return surface diff --git a/jinete/loaders/formatters/hashcode.py b/jinete/loaders/formatters/hashcode.py index 8c8630b3..ee20fb95 100644 --- a/jinete/loaders/formatters/hashcode.py +++ b/jinete/loaders/formatters/hashcode.py @@ -73,7 +73,7 @@ def _build_trip( origin = Service(position=surface.get_or_create_position([x1, y1]), earliest=earliest, latest=latest,) destination = Service(position=surface.get_or_create_position([x2, y2]),) trip = Trip(identifier, on_time_bonus=bonus, origin=origin, destination=destination) - logger.debug(f"Created trip!") + logger.debug("Created trip!") return trip def surface(self, *args, **kwargs) -> Surface: @@ -84,5 +84,5 @@ def surface(self, *args, **kwargs) -> Surface: :return: A surface instance from the loaded instance. """ surface = GeometricSurface(DistanceMetric.MANHATTAN) - logger.info(f"Created surface!") + logger.info("Created surface!") return surface diff --git a/jinete/storers/formatters/columnar.py b/jinete/storers/formatters/columnar.py index adb19821..e37b12c5 100644 --- a/jinete/storers/formatters/columnar.py +++ b/jinete/storers/formatters/columnar.py @@ -36,7 +36,7 @@ def format(self) -> str: ( f'Planning UUID: "{self._planning.uuid}"', f'Routes count: "{len(self._routes)}"', - f"Routes: ", + "Routes: ", "\n".join(f"{self.tab_character}{row}" for row in rows), f'Computation time: "{self._computation_time:0.4f}" seconds', f'Coverage Rate: "{self._coverage_rate}"', @@ -51,7 +51,7 @@ def _route_to_str(self, route: Route) -> List[str]: planned_trip_rows = [self._planned_trip_to_str(planned_trip) for planned_trip in route.planned_trips] stop_rows = [self._stop_to_str(stop) for stop in route.stops] return [ - f"Vehicle: ", + "Vehicle: ", *(f"{self.tab_character}{row}" for row in self._vehicle_to_str(route.vehicle)), f'Planned Trips: "{sum(1 for _ in route.planned_trips)}"', *(f"{self.tab_character}{row}" for row in planned_trip_rows), diff --git a/jinete/storers/plots/graph.py b/jinete/storers/plots/graph.py index 6a809139..4d0623b7 100644 --- a/jinete/storers/plots/graph.py +++ b/jinete/storers/plots/graph.py @@ -22,12 +22,23 @@ Any, Tuple, ) + from pathlib import Path from ...models import Position class GraphPlotStorer(Storer): """Generate a directed graph representation of the solution.""" + def __init__(self, file_path: Path = None, *args, **kwargs): + """Construct a new object instance. + + :param file_path: The file path in which to store the problem solution. + :param args: Additional positional arguments. + :param kwargs: Additional named arguments. + """ + super().__init__(*args, **kwargs) + self.file_path = file_path + def _generate_nodes(self, edges: Dict[Tuple[Position, Position], Dict[str, Any]]) -> Dict[Position, Dict[str, Any]]: nodes: Dict[Position, Dict[str, Any]] = dict() for trip in self._trips: @@ -73,8 +84,7 @@ def _generate_graph(self) -> nx.Graph: return graph - @staticmethod - def _show_graph(graph: nx.Graph) -> None: + def _show_graph(self, graph: nx.Graph) -> None: import matplotlib as mpl mpl.rcParams["figure.dpi"] = 300 @@ -90,7 +100,10 @@ def _show_graph(graph: nx.Graph) -> None: nx.draw_networkx_labels(graph, pos, labels=node_labels, font_size=5, font_color="white") nx.draw_networkx_edge_labels(graph, pos, edge_labels=edge_labels) - plt.show() + if self.file_path is not None: + plt.savefig(str(self.file_path)) + else: + plt.show() def store(self) -> None: """Perform a storage process.""" diff --git a/jinete/storers/sets.py b/jinete/storers/sets.py index bc0c1880..399ff6b2 100644 --- a/jinete/storers/sets.py +++ b/jinete/storers/sets.py @@ -43,7 +43,8 @@ def __init__(self, storer_cls_set: Set[Type[Storer]], *args, **kwargs): def store(self) -> None: """Perform a storage process.""" for storer_cls in self.storer_cls_set: - logger.info(f'Storing result with "{storer_cls.__name__}"...') + name = getattr(storer_cls, "__name__", None) + logger.info(f'Storing result with "{name}"...') storer = storer_cls(*self.args, **self.kwargs) storer.store() diff --git a/jinete/utils/__init__.py b/jinete/utils/__init__.py index b46202b6..c5ba3528 100644 --- a/jinete/utils/__init__.py +++ b/jinete/utils/__init__.py @@ -3,3 +3,6 @@ from .collections import ( remove_duplicates, ) +from .random import ( + sample_index_pairs, +) diff --git a/jinete/utils/random.py b/jinete/utils/random.py new file mode 100644 index 00000000..a72959ae --- /dev/null +++ b/jinete/utils/random.py @@ -0,0 +1,44 @@ +import itertools as it +import operator as op +from functools import ( + reduce, +) +from random import ( + Random, +) +from typing import ( + Iterable, + Tuple, +) + + +def ncr(n, r): + """ + Based on: https://stackoverflow.com/a/4941932/3921457 + :param n: + :param r: + :return: + """ + r = min(r, n - r) + numer = reduce(op.mul, range(n, n - r, -1), 1) + denom = reduce(op.mul, range(1, r + 1), 1) + return numer // denom + + +def sample_index_pairs(n: int, count: int, random: Random = None) -> Iterable[Tuple[int, int]]: + if random is None: + from random import randint as generator + else: + generator = random.randint + + maximum = ncr(n, 2) + if maximum <= count: + indices = it.combinations(range(n), 2) + else: + indices = set() + while len(indices) < count: + sampled_i = generator(0, n - 2) + sampled_j = generator(sampled_i + 1, n - 1) + pair = (sampled_i, sampled_j) + indices.add(pair) + return indices diff --git a/res/datasets/cordeau-laporte/a3-18.txt b/res/datasets/cordeau-laporte/a3-18.txt deleted file mode 100644 index ff42447f..00000000 --- a/res/datasets/cordeau-laporte/a3-18.txt +++ /dev/null @@ -1,39 +0,0 @@ -3 18 360 3 30 - 0 0.000 0.000 0 0 0 360 - 1 0.402 -3.458 3 1 0 1440 - 2 -7.109 -6.363 3 1 0 1440 - 3 9.418 5.880 3 1 0 1440 - 4 -6.219 -6.439 3 1 0 1440 - 5 9.380 9.318 3 1 0 1440 - 6 -5.523 -7.451 3 1 0 1440 - 7 9.139 0.579 3 1 0 1440 - 8 -2.985 7.011 3 1 0 1440 - 9 -8.475 -4.578 3 1 0 1440 - 10 -7.731 7.409 3 1 245 260 - 11 -9.853 3.676 3 1 164 179 - 12 -4.339 3.263 3 1 206 221 - 13 -8.243 3.928 3 1 195 210 - 14 5.889 -8.397 3 1 150 165 - 15 9.544 -9.925 3 1 177 192 - 16 -4.591 3.016 3 1 195 210 - 17 -0.644 8.275 3 1 36 51 - 18 -0.741 -3.629 3 1 253 268 - 19 -7.915 -8.234 3 -1 291 306 - 20 5.051 -0.631 3 -1 112 127 - 21 -2.519 5.263 3 -1 297 312 - 22 2.076 5.155 3 -1 86 101 - 23 7.336 -4.226 3 -1 224 239 - 24 -7.721 -0.208 3 -1 172 187 - 25 -9.806 -4.319 3 -1 74 89 - 26 -1.428 0.653 3 -1 314 329 - 27 0.071 7.405 3 -1 125 140 - 28 -8.814 -4.170 3 -1 0 1440 - 29 -4.790 9.465 3 -1 0 1440 - 30 -6.058 -1.790 3 -1 0 1440 - 31 -2.651 2.336 3 -1 0 1440 - 32 9.352 2.900 3 -1 0 1440 - 33 4.425 -5.034 3 -1 0 1440 - 34 -2.765 -7.182 3 -1 0 1440 - 35 -7.036 -6.967 3 -1 0 1440 - 36 -1.306 -7.478 3 -1 0 1440 - 37 0.000 0.000 0 0 0 360 diff --git a/res/datasets/cordeau-laporte/a4-16.txt b/res/datasets/cordeau-laporte/a4-16.txt deleted file mode 100644 index 2976af56..00000000 --- a/res/datasets/cordeau-laporte/a4-16.txt +++ /dev/null @@ -1,35 +0,0 @@ -4 16 240 3 30 - 0 0.000 0.000 0 0 0 240 - 1 6.267 0.981 3 1 0 1440 - 2 -4.718 6.925 3 1 0 1440 - 3 3.254 7.621 3 1 0 1440 - 4 9.654 2.799 3 1 0 1440 - 5 8.575 2.701 3 1 0 1440 - 6 -9.732 -8.314 3 1 0 1440 - 7 9.937 4.969 3 1 0 1440 - 8 -4.604 6.053 3 1 0 1440 - 9 -5.029 5.070 3 1 53 68 - 10 -9.264 -7.979 3 1 105 120 - 11 8.738 -9.966 3 1 74 89 - 12 -3.420 7.617 3 1 3 18 - 13 -5.865 -4.571 3 1 172 187 - 14 6.300 5.322 3 1 61 76 - 15 -2.219 -7.605 3 1 179 194 - 16 -1.654 -7.838 3 1 154 169 - 17 -1.548 -4.124 3 -1 138 153 - 18 -4.818 6.259 3 -1 125 140 - 19 -7.389 0.376 3 -1 95 110 - 20 6.070 -0.549 3 -1 195 210 - 21 -1.663 6.171 3 -1 103 118 - 22 -0.665 -1.272 3 -1 103 118 - 23 -5.005 0.918 3 -1 142 157 - 24 6.200 -7.679 3 -1 157 172 - 25 5.575 -7.408 3 -1 0 1440 - 26 -7.754 3.535 3 -1 0 1440 - 27 2.111 1.439 3 -1 0 1440 - 28 1.706 -1.733 3 -1 0 1440 - 29 -1.796 9.104 3 -1 0 1440 - 30 -5.500 2.353 3 -1 0 1440 - 31 7.323 -7.149 3 -1 0 1440 - 32 -6.413 0.367 3 -1 0 1440 - 33 0.000 0.000 0 0 0 240 diff --git a/res/datasets/cordeau-laporte/a4-24.txt b/res/datasets/cordeau-laporte/a4-24.txt deleted file mode 100644 index af2d7923..00000000 --- a/res/datasets/cordeau-laporte/a4-24.txt +++ /dev/null @@ -1,51 +0,0 @@ -4 24 360 3 30 - 0 0.000 0.000 0 0 0 360 - 1 9.687 -5.197 3 1 0 1440 - 2 -0.955 -4.964 3 1 0 1440 - 3 -0.245 -0.351 3 1 0 1440 - 4 -5.758 -6.045 3 1 0 1440 - 5 -6.527 0.793 3 1 0 1440 - 6 -8.967 1.254 3 1 0 1440 - 7 8.719 0.727 3 1 0 1440 - 8 -7.198 -2.029 3 1 0 1440 - 9 4.269 -5.979 3 1 0 1440 - 10 1.731 5.973 3 1 0 1440 - 11 4.197 7.644 3 1 0 1440 - 12 -4.371 1.759 3 1 0 1440 - 13 -1.856 -7.891 3 1 286 301 - 14 7.838 1.692 3 1 142 157 - 15 1.392 -2.897 3 1 59 74 - 16 9.773 2.158 3 1 42 57 - 17 9.355 -8.010 3 1 224 239 - 18 -8.058 -9.043 3 1 292 307 - 19 -4.821 5.113 3 1 10 25 - 20 4.237 -7.611 3 1 164 179 - 21 1.900 1.014 3 1 191 206 - 22 5.011 9.878 3 1 217 232 - 23 -7.292 -5.534 3 1 52 67 - 24 9.399 -3.848 3 1 5 20 - 25 4.342 7.843 3 -1 226 241 - 26 2.241 -2.867 3 -1 139 154 - 27 -5.680 5.078 3 -1 160 175 - 28 -6.697 9.037 3 -1 318 333 - 29 0.447 -9.207 3 -1 164 179 - 30 1.806 8.200 3 -1 290 305 - 31 7.886 -6.478 3 -1 280 295 - 32 2.567 -2.163 3 -1 340 355 - 33 7.592 -6.082 3 -1 235 250 - 34 -1.839 5.686 3 -1 152 167 - 35 9.159 -5.010 3 -1 344 359 - 36 -3.977 6.882 3 -1 108 123 - 37 5.602 8.871 3 -1 0 1440 - 38 -8.327 -4.191 3 -1 0 1440 - 39 -9.922 5.413 3 -1 0 1440 - 40 -2.856 5.746 3 -1 0 1440 - 41 -0.056 7.000 3 -1 0 1440 - 42 -7.372 3.701 3 -1 0 1440 - 43 -8.155 -2.712 3 -1 0 1440 - 44 -4.874 -4.071 3 -1 0 1440 - 45 7.321 9.002 3 -1 0 1440 - 46 8.776 -2.832 3 -1 0 1440 - 47 -3.476 -5.302 3 -1 0 1440 - 48 -3.360 -9.644 3 -1 0 1440 - 49 0.000 0.000 0 0 0 360 diff --git a/res/datasets/cordeau-laporte/b3-18.txt b/res/datasets/cordeau-laporte/b3-18.txt deleted file mode 100644 index 2b027d5a..00000000 --- a/res/datasets/cordeau-laporte/b3-18.txt +++ /dev/null @@ -1,39 +0,0 @@ -3 18 360 6 45 - 0 0.000 0.000 0 0 0 360 - 1 -0.819 -2.163 6 6 0 1440 - 2 2.315 -1.741 2 2 0 1440 - 3 6.305 3.849 3 3 0 1440 - 4 -4.496 0.365 4 4 0 1440 - 5 -4.858 -3.155 6 6 0 1440 - 6 3.891 1.153 1 1 0 1440 - 7 -0.123 -7.644 5 5 0 1440 - 8 -5.949 -3.737 2 2 0 1440 - 9 3.015 1.707 6 6 0 1440 - 10 -0.370 6.703 3 3 243 258 - 11 -4.317 -4.992 2 2 119 134 - 12 -4.742 -6.746 4 4 284 299 - 13 6.469 0.941 5 5 218 233 - 14 8.450 -6.348 4 4 35 50 - 15 1.986 -3.734 6 6 237 252 - 16 -5.045 9.258 1 1 66 81 - 17 7.937 9.473 4 4 189 204 - 18 -9.863 -0.111 4 4 75 90 - 19 -8.055 4.222 6 -6 167 182 - 20 3.934 -9.589 2 -2 133 148 - 21 7.408 1.608 3 -3 271 286 - 22 5.988 -0.372 4 -4 138 153 - 23 4.874 -4.327 6 -6 183 198 - 24 -1.257 -8.272 1 -1 171 186 - 25 5.281 8.136 5 -5 177 192 - 26 1.996 7.901 2 -2 280 295 - 27 -0.892 -6.621 6 -6 72 87 - 28 -6.122 6.475 3 -3 0 1440 - 29 -6.559 6.836 2 -2 0 1440 - 30 5.046 -2.387 4 -4 0 1440 - 31 7.233 -7.268 5 -5 0 1440 - 32 -1.851 0.157 4 -4 0 1440 - 33 -8.842 -1.310 6 -6 0 1440 - 34 -6.685 -0.037 1 -1 0 1440 - 35 -4.591 -8.809 4 -4 0 1440 - 36 -0.509 1.078 4 -4 0 1440 - 37 0.000 0.000 0 0 0 360 diff --git a/res/datasets/cordeau-laporte/b4-16.txt b/res/datasets/cordeau-laporte/b4-16.txt deleted file mode 100644 index fa77e42b..00000000 --- a/res/datasets/cordeau-laporte/b4-16.txt +++ /dev/null @@ -1,35 +0,0 @@ -4 16 240 6 45 - 0 0.000 0.000 0 0 0 240 - 1 -6.927 -7.741 5 5 0 1440 - 2 -5.327 1.806 3 3 0 1440 - 3 8.362 -3.341 2 2 0 1440 - 4 7.616 3.165 4 4 0 1440 - 5 -5.435 1.997 6 6 0 1440 - 6 -2.339 1.347 5 5 0 1440 - 7 8.998 9.829 4 4 0 1440 - 8 7.884 0.580 1 1 0 1440 - 9 0.420 1.585 6 6 41 56 - 10 -5.398 -7.231 3 3 1 16 - 11 6.002 -9.365 3 3 143 158 - 12 3.413 5.970 6 6 79 94 - 13 9.887 -9.844 6 6 26 41 - 14 -4.903 -6.750 5 5 26 41 - 15 -8.680 -2.884 5 5 150 165 - 16 -5.745 3.917 6 6 9 24 - 17 1.944 4.861 5 -5 126 141 - 18 3.765 2.908 3 -3 108 123 - 19 -6.065 1.465 2 -2 97 112 - 20 6.955 9.295 4 -4 54 69 - 21 8.131 -4.645 6 -6 219 234 - 22 -9.531 -0.080 5 -5 99 114 - 23 -9.996 0.804 4 -4 104 119 - 24 1.274 -5.458 1 -1 176 191 - 25 -9.645 -6.415 6 -6 0 1440 - 26 -5.784 6.599 3 -3 0 1440 - 27 7.231 -2.651 3 -3 0 1440 - 28 -3.850 3.242 6 -6 0 1440 - 29 4.837 0.467 6 -6 0 1440 - 30 -0.200 6.682 5 -5 0 1440 - 31 7.987 -5.911 5 -5 0 1440 - 32 0.588 -5.109 6 -6 0 1440 - 33 0.000 0.000 0 0 0 240 diff --git a/res/datasets/cordeau-laporte/b4-24.txt b/res/datasets/cordeau-laporte/b4-24.txt deleted file mode 100644 index 515a5f33..00000000 --- a/res/datasets/cordeau-laporte/b4-24.txt +++ /dev/null @@ -1,51 +0,0 @@ -4 24 360 6 45 - 0 0.000 0.000 0 0 0 360 - 1 4.413 -3.761 4 4 0 1440 - 2 8.051 -5.674 5 5 0 1440 - 3 -1.154 8.774 4 4 0 1440 - 4 5.763 0.115 2 2 0 1440 - 5 -9.108 -3.534 6 6 0 1440 - 6 5.606 -0.973 5 5 0 1440 - 7 -0.809 -5.812 4 4 0 1440 - 8 1.430 -2.964 4 4 0 1440 - 9 -3.821 -4.910 3 3 0 1440 - 10 -1.002 -0.660 1 1 0 1440 - 11 5.065 -7.094 6 6 0 1440 - 12 -6.793 0.981 1 1 0 1440 - 13 -3.135 -8.580 1 1 182 197 - 14 1.831 8.740 2 2 119 134 - 15 -4.175 8.086 3 3 277 292 - 16 -5.919 4.471 3 3 184 199 - 17 -5.096 9.342 6 6 298 313 - 18 7.663 1.869 6 6 210 225 - 19 1.280 -7.585 3 3 106 121 - 20 7.987 -9.275 3 3 147 162 - 21 4.548 4.026 1 1 34 49 - 22 -8.670 3.785 1 1 189 204 - 23 -9.438 8.405 6 6 270 285 - 24 -3.516 -0.477 6 6 268 283 - 25 -0.574 -8.977 4 -4 155 170 - 26 -4.927 7.733 5 -5 313 328 - 27 -2.434 1.137 4 -4 284 299 - 28 -2.839 -7.315 2 -2 235 250 - 29 -9.564 0.271 6 -6 339 354 - 30 3.989 -8.155 5 -5 331 346 - 31 3.063 3.517 4 -4 337 352 - 32 9.642 0.205 4 -4 313 328 - 33 -3.458 6.294 3 -3 178 193 - 34 0.119 5.464 1 -1 178 193 - 35 5.995 -5.907 6 -6 313 328 - 36 -2.969 -2.605 1 -1 71 86 - 37 1.979 3.901 1 -1 0 1440 - 38 -1.638 6.921 2 -2 0 1440 - 39 3.654 5.164 3 -3 0 1440 - 40 -5.817 -3.014 3 -3 0 1440 - 41 1.483 5.885 6 -6 0 1440 - 42 -4.258 9.082 6 -6 0 1440 - 43 1.474 0.020 3 -3 0 1440 - 44 4.220 6.072 3 -3 0 1440 - 45 3.465 -0.981 1 -1 0 1440 - 46 -4.644 -9.328 1 -1 0 1440 - 47 8.904 2.431 6 -6 0 1440 - 48 -0.733 -1.101 6 -6 0 1440 - 49 0.000 0.000 0 0 0 360 diff --git a/setup.cfg b/setup.cfg index 7865d75d..bca7b8de 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,7 +6,9 @@ filename = ./tests/**/*.py per-file-ignores = ./**/__init__.py:F401 - +ignore = + E203 + W503 [coverage:run] source = jinete