Skip to content

Commit

Permalink
Merge pull request #83 from BrunoRosendo/feature/additional-metrics
Browse files Browse the repository at this point in the history
Feature/additional metrics
  • Loading branch information
BrunoRosendo authored Apr 19, 2024
2 parents f3dbadc + 877f1bb commit 2dea482
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 18 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
*.out
__pycache__/
.idea/
results/
results/json/*
results/html/*
.env
5 changes: 2 additions & 3 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from dotenv import load_dotenv

from src.solver.qubo.CplexSolver import CplexSolver, get_backend_sampler
from src.solver.qubo.CplexSolver import CplexSolver

if __name__ == "__main__":
load_dotenv()
Expand All @@ -16,7 +16,6 @@
(0, 1, 6),
],
True,
sampler=get_backend_sampler(),
)
result = cvrp.solve()
result.display()
result.print()
75 changes: 73 additions & 2 deletions src/model/VRPSolution.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
from __future__ import annotations

import json
from pathlib import Path

import plotly.graph_objects as go


Expand All @@ -17,6 +22,9 @@ class VRPSolution:
- loads (list of lists): The load of each vehicle at each location of its route.
- depot (int): The index of the depot location.
- use_capacity (bool): Whether the solution uses vehicle capacity or not.
- run_time (float): The total runtime of the solver.
- qpu_access_time (float): The total runtime of the QPU.
- local_run_time (float): The total runtime of the local machine, including pre-processing and queues.
"""

COLOR_LIST = [
Expand All @@ -34,6 +42,8 @@ class VRPSolution:
"magenta",
]

RESULTS_PATH = "results"

def __init__(
self,
num_vehicles: int,
Expand All @@ -46,6 +56,9 @@ def __init__(
capacities: int | list[int] = None,
loads: list[list[int]] = None,
use_depot: bool = None,
run_time: float = None,
qpu_access_time: float = None,
local_run_time: float = None,
):
self.num_vehicles = num_vehicles
self.locations = locations
Expand All @@ -55,6 +68,9 @@ def __init__(
self.distances = distances
self.loads = loads
self.depot = depot
self.run_time = run_time
self.qpu_access_time = qpu_access_time
self.local_run_time = local_run_time

if use_depot is None:
self.use_depot = depot is not None
Expand All @@ -64,10 +80,12 @@ def __init__(
self.capacities = capacities
self.use_capacity = loads is not None and capacities is not None

def display(self):
def display(self, file_name: str = None, results_path: str = RESULTS_PATH):
"""
Display the solution using a plotly figure.
Saves the figure to an HTML file if a file name is provided.
"""

fig = go.Figure()

for vehicle_id in range(self.num_vehicles):
Expand Down Expand Up @@ -142,6 +160,11 @@ def display(self):

fig.show()

if file_name is not None:
html_path = f"{results_path}/html"
Path(html_path).mkdir(parents=True, exist_ok=True)
fig.write_html(f"{html_path}/{file_name}.html")

def plot_direction(self, fig, loc1, loc2, color, line_width, legend_group=None):
"""
Plot an arrow representing the direction from coord1 to coord2 with the given color and line width.
Expand Down Expand Up @@ -198,7 +221,19 @@ def plot_location(

def print(self):
"""Print the solution to the console."""
print(f"Objective: {self.objective}\n")

print()

if self.run_time is not None:
print(f"Solver runtime: {self.run_time}µs")

if self.qpu_access_time is not None:
print(f"QPU access time: {self.qpu_access_time}µs")

if self.local_run_time is not None:
print(f"Local runtime: {self.local_run_time}µs")

print(f"\nObjective: {self.objective}\n")

for vehicle_id in range(self.num_vehicles):
print(f"Route for vehicle {vehicle_id}:")
Expand All @@ -215,3 +250,39 @@ def print(self):
print(f"\nDistance of the route: {self.distances[vehicle_id]}m\n")

print(f"Total distance of all routes: {self.total_distance}m")

def save_json(self, file_name: str, results_path: str = RESULTS_PATH):
"""
Save the solution to a JSON file.
"""

json_path = f"{results_path}/json"
Path(json_path).mkdir(parents=True, exist_ok=True)

with open(f"{json_path}/{file_name}.json", "w") as file:
json.dump(self, file, default=lambda o: o.__dict__, indent=4)

@staticmethod
def from_json(file_name: str, results_path: str = RESULTS_PATH) -> VRPSolution:
"""
Load a solution from a JSON file.
"""

with open(f"{results_path}/json/{file_name}.json", "r") as file:
data = json.load(file)

return VRPSolution(
data["num_vehicles"],
[(loc[0], loc[1]) for loc in data["locations"]],
data["objective"],
data["total_distance"],
data["routes"],
data["distances"],
data["depot"],
data["capacities"],
data["loads"],
data["use_depot"],
data["run_time"],
data["qpu_access_time"],
data["local_run_time"],
)
10 changes: 9 additions & 1 deletion src/model/qubo/QuboVRP.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,12 @@ def get_var(
return round(var_dict[var_name])

def convert_result(
self, var_dict: dict[str, float], objective: float
self,
var_dict: dict[str, float],
objective: float,
run_time: float,
local_run_time: float,
qpu_access_time: float = None,
) -> VRPSolution:
"""
Convert the final variables into a VRPSolution result.
Expand Down Expand Up @@ -184,6 +189,9 @@ def convert_result(
self.depot,
self.get_capacity(),
loads if self.get_capacity() else None,
run_time=run_time,
qpu_access_time=qpu_access_time,
local_run_time=local_run_time,
)

def get_capacity(self) -> int | list[int] | None:
Expand Down
9 changes: 8 additions & 1 deletion src/solver/ClassicSolver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import time

from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2

Expand Down Expand Up @@ -69,7 +71,10 @@ def _solve_cvrp(self) -> any:
self.set_pickup_and_deliveries()

search_parameters = self.get_search_parameters()

start_time = time.perf_counter_ns()
or_solution = self.routing.SolveWithParameters(search_parameters)
self.run_time = (time.perf_counter_ns() - start_time) // 1000

if or_solution is None:
raise Exception("The solution is infeasible, aborting!")
Expand Down Expand Up @@ -197,7 +202,7 @@ def remove_unused_locations(
for trip in trips
]

def _convert_solution(self, result: any) -> VRPSolution:
def _convert_solution(self, result: any, local_run_time: float) -> VRPSolution:
"""Converts OR-Tools result to CVRP solution."""

routes = []
Expand Down Expand Up @@ -249,6 +254,8 @@ def _convert_solution(self, result: any) -> VRPSolution:
self.capacities,
loads if self.use_capacity else None,
not self.use_rpp,
run_time=self.run_time,
local_run_time=local_run_time,
)

def get_model(self) -> VRP:
Expand Down
13 changes: 11 additions & 2 deletions src/solver/VRPSolver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
from abc import ABC, abstractmethod

from src.model.VRP import VRP
Expand All @@ -22,6 +23,7 @@ class VRPSolver(ABC):
track_progress (bool): Whether to track the progress of the solver or not.
simplify (bool): Whether to simplify the problem by removing unnecessary variables.
model (QuboVRP): VRP instance of the model.
run_time (int): Time taken to run the solver (measured locally).
"""

def __init__(
Expand Down Expand Up @@ -52,6 +54,7 @@ def __init__(
self.use_rpp = use_rpp
self.track_progress = track_progress
self.simplify = simplify
self.run_time: int | None = None
self.distance_matrix = self.compute_distance()
self.model = self.get_model()

Expand Down Expand Up @@ -79,7 +82,7 @@ def _solve_cvrp(self) -> any:
pass

@abstractmethod
def _convert_solution(self, result: any) -> VRPSolution:
def _convert_solution(self, result: any, local_run_time: float) -> VRPSolution:
"""
Convert the result from the solver to a CVRP solution.
"""
Expand All @@ -89,8 +92,14 @@ def solve(self) -> VRPSolution:
"""
Solve the CVRP.
"""

start_time = time.perf_counter_ns()
result = self._solve_cvrp()
return self._convert_solution(result)
execution_time = (
time.perf_counter_ns() - start_time
) // 1000 # Convert to microseconds

return self._convert_solution(result, execution_time)

@abstractmethod
def get_model(self) -> VRP:
Expand Down
16 changes: 14 additions & 2 deletions src/solver/qubo/CplexSolver.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import time

from docplex.util.status import JobSolveStatus
from numpy import ndarray
Expand Down Expand Up @@ -125,7 +126,11 @@ def solve_classic(self, qp: QuadraticProgram) -> OptimizationResult:
"""

optimizer = CplexOptimizer(disp=self.track_progress)

start_time = time.perf_counter_ns()
result = optimizer.solve(qp)
self.run_time = (time.perf_counter_ns() - start_time) // 1000

return result

def solve_qubo(self, qp: QuadraticProgram) -> OptimizationResult:
Expand All @@ -146,7 +151,10 @@ def solve_qubo(self, qp: QuadraticProgram) -> OptimizationResult:
else:
optimizer = MinimumEigenOptimizer(qaoa)

start_time = time.perf_counter_ns()
result = optimizer.solve(qp)
self.run_time = (time.perf_counter_ns() - start_time) // 1000

return result

def qaoa_callback(
Expand Down Expand Up @@ -180,12 +188,16 @@ def check_feasibility(self, result: OptimizationResult):
if not self.model.is_result_feasible(self.var_dict):
raise Exception("The solution is infeasible, aborting!")

def _convert_solution(self, result: OptimizationResult) -> VRPSolution:
def _convert_solution(
self, result: OptimizationResult, local_run_time: int
) -> VRPSolution:
"""
Convert the optimizer result to a VRPSolution result.
"""

return self.model.convert_result(self.var_dict, result.fval)
return self.model.convert_result(
self.var_dict, result.fval, self.run_time, local_run_time
)

def build_var_dict(self, result: OptimizationResult) -> dict[str, float]:
"""
Expand Down
32 changes: 26 additions & 6 deletions src/solver/qubo/DWaveSolver.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import time
from logging import warning

from dimod import (
Expand Down Expand Up @@ -84,17 +85,27 @@ def _solve_cvrp(self) -> SampleSet:

if self.use_bqm:
result, self.invert = self.sample_as_bqm(self.cqm)
return result
else:
result = self.sample_cqm()

return self.sample_cqm()
return result

def _convert_solution(self, result: SampleSet) -> VRPSolution:
def _convert_solution(
self, result: SampleSet, local_run_time: float
) -> VRPSolution:
"""
Convert the optimizer result to a VRPSolution result.
"""

var_dict, energy = self.build_var_dict(result)
return self.model.convert_result(var_dict, energy)
timing = result.info.get("timing") or {}
return self.model.convert_result(
var_dict,
energy,
timing.get("run_time") or timing.get("qpu_access_time") or self.run_time,
local_run_time,
timing.get("qpu_access_time"),
)

def build_var_dict(self, result: SampleSet) -> (dict[str, float], float):
"""
Expand Down Expand Up @@ -167,7 +178,12 @@ def sample_cqm(self) -> SampleSet:
Sample the CQM using the selected sampler and time limit.
"""
kwargs = {"time_limit": self.time_limit} if self.time_limit else {}
return self.sampler.sample_cqm(self.cqm, **kwargs)

start_time = time.perf_counter_ns()
result = self.sampler.sample_cqm(self.cqm, **kwargs)
self.run_time = (time.perf_counter_ns() - start_time) // 1000

return result

def sample_bqm(
self, bqm: BinaryQuadraticModel, sampler: Sampler = None
Expand All @@ -181,4 +197,8 @@ def sample_bqm(
if self.time_limit:
kwargs["time_limit"] = self.time_limit

return sampler.sample(bqm, **kwargs)
start_time = time.perf_counter_ns()
result = sampler.sample(bqm, **kwargs)
self.run_time = (time.perf_counter_ns() - start_time) // 1000

return result

0 comments on commit 2dea482

Please sign in to comment.