From 069f8c99d186e012249224c22625b0e311546cef Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 20:35:50 -0500 Subject: [PATCH 01/13] Cython wrapper --- .gitignore | 3 + hygese/__init__.py | 2 +- hygese/hygese.py | 352 -------------------------------------- hygese/solver.py | 149 ++++++++++++++++ hygese/tests/test_cvrp.py | 6 + hygese/tests/test_tsp.py | 7 + hygese/wrapper.pyx | 202 ++++++++++++++++++++++ pyproject.toml | 3 +- setup.py | 54 +++--- 9 files changed, 391 insertions(+), 387 deletions(-) delete mode 100644 hygese/hygese.py create mode 100644 hygese/solver.py create mode 100644 hygese/wrapper.pyx diff --git a/.gitignore b/.gitignore index 6342ad2..1cb66d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +deps/ +wrapper.c + build_tmp/ .idea *.dylib diff --git a/hygese/__init__.py b/hygese/__init__.py index 759ab99..cc2927c 100644 --- a/hygese/__init__.py +++ b/hygese/__init__.py @@ -1 +1 @@ -from .hygese import * \ No newline at end of file +from hygese.solver import AlgorithmParameters, Solver diff --git a/hygese/hygese.py b/hygese/hygese.py deleted file mode 100644 index 53c763a..0000000 --- a/hygese/hygese.py +++ /dev/null @@ -1,352 +0,0 @@ -import os -import platform -from ctypes import ( - Structure, - CDLL, - POINTER, - c_int, - c_double, - c_char, - sizeof, - cast, - byref, -) -from dataclasses import dataclass -import numpy as np -import sys - - -def get_lib_filename(): - if platform.system() == "Linux": - lib_ext = "so" - elif platform.system() == "Darwin": - lib_ext = "dylib" - elif platform.system() == "Windows": - lib_ext = "dll" - else: - lib_ext = "so" - return f"libhgscvrp.{lib_ext}" - - -# basedir = os.path.abspath(os.path.dirname(__file__)) -basedir = os.path.dirname(os.path.realpath(__file__)) -# os.add_dll_directory(basedir) -HGS_LIBRARY_FILEPATH = os.path.join(basedir, get_lib_filename()) - -c_double_p = POINTER(c_double) -c_int_p = POINTER(c_int) -C_INT_MAX = 2 ** (sizeof(c_int) * 8 - 1) - 1 -C_DBL_MAX = sys.float_info.max - - -# Must match with AlgorithmParameters.h in HGS-CVRP: https://github.com/vidalt/HGS-CVRP -class CAlgorithmParameters(Structure): - _fields_ = [ - ("nbGranular", c_int), - ("mu", c_int), - ("lambda", c_int), - ("nbElite", c_int), - ("nbClose", c_int), - ("targetFeasible", c_double), - ("seed", c_int), - ("nbIter", c_int), - ("timeLimit", c_double), - ("useSwapStar", c_int), - ] - - -@dataclass -class AlgorithmParameters: - nbGranular: int = 20 - mu: int = 25 - lambda_: int = 40 - nbElite: int = 4 - nbClose: int = 5 - targetFeasible: float = 0.2 - seed: int = 0 - nbIter: int = 20000 - timeLimit: float = 0.0 - useSwapStar: bool = True - - @property - def ctypes(self) -> CAlgorithmParameters: - return CAlgorithmParameters( - self.nbGranular, - self.mu, - self.lambda_, - self.nbElite, - self.nbClose, - self.targetFeasible, - self.seed, - self.nbIter, - self.timeLimit, - int(self.useSwapStar), - ) - - -class _SolutionRoute(Structure): - _fields_ = [("length", c_int), ("path", c_int_p)] - - -class _Solution(Structure): - _fields_ = [ - ("cost", c_double), - ("time", c_double), - ("n_routes", c_int), - ("routes", POINTER(_SolutionRoute)), - ] - - -class RoutingSolution: - def __init__(self, sol_ptr): - if not sol_ptr: - raise TypeError("The solution pointer is null.") - - self.cost = sol_ptr[0].cost - self.time = sol_ptr[0].time - self.n_routes = sol_ptr[0].n_routes - self.routes = [] - for i in range(self.n_routes): - r = sol_ptr[0].routes[i] - path = r.path[0 : r.length] - self.routes.append(path) - - -class Solver: - def __init__(self, parameters=AlgorithmParameters(), verbose=True): - if platform.system() == "Windows": - hgs_library = CDLL(HGS_LIBRARY_FILEPATH, winmode=0) - else: - hgs_library = CDLL(HGS_LIBRARY_FILEPATH) - - self.algorithm_parameters = parameters - self.verbose = verbose - - # solve_cvrp - self._c_api_solve_cvrp = hgs_library.solve_cvrp - self._c_api_solve_cvrp.argtypes = [ - c_int, - c_double_p, - c_double_p, - c_double_p, - c_double_p, - c_double, - c_double, - c_char, - c_char, - c_int, - POINTER(CAlgorithmParameters), - c_char, - ] - self._c_api_solve_cvrp.restype = POINTER(_Solution) - - # solve_cvrp_dist_mtx - self._c_api_solve_cvrp_dist_mtx = hgs_library.solve_cvrp_dist_mtx - self._c_api_solve_cvrp_dist_mtx.argtypes = [ - c_int, - c_double_p, - c_double_p, - c_double_p, - c_double_p, - c_double_p, - c_double, - c_double, - c_char, - c_int, - POINTER(CAlgorithmParameters), - c_char, - ] - self._c_api_solve_cvrp_dist_mtx.restype = POINTER(_Solution) - - # delete_solution - self._c_api_delete_sol = hgs_library.delete_solution - self._c_api_delete_sol.restype = None - self._c_api_delete_sol.argtypes = [POINTER(_Solution)] - - def solve_cvrp(self, data, rounding=True): - # required data - demand = np.asarray(data["demands"]) - vehicle_capacity = data["vehicle_capacity"] - n_nodes = len(demand) - - # optional depot - depot = data.get("depot", 0) - if depot != 0: - raise ValueError("In HGS, the depot location must be 0.") - - # optional num_vehicles - maximum_number_of_vehicles = data.get("num_vehicles", C_INT_MAX) - - # optional service_times - service_times = data.get("service_times") - if service_times is None: - service_times = np.zeros(n_nodes) - else: - service_times = np.asarray(service_times) - - # optional duration_limit - duration_limit = data.get("duration_limit") - if duration_limit is None: - is_duration_constraint = False - duration_limit = C_DBL_MAX - else: - is_duration_constraint = True - - is_rounding_integer = rounding - - x_coords = data.get("x_coordinates") - y_coords = data.get("y_coordinates") - dist_mtx = data.get("distance_matrix") - - if x_coords is None or y_coords is None: - assert dist_mtx is not None - x_coords = np.zeros(n_nodes) - y_coords = np.zeros(n_nodes) - else: - x_coords = np.asarray(x_coords) - y_coords = np.asarray(y_coords) - - assert len(x_coords) == len(y_coords) == len(service_times) == len(demand) - assert (x_coords >= 0.0).all() - assert (y_coords >= 0.0).all() - assert (service_times >= 0.0).all() - assert (demand >= 0.0).all() - - if dist_mtx is not None: - dist_mtx = np.asarray(dist_mtx) - assert dist_mtx.shape[0] == dist_mtx.shape[1] - assert (dist_mtx >= 0.0).all() - return self._solve_cvrp_dist_mtx( - x_coords, - y_coords, - dist_mtx, - service_times, - demand, - vehicle_capacity, - duration_limit, - is_duration_constraint, - maximum_number_of_vehicles, - self.algorithm_parameters, - self.verbose, - ) - else: - return self._solve_cvrp( - x_coords, - y_coords, - service_times, - demand, - vehicle_capacity, - duration_limit, - is_rounding_integer, - is_duration_constraint, - maximum_number_of_vehicles, - self.algorithm_parameters, - self.verbose, - ) - - def solve_tsp(self, data, rounding=True): - x_coords = data.get("x_coordinates") - dist_mtx = data.get("distance_matrix") - if dist_mtx is None: - n_nodes = x_coords.size - else: - dist_mtx = np.asarray(dist_mtx) - n_nodes = dist_mtx.shape[0] - - data["num_vehicles"] = 1 - data["depot"] = 0 - data["demands"] = np.ones(n_nodes) - data["vehicle_capacity"] = n_nodes - - return self.solve_cvrp(data, rounding=rounding) - - def _solve_cvrp( - self, - x_coords: np.ndarray, - y_coords: np.ndarray, - service_times: np.ndarray, - demand: np.ndarray, - vehicle_capacity: int, - duration_limit: float, - is_rounding_integer: bool, - is_duration_constraint: bool, - maximum_number_of_vehicles: int, - algorithm_parameters: AlgorithmParameters, - verbose: bool, - ): - n_nodes = x_coords.size - x_ct = x_coords.astype(c_double).ctypes - y_ct = y_coords.astype(c_double).ctypes - s_ct = service_times.astype(c_double).ctypes - d_ct = demand.astype(c_double).ctypes - ap_ct = algorithm_parameters.ctypes - - # struct Solution * solve_cvrp( - # int n, double* x, double* y, double* serv_time, double* dem, - # double vehicleCapacity, double durationLimit, char isRoundingInteger, char isDurationConstraint, - # int max_nbVeh, const struct AlgorithmParameters* ap, char verbose); - sol_p = self._c_api_solve_cvrp( - n_nodes, - cast(x_ct, c_double_p), - cast(y_ct, c_double_p), - cast(s_ct, c_double_p), - cast(d_ct, c_double_p), - vehicle_capacity, - duration_limit, - is_rounding_integer, - is_duration_constraint, - maximum_number_of_vehicles, - byref(ap_ct), - verbose, - ) - - result = RoutingSolution(sol_p) - self._c_api_delete_sol(sol_p) - return result - - def _solve_cvrp_dist_mtx( - self, - x_coords: np.ndarray, - y_coords: np.ndarray, - dist_mtx: np.ndarray, - service_times: np.ndarray, - demand: np.ndarray, - vehicle_capacity: int, - duration_limit: float, - is_duration_constraint: bool, - maximum_number_of_vehicles: int, - algorithm_parameters: AlgorithmParameters, - verbose: bool, - ): - n_nodes = x_coords.size - - x_ct = x_coords.astype(c_double).ctypes - y_ct = y_coords.astype(c_double).ctypes - s_ct = service_times.astype(c_double).ctypes - d_ct = demand.astype(c_double).ctypes - - m_ct = dist_mtx.reshape(n_nodes * n_nodes).astype(c_double).ctypes - ap_ct = algorithm_parameters.ctypes - - # struct Solution *solve_cvrp_dist_mtx( - # int n, double* x, double* y, double *dist_mtx, double *serv_time, double *dem, - # double vehicleCapacity, double durationLimit, char isDurationConstraint, - # int max_nbVeh, const struct AlgorithmParameters *ap, char verbose); - sol_p = self._c_api_solve_cvrp_dist_mtx( - n_nodes, - cast(x_ct, c_double_p), - cast(y_ct, c_double_p), - cast(m_ct, c_double_p), - cast(s_ct, c_double_p), - cast(d_ct, c_double_p), - vehicle_capacity, - duration_limit, - is_duration_constraint, - maximum_number_of_vehicles, - byref(ap_ct), - verbose, - ) - - result = RoutingSolution(sol_p) - self._c_api_delete_sol(sol_p) - return result diff --git a/hygese/solver.py b/hygese/solver.py new file mode 100644 index 0000000..f5bcf2c --- /dev/null +++ b/hygese/solver.py @@ -0,0 +1,149 @@ +from hygese.wrapper import _solve_cvrp_dist_mtx, _solve_cvrp, RoutingSolution +from dataclasses import dataclass +import numpy as np + +from ctypes import c_int, sizeof +import sys + +C_INT_MAX = 2 ** (sizeof(c_int) * 8 - 1) - 1 +C_DBL_MAX = sys.float_info.max + + +@dataclass +class AlgorithmParameters: + nbGranular: int = 20 + mu: int = 25 + lambda_: int = 40 + nbElite: int = 4 + nbClose: int = 5 + targetFeasible: float = 0.2 + seed: int = 0 + nbIter: int = 20000 + timeLimit: float = 0.0 + useSwapStar: bool = True + + +class Solver: + def __init__(self, parameters=AlgorithmParameters(), verbose=True): + self.algorithm_parameters = parameters + self.verbose = verbose + + def solve_cvrp(self, data, rounding=True): + # required data + demand = np.asarray(data["demands"]).astype(np.float64) + vehicle_capacity = data["vehicle_capacity"] + n_nodes = len(demand) + + # optional depot + depot = data.get("depot", 0) + if depot != 0: + raise ValueError("In HGS, the depot location must be 0.") + + # optional num_vehicles + maximum_number_of_vehicles = data.get("num_vehicles", C_INT_MAX) + + # optional service_times + service_times = data.get("service_times") + if service_times is None: + service_times = np.zeros(n_nodes).astype(np.float64) + else: + service_times = np.asarray(service_times).astype(np.float64) + + # optional duration_limit + duration_limit = data.get("duration_limit") + if duration_limit is None: + is_duration_constraint = False + duration_limit = C_DBL_MAX + else: + is_duration_constraint = True + + is_rounding_integer = rounding + + x_coords = data.get("x_coordinates") + y_coords = data.get("y_coordinates") + dist_mtx = data.get("distance_matrix") + + if x_coords is None or y_coords is None: + assert dist_mtx is not None + x_coords = np.zeros(n_nodes).astype(np.float64) + y_coords = np.zeros(n_nodes).astype(np.float64) + else: + x_coords = np.asarray(x_coords).astype(np.float64) + y_coords = np.asarray(y_coords).astype(np.float64) + + assert len(x_coords) == len(y_coords) == len(service_times) == len(demand) + assert (x_coords >= 0.0).all() + assert (y_coords >= 0.0).all() + assert (service_times >= 0.0).all() + assert (demand >= 0.0).all() + + if dist_mtx is not None: + dist_mtx = np.asarray(dist_mtx).astype(np.float64) + assert dist_mtx.shape[0] == dist_mtx.shape[1] + assert (dist_mtx >= 0.0).all() + + dist_mtx = dist_mtx.reshape((n_nodes * n_nodes, )) + + print("shape = " , dist_mtx.shape) + + return _solve_cvrp_dist_mtx( + x_coords, + y_coords, + dist_mtx, + service_times, + demand, + vehicle_capacity, + duration_limit, + is_duration_constraint, + maximum_number_of_vehicles, + self.algorithm_parameters.nbGranular, + self.algorithm_parameters.mu, + self.algorithm_parameters.lambda_, + self.algorithm_parameters.nbElite, + self.algorithm_parameters.nbClose, + self.algorithm_parameters.targetFeasible, + self.algorithm_parameters.seed, + self.algorithm_parameters.nbIter, + self.algorithm_parameters.timeLimit, + self.algorithm_parameters.useSwapStar, + self.verbose, + ) + else: + return _solve_cvrp( + x_coords, + y_coords, + service_times, + demand, + vehicle_capacity, + duration_limit, + is_rounding_integer, + is_duration_constraint, + maximum_number_of_vehicles, + self.algorithm_parameters.nbGranular, + self.algorithm_parameters.mu, + self.algorithm_parameters.lambda_, + self.algorithm_parameters.nbElite, + self.algorithm_parameters.nbClose, + self.algorithm_parameters.targetFeasible, + self.algorithm_parameters.seed, + self.algorithm_parameters.nbIter, + self.algorithm_parameters.timeLimit, + self.algorithm_parameters.useSwapStar, + self.verbose, + ) + + def solve_tsp(self, data, rounding=True): + x_coords = data.get("x_coordinates") + dist_mtx = data.get("distance_matrix") + if dist_mtx is None: + n_nodes = x_coords.size + else: + dist_mtx = np.asarray(dist_mtx) + n_nodes = dist_mtx.shape[0] + + data["num_vehicles"] = 1 + data["depot"] = 0 + data["demands"] = np.ones(n_nodes) + data["vehicle_capacity"] = n_nodes + + return self.solve_cvrp(data, rounding=rounding) diff --git a/hygese/tests/test_cvrp.py b/hygese/tests/test_cvrp.py index 105ea1d..3f950ec 100644 --- a/hygese/tests/test_cvrp.py +++ b/hygese/tests/test_cvrp.py @@ -107,3 +107,9 @@ def test_cvrp_duration(): result = hgs_solver.solve_cvrp(data, rounding=True) assert result.cost == 42 + +if __name__ == "__main__": + test_cvrp() + test_cvrp_inputs() + test_cvrp_dist_mtx() + test_cvrp_duration() diff --git a/hygese/tests/test_tsp.py b/hygese/tests/test_tsp.py index 7448fd9..d9e11e0 100644 --- a/hygese/tests/test_tsp.py +++ b/hygese/tests/test_tsp.py @@ -63,3 +63,10 @@ def test_tsp(): # cost += dist_mtx_int[route[n-1], route[0]] # # assert result.cost == cost + + +if __name__ == "__main__": + test_tsp() + # test_elkai() + + \ No newline at end of file diff --git a/hygese/wrapper.pyx b/hygese/wrapper.pyx new file mode 100644 index 0000000..579c776 --- /dev/null +++ b/hygese/wrapper.pyx @@ -0,0 +1,202 @@ +cdef extern from "AlgorithmParameters.h": + cdef struct AlgorithmParameters: + int nbGranular + int mu + int lambda_ "lambda" + int nbElite + int nbClose + double targetFeasible + int seed + int nbIter + double timeLimit + int useSwapStar + +cdef extern from "C_Interface.h": + cdef struct SolutionRoute: + int length + int *path + + cdef struct Solution: + double cost + double time + int n_routes + SolutionRoute *routes + + # Define the C functions in Cython syntax + Solution* solve_cvrp(int n, double* x, double* y, double* serv_time, double* dem, + double vehicleCapacity, double durationLimit, char isRoundingInteger, char isDurationConstraint, + int max_nbVeh, const AlgorithmParameters* ap, char verbose) + + Solution* solve_cvrp_dist_mtx(int n, double* x, double* y, double *dist_mtx, double *serv_time, double *dem, + double vehicleCapacity, double durationLimit, char isDurationConstraint, + int max_nbVeh, const AlgorithmParameters *ap, char verbose) + + + void delete_solution(Solution * sol) + + +class RoutingSolution: + def __init__(self, cost, time, n_routes, routes): + self.cost = cost + self.time = time + self.n_routes = n_routes + self.routes = routes + +cdef get_solution_py(Solution *sol_c): + cdef int i, j + routes_py = [] + for i in range(sol_c.n_routes): + route_py = {} + route_py['length'] = sol_c.routes[i].length + route_py['path'] = [sol_c.routes[i].path[j] for j in range(sol_c.routes[i].length)] + routes_py.append(route_py) + + sol_py = RoutingSolution(sol_c.cost, sol_c.time, sol_c.n_routes, routes_py) + return sol_py + +# cdef get_algorithm_parameters( +# int nbGranular, +# int mu, +# int lambda_, +# int nbElite, +# int nbClose, +# double targetFeasible, +# int seed, +# int nbIter, +# double timeLimit, +# int useSwapStar, +# ): +# # Create an instance of AlgorithmParameters C struct and set its values +# cdef AlgorithmParameters ap + +# ap.nbGranular = nbGranular +# ap.mu = mu +# ap.lambda_ = lambda_ +# ap.nbElite = nbElite +# ap.nbClose = nbClose +# ap.targetFeasible = targetFeasible +# ap.seed = seed +# ap.nbIter = nbIter +# ap.timeLimit = timeLimit +# ap.useSwapStar = useSwapStar + +# return ap + + + +def _solve_cvrp_dist_mtx( + double[:] x, + double[:] y, + double[:] dist_mtx, + double[:] serv_time, + double[:] dem, + double vehicleCapacity, + double durationLimit, + char isDurationConstraint, + int max_nbVeh, + int nbGranular, + int mu, + int lambda_, + int nbElite, + int nbClose, + double targetFeasible, + int seed, + int nbIter, + double timeLimit, + int useSwapStar, + char verbose +): + + # Create an instance of AlgorithmParameters C struct and set its values + # cdef AlgorithmParameters ap + cdef AlgorithmParameters ap + + ap.nbGranular = nbGranular + ap.mu = mu + ap.lambda_ = lambda_ + ap.nbElite = nbElite + ap.nbClose = nbClose + ap.targetFeasible = targetFeasible + ap.seed = seed + ap.nbIter = nbIter + ap.timeLimit = timeLimit + ap.useSwapStar = useSwapStar + + # ap = get_algorithm_parameters(nbGranular, mu, lambda_, nbElite, nbClose, targetFeasible, + # seed, nbIter, timeLimit, useSwapStar) + + # Convert the Python input arrays to C-compatible arrays + cdef double *x_c = &x[0] + cdef double *y_c = &y[0] + cdef double *dist_mtx_c = &dist_mtx[0] + cdef double *serv_time_c = &serv_time[0] + cdef double *dem_c = &dem[0] + + # Call the C function + cdef Solution *sol_c = solve_cvrp_dist_mtx(len(x), x_c, y_c, dist_mtx_c, serv_time_c, dem_c, vehicleCapacity, durationLimit, + isDurationConstraint, max_nbVeh, &ap, verbose) + + # Convert the C output to Python objects + sol_py = get_solution_py(sol_c) + + # Free the memory allocated in C + delete_solution(sol_c) + + return sol_py + + + +def _solve_cvrp( + double[:] x, + double[:] y, + double[:] serv_time, + double[:] dem, + double vehicleCapacity, + double durationLimit, + char isRoundingInteger, + char isDurationConstraint, + int max_nbVeh, + int nbGranular, + int mu, + int lambda_, + int nbElite, + int nbClose, + double targetFeasible, + int seed, + int nbIter, + double timeLimit, + int useSwapStar, + char verbose +): + + # Create an instance of AlgorithmParameters C struct and set its values + cdef AlgorithmParameters ap + + ap.nbGranular = nbGranular + ap.mu = mu + ap.lambda_ = lambda_ + ap.nbElite = nbElite + ap.nbClose = nbClose + ap.targetFeasible = targetFeasible + ap.seed = seed + ap.nbIter = nbIter + ap.timeLimit = timeLimit + ap.useSwapStar = useSwapStar + + # Convert the Python input arrays to C-compatible arrays + cdef double *x_c = &x[0] + cdef double *y_c = &y[0] + cdef double *serv_time_c = &serv_time[0] + cdef double *dem_c = &dem[0] + + # Call the C function + cdef Solution *sol_c = solve_cvrp(len(x), x_c, y_c, serv_time_c, dem_c, vehicleCapacity, durationLimit, + isRoundingInteger, isDurationConstraint, max_nbVeh, &ap, verbose) + + # Convert the C output to Python objects + sol_py = get_solution_py(sol_c) + + # Free the memory allocated in C + delete_solution(sol_c) + + return sol_py \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 7fd26b9..987e224 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,2 @@ [build-system] -requires = ["setuptools"] -build-backend = "setuptools.build_meta" \ No newline at end of file +requires = ["setuptools", "numpy", "Cython"] \ No newline at end of file diff --git a/setup.py b/setup.py index ab02231..133c0c2 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,10 @@ from setuptools import setup, find_packages from setuptools.command.build_ext import build_ext as _build_ext from setuptools.command.build_py import build_py as _build_py + +from distutils.core import setup, Extension +from Cython.Build import cythonize + import subprocess import os import platform @@ -11,13 +15,6 @@ urlretrieve = urllib.request.urlretrieve -# try: -# import urllib.request -# urlretrieve = urllib.request.urlretrieve -# except ImportError: # python 2 -# from urllib import urlretrieve - - # read the contents of your README file from pathlib import Path @@ -40,9 +37,9 @@ def _safe_makedirs(*paths): HGS_VERSION = "2.0.0" HGS_SRC = f"https://github.com/vidalt/HGS-CVRP/archive/v{HGS_VERSION}.tar.gz" -LIB_DIR = "lib" -BUILD_DIR = "lib/build" -BIN_DIR = "lib/bin" +LIB_DIR = "deps" +BUILD_DIR = "deps/build" +BIN_DIR = "deps/bin" def get_lib_filename(): @@ -76,37 +73,29 @@ def download_build_hgs(): shutil.copyfile(f"{BUILD_DIR}/{LIB_FILENAME}", f"hygese/{LIB_FILENAME}") -# LIB_VERSION = "0.0.1" -# HGS_CVRP_WIN = f"https://github.com/chkwon/Libhgscvrp_jll.jl/releases/download/libhgscvrp-v{LIB_VERSION}%2B0/" + \ -# f"libhgscvrp.v{LIB_VERSION}.x86_64-w64-mingw32-cxx11.tar.gz" - -# def download_binary_hgs(): -# print(HGS_CVRP_WIN) +class BuildPyCommand(_build_py): + def run(self): + download_build_hgs() + _build_py.run(self) -# _safe_makedirs(LIB_DIR) -# dll_tarball_name = "win_bin.tar.gz" -# hgs_bin_path = pjoin(LIB_DIR, dll_tarball_name) -# urlretrieve(HGS_CVRP_WIN, hgs_bin_path) -# _run(f"tar xzvf {dll_tarball_name}", LIB_DIR) -# shutil.copyfile(f"{BIN_DIR}/{LIB_FILENAME}", f"hygese/{LIB_FILENAME}") -class BuildPyCommand(_build_py): - def run(self): - print("Build!!!!!! Run!!!!") +extentions = [ + Extension( + name="hygese.wrapper", + sources=["hygese/wrapper.pyx"], + include_dirs = [f"deps/HGS-CVRP-{HGS_VERSION}/Program/"], + library_dirs = ["hygese"], + libraries = ["hgscvrp"], + ) +] - if platform.system() == "Windows": - # download_binary_hgs() - download_build_hgs() - else: - download_build_hgs() - _build_py.run(self) setup( name="hygese", - version="0.0.0.8", + version="0.0.1.0", description="A Python wrapper for the HGS-CVRP solver", long_description=long_description, long_description_content_type="text/markdown", @@ -127,6 +116,7 @@ def run(self): cmdclass={ "build_py": BuildPyCommand, }, + ext_modules=cythonize(extentions), package_data={ "": ["libhgscvrp.*"], }, From f71860ee37242e6a942cf455bcaecce68d60b489 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 20:40:35 -0500 Subject: [PATCH 02/13] Update setup.py --- setup.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 133c0c2..6d0c523 100644 --- a/setup.py +++ b/setup.py @@ -120,5 +120,8 @@ def run(self): package_data={ "": ["libhgscvrp.*"], }, - install_requires=["numpy"], + install_requires=[ + "Cython>=0.29.0", + "numpy>=1.23.0", + ], ) From e9f4667e37afadc960201393be13071114eb04e2 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 21:53:03 -0500 Subject: [PATCH 03/13] data_files --- hygese/wrapper.pyx | 29 ----------------------------- setup.py | 8 +++++--- 2 files changed, 5 insertions(+), 32 deletions(-) diff --git a/hygese/wrapper.pyx b/hygese/wrapper.pyx index 579c776..f9f5d31 100644 --- a/hygese/wrapper.pyx +++ b/hygese/wrapper.pyx @@ -54,35 +54,6 @@ cdef get_solution_py(Solution *sol_c): sol_py = RoutingSolution(sol_c.cost, sol_c.time, sol_c.n_routes, routes_py) return sol_py -# cdef get_algorithm_parameters( -# int nbGranular, -# int mu, -# int lambda_, -# int nbElite, -# int nbClose, -# double targetFeasible, -# int seed, -# int nbIter, -# double timeLimit, -# int useSwapStar, -# ): -# # Create an instance of AlgorithmParameters C struct and set its values -# cdef AlgorithmParameters ap - -# ap.nbGranular = nbGranular -# ap.mu = mu -# ap.lambda_ = lambda_ -# ap.nbElite = nbElite -# ap.nbClose = nbClose -# ap.targetFeasible = targetFeasible -# ap.seed = seed -# ap.nbIter = nbIter -# ap.timeLimit = timeLimit -# ap.useSwapStar = useSwapStar - -# return ap - - def _solve_cvrp_dist_mtx( double[:] x, diff --git a/setup.py b/setup.py index 6d0c523..65c9281 100644 --- a/setup.py +++ b/setup.py @@ -117,9 +117,11 @@ def run(self): "build_py": BuildPyCommand, }, ext_modules=cythonize(extentions), - package_data={ - "": ["libhgscvrp.*"], - }, + data_files=[("lib", [f"hygese/{LIB_FILENAME}"])], + # include_package_data=True, + # package_data={ + # "": ["libhgscvrp.*"], + # }, install_requires=[ "Cython>=0.29.0", "numpy>=1.23.0", From 8341bbe07a517f6c651f8e37a6acf5b9bd211ac4 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:03:56 -0500 Subject: [PATCH 04/13] Update ci.yml --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57aa066..d2a3d68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: os: [ "ubuntu-latest", "windows-2019", "macos-latest" ] - python-version: [ "3.8", "3.9", "3.10" ] + python-version: [ "3.10" ] steps: - uses: actions/checkout@v2 @@ -43,7 +43,7 @@ jobs: - name: Install and Build run: | pip install . - python setup.py build + # python setup.py build - name: Test with pytest run: | From 260c0b1bad26ce53d74ced44352178faff08ff63 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:10:18 -0500 Subject: [PATCH 05/13] Update ci.yml --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d2a3d68..07a008a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,8 +47,9 @@ jobs: - name: Test with pytest run: | - pip install pytest-cov - pytest -s --cov=hygese --cov-report=xml + # pip install pytest-cov + # pytest -s --cov=hygese --cov-report=xml + python hygese/tests/test_cvrp.py - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v2 + # - name: Upload coverage to Codecov + # uses: codecov/codecov-action@v2 From 432954bea1cde0e6bae637d8e5cf8076bcf32a4b Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:19:03 -0500 Subject: [PATCH 06/13] Update ci.yml --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 07a008a..262ce6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,9 +47,9 @@ jobs: - name: Test with pytest run: | - # pip install pytest-cov - # pytest -s --cov=hygese --cov-report=xml - python hygese/tests/test_cvrp.py + pip install pytest-cov + python -m pytest -s --cov=hygese --cov-report=xml + # python hygese/tests/test_cvrp.py # - name: Upload coverage to Codecov # uses: codecov/codecov-action@v2 From 85814116fda1da1d5bce522b81e7dc11777ff52f Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:21:26 -0500 Subject: [PATCH 07/13] Update solver.py --- hygese/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hygese/solver.py b/hygese/solver.py index f5bcf2c..ac5c3d0 100644 --- a/hygese/solver.py +++ b/hygese/solver.py @@ -1,4 +1,4 @@ -from hygese.wrapper import _solve_cvrp_dist_mtx, _solve_cvrp, RoutingSolution +from .wrapper import _solve_cvrp_dist_mtx, _solve_cvrp, RoutingSolution from dataclasses import dataclass import numpy as np From 8d3fce34070a8679f9a944226f5a12b03be1ac31 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:25:18 -0500 Subject: [PATCH 08/13] Update ci.yml --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 262ce6b..5c41b1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: - name: Install and Build run: | + python setup.py build_ext --inplace pip install . # python setup.py build From e7f4af2fba1b7d924fe7992d3905d38aca564dce Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:26:38 -0500 Subject: [PATCH 09/13] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c41b1e..1756d48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,9 @@ jobs: - name: Install and Build run: | + python setup.py build python setup.py build_ext --inplace pip install . - # python setup.py build - name: Test with pytest run: | From 1fe33534e68c67f2d30739335863f056f9520c10 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 22:38:48 -0500 Subject: [PATCH 10/13] update --- .github/workflows/ci.yml | 4 ++-- hygese/solver.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1756d48..2930af6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,9 +42,9 @@ jobs: - name: Install and Build run: | - python setup.py build + python -m pip install . python setup.py build_ext --inplace - pip install . + python -m pip install . - name: Test with pytest run: | diff --git a/hygese/solver.py b/hygese/solver.py index ac5c3d0..f5bcf2c 100644 --- a/hygese/solver.py +++ b/hygese/solver.py @@ -1,4 +1,4 @@ -from .wrapper import _solve_cvrp_dist_mtx, _solve_cvrp, RoutingSolution +from hygese.wrapper import _solve_cvrp_dist_mtx, _solve_cvrp, RoutingSolution from dataclasses import dataclass import numpy as np From 8aa5fdc701e02805a9456fc5e44d7bf1318431db Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 23:09:58 -0500 Subject: [PATCH 11/13] using static library --- .github/workflows/ci.yml | 3 +-- .gitignore | 1 + setup.py | 31 +++++++++++++++++++++++-------- 3 files changed, 25 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2930af6..d44b668 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,11 +44,10 @@ jobs: run: | python -m pip install . python setup.py build_ext --inplace - python -m pip install . - name: Test with pytest run: | - pip install pytest-cov + python -m pip install pytest-cov python -m pytest -s --cov=hygese --cov-report=xml # python hygese/tests/test_cvrp.py diff --git a/.gitignore b/.gitignore index 1cb66d8..3f6b11e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build_tmp/ *.so *.dll *.dll.a +*.a # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/setup.py b/setup.py index 65c9281..11c7f08 100644 --- a/setup.py +++ b/setup.py @@ -37,7 +37,7 @@ def _safe_makedirs(*paths): HGS_VERSION = "2.0.0" HGS_SRC = f"https://github.com/vidalt/HGS-CVRP/archive/v{HGS_VERSION}.tar.gz" -LIB_DIR = "deps" +DEPS_DIR = "deps" BUILD_DIR = "deps/build" BIN_DIR = "deps/bin" @@ -51,19 +51,32 @@ def get_lib_filename(): lib_ext = "dll" else: lib_ext = "so" + + lib_ext = "a" return f"libhgscvrp.{lib_ext}" LIB_FILENAME = get_lib_filename() +def convert_shared_to_static(): + with open(f"{DEPS_DIR}/HGS-CVRP-{HGS_VERSION}/CMakeLists.txt", "r") as f: + lines = f.read() + + new_lines = lines.replace("SHARED", "STATIC") + + with open(f"{DEPS_DIR}/HGS-CVRP-{HGS_VERSION}/CMakeLists.txt", "w") as f: + f.write(new_lines) + + def download_build_hgs(): - _safe_makedirs(LIB_DIR) + _safe_makedirs(DEPS_DIR) _safe_makedirs(BUILD_DIR) hgs_src_tarball_name = "{}.tar.gz".format(HGS_VERSION) - hgs_src_path = pjoin(LIB_DIR, hgs_src_tarball_name) + hgs_src_path = pjoin(DEPS_DIR, hgs_src_tarball_name) urlretrieve(HGS_SRC, hgs_src_path) - _run(f"tar xzvf {hgs_src_tarball_name}", LIB_DIR) + _run(f"tar xzvf {hgs_src_tarball_name}", DEPS_DIR) + convert_shared_to_static() _run( f'cmake -DCMAKE_BUILD_TYPE=Release -G "Unix Makefiles" ../HGS-CVRP-{HGS_VERSION}', BUILD_DIR, @@ -71,7 +84,7 @@ def download_build_hgs(): _run("make lib", BUILD_DIR) shutil.copyfile(f"{BUILD_DIR}/{LIB_FILENAME}", f"hygese/{LIB_FILENAME}") - + class BuildPyCommand(_build_py): def run(self): @@ -85,8 +98,10 @@ def run(self): name="hygese.wrapper", sources=["hygese/wrapper.pyx"], include_dirs = [f"deps/HGS-CVRP-{HGS_VERSION}/Program/"], - library_dirs = ["hygese"], - libraries = ["hgscvrp"], + language="c", + # library_dirs = ["hygese"], + # libraries = ["hgscvrp"], + extra_objects = [f"hygese/{LIB_FILENAME}"] ) ] @@ -117,7 +132,7 @@ def run(self): "build_py": BuildPyCommand, }, ext_modules=cythonize(extentions), - data_files=[("lib", [f"hygese/{LIB_FILENAME}"])], + # data_files=[("lib", [f"hygese/{LIB_FILENAME}"])], # include_package_data=True, # package_data={ # "": ["libhgscvrp.*"], From cbb832a0a118f754321dbaaf5a695e54ec175813 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 23:16:07 -0500 Subject: [PATCH 12/13] Update setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 11c7f08..e988344 100644 --- a/setup.py +++ b/setup.py @@ -83,7 +83,7 @@ def download_build_hgs(): ) _run("make lib", BUILD_DIR) - shutil.copyfile(f"{BUILD_DIR}/{LIB_FILENAME}", f"hygese/{LIB_FILENAME}") + # shutil.copyfile(f"{BUILD_DIR}/{LIB_FILENAME}", f"hygese/{LIB_FILENAME}") class BuildPyCommand(_build_py): @@ -101,7 +101,7 @@ def run(self): language="c", # library_dirs = ["hygese"], # libraries = ["hgscvrp"], - extra_objects = [f"hygese/{LIB_FILENAME}"] + extra_objects = [f"{BUILD_DIR}/{LIB_FILENAME}"] ) ] From d3c4b2198a5643991b88c8228ee2f3086145e789 Mon Sep 17 00:00:00 2001 From: Changhyun Kwon Date: Thu, 9 Mar 2023 23:30:02 -0500 Subject: [PATCH 13/13] Update setup.py --- setup.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index e988344..1ceff65 100644 --- a/setup.py +++ b/setup.py @@ -39,8 +39,6 @@ def _safe_makedirs(*paths): DEPS_DIR = "deps" BUILD_DIR = "deps/build" -BIN_DIR = "deps/bin" - def get_lib_filename(): if platform.system() == "Linux": @@ -52,11 +50,11 @@ def get_lib_filename(): else: lib_ext = "so" - lib_ext = "a" return f"libhgscvrp.{lib_ext}" -LIB_FILENAME = get_lib_filename() +# LIB_FILENAME = get_lib_filename() +LIB_FILENAME = "libhgscvrp.a" def convert_shared_to_static(): @@ -67,6 +65,8 @@ def convert_shared_to_static(): with open(f"{DEPS_DIR}/HGS-CVRP-{HGS_VERSION}/CMakeLists.txt", "w") as f: f.write(new_lines) + f.write("\n") + f.write("set(CMAKE_POSITION_INDEPENDENT_CODE ON)") def download_build_hgs():