diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml new file mode 100644 index 0000000..12c8d56 --- /dev/null +++ b/.github/workflows/build_wheels.yml @@ -0,0 +1,71 @@ +name: Build and upload to PyPI + +on: + workflow_dispatch: + pull_request: + branches: + - main + push: + branches: + - main + +jobs: + build_wheels: + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + #fail-fast: false #commented until windows support is added + matrix: + # macos-13 is an intel runner, macos-14 is apple silicon + os: [ubuntu-latest, windows-latest, macos-13, macos-14] + + steps: + - uses: actions/checkout@v4 + + - name: Build wheels + uses: pypa/cibuildwheel@v2.22.0 + + - uses: actions/upload-artifact@v4 + with: + name: cibw-wheels-${{ matrix.os }}-${{ strategy.job-index }} + path: ./wheelhouse/*.whl + + build_sdist: + name: Build source distribution + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Build sdist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v4 + with: + name: cibw-sdist + path: dist/*.tar.gz + + upload_pypi: + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + if: github.event_name == 'release' && github.event.action == 'published' + # or, alternatively, upload to PyPI on every tag starting with 'v' (remove on: release above to use this) + #if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + steps: + - uses: actions/download-artifact@v4 + with: + # unpacks all CIBW artifacts into dist/ + pattern: cibw-* + path: dist + merge-multiple: true + + - name: Generate artifact attestations + uses: actions/attest-build-provenance@v1.4.4 + with: + subject-path: "dist/*" + + - uses: pypa/gh-action-pypi-publish@release/v1 + #with: + # To test: repository-url: https://test.pypi.org/legacy/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index c3374dc..a207a50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,11 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "radius-clustering" -version = "1.0.0" +version = "1.0.1" description = "A Clustering under radius constraints algorithm using minimum dominating sets" readme = "README.md" authors = [ {name = "Quentin Haenn"}, + {name = "Lias Laboratory"} ] dependencies = [ @@ -37,6 +38,7 @@ classifiers=[ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", ] keywords = ["Unsupervised learning","clustering", "minimum dominating sets","clustering under radius constraint"] @@ -137,3 +139,8 @@ docstring-code-format = true # Set the line length limit used when formatting code snippets in # docstrings. docstring-code-line-length = "dynamic" + + +[tool.cibuildwheel] +# Skip building for PyPy, python 3.6/7/8 and 13t, and 32-bit platforms. +skip = ["pp*", "cp36-*", "cp37-*", "cp38-*", "*-win32", "*linux_i686", "*musllinux*"] diff --git a/radius_clustering/radius_clustering.py b/radius_clustering/radius_clustering.py index 8fd3cc0..5fbcc9f 100644 --- a/radius_clustering/radius_clustering.py +++ b/radius_clustering/radius_clustering.py @@ -10,7 +10,7 @@ import os import numpy as np -import scipy.spatial as sp_spatial +from sklearn.metrics import pairwise_distances from sklearn.base import BaseEstimator, ClusterMixin from sklearn.utils.validation import check_array @@ -38,7 +38,7 @@ class RadiusClustering(BaseEstimator, ClusterMixin): ----------- X : array-like, shape (n_samples, n_features) The input data. - centers : list + centers\_ : list The indices of the cluster centers. labels\_ : array-like, shape (n_samples,) The cluster labels for each point in the input data. @@ -50,6 +50,9 @@ def __init__(self, manner="approx", threshold=0.5): self.manner = manner self.threshold = threshold + def _check_symmetric(self, a, tol=1e-8): + return np.allclose(a, a.T, atol=tol) + def fit(self, X, y=None): """ Fit the MDS clustering model to the input data. @@ -87,10 +90,19 @@ def fit(self, X, y=None): self.X = check_array(X) # Create dist and adj matrices - dist_mat = sp_spatial.distance_matrix(self.X, self.X) + if not self._check_symmetric(self.X): + dist_mat = pairwise_distances(self.X, metric="euclidean") + else: + dist_mat = self.X adj_mask = np.triu((dist_mat <= self.threshold), k=1) self.nb_edges = np.sum(adj_mask) - self.edges = np.argwhere(adj_mask).astype(np.int32) + if self.nb_edges == 0: + self.centers_ = list(range(self.X.shape[0])) + self.labels_ = self.centers_ + self.effective_radius = 0 + self._mds_exec_time = 0 + return self + self.edges = np.argwhere(adj_mask).astype(np.uint32) #TODO: changer en uint32 self.dist_mat = dist_mat self._clustering() diff --git a/radius_clustering/utils/emos.pyx b/radius_clustering/utils/emos.pyx index d6547b7..c6da2a5 100644 --- a/radius_clustering/utils/emos.pyx +++ b/radius_clustering/utils/emos.pyx @@ -17,7 +17,7 @@ cdef extern from "mds3-util.h": int set_size double exec_time - Result* emos_main(int* edges, int nb_edge, int n) + Result* emos_main(unsigned int* edges, int nb_edge, int n) void cleanup() @@ -26,7 +26,7 @@ cdef extern from "mds3-util.h": import numpy as np cimport numpy as np -def py_emos_main(np.ndarray[int, ndim=1] edges, int n, int nb_edge): +def py_emos_main(np.ndarray[unsigned int, ndim=1] edges, int n, int nb_edge): cdef Result* result = emos_main(&edges[0], n, nb_edge) dominating_set = [result.dominating_set[i] - 1 for i in range(result.set_size)] diff --git a/radius_clustering/utils/main-emos.c b/radius_clustering/utils/main-emos.c index 6d25215..8015cb1 100644 --- a/radius_clustering/utils/main-emos.c +++ b/radius_clustering/utils/main-emos.c @@ -10,15 +10,27 @@ Copyright (C) 2024, Haenn Quentin. #include #include #include -#include #include -#include -#include -#include -#include -#include -#include #include +#include + +#ifdef _WIN32 + #include + #include + #include + #define SIGINT 2 + typedef void (*SignalHandlerFn)(int); +#elif defined(__APPLE__) || defined(__linux__) + #include + #include + #include + #include + #include +#else + #error "Unsupported platform" +#endif + + #include "mds3-util.h" #include "util_heap.h" @@ -1373,7 +1385,7 @@ void solve_subproblems(){ static void print_final_solution(char *inst){ printf("--------------------------------\n"); printf("Solution: "); - for(int i=0;i #include #include +#include + +#ifdef _WIN32 +#include +#elif defined(__APPLE__) || defined(__linux__) #include #include +#else +#error "Unsupported platform" +#endif #define WORD_LENGTH 100 #define TRUE 1 @@ -200,10 +208,27 @@ struct Result { }; static double get_utime() { - struct rusage utime; - getrusage(RUSAGE_SELF, &utime); - return (double) (utime.ru_utime.tv_sec - + (double) utime.ru_utime.tv_usec / 1000000); + #ifdef _WIN32 + FILETIME createTime; + FILETIME exitTime; + FILETIME kernelTime; + FILETIME userTime; + if (GetProcessTimes(GetCurrentProcess(), + &createTime, &exitTime, + &kernelTime, &userTime) != 0) { + ULARGE_INTEGER li = {{userTime.dwLowDateTime, userTime.dwHighDateTime}}; + return li.QuadPart * 1e-7; + } + return 0.0; + #elif defined(__APPLE__) || defined(__linux__) + struct rusage utime; + if (getrusage(RUSAGE_SELF, &utime) == 0) { + return (double)utime.ru_utime.tv_sec + (double)utime.ru_utime.tv_usec * 1e-6; + } + return 0.0; + #else + return (double)clock() / CLOCKS_PER_SEC; + #endif } static int cmp_branching_vertex_score(const void * a, const void *b){ @@ -230,7 +255,8 @@ static void parse_parmerters(int argc, char *argv[]) { } static void allcoate_memory_for_adjacency_list(int nb_node, int nb_edge,int offset) { - int i, block_size = 40960000, free_size = 0; + int i, block_size = 40960000; + unsigned int free_size = 0; Init_Adj_List = (int *) malloc((2 * nb_edge + nb_node) * sizeof(int)); if (Init_Adj_List == NULL ) { for (i = 1; i <= NB_NODE; i++) { @@ -317,7 +343,7 @@ static int _read_graph_from_adjacency_matrix(int** adj_matrix, int num_nodes) { return TRUE; } -static int _read_graph_from_edge_list(int* edges, int n, int nb_edges) { +static int _read_graph_from_edge_list(unsigned int* edges, int n, int nb_edges) { int i, j, l_node, r_node, nb_edge = 0, max_node = n, offset = 0; int node = 1; @@ -740,10 +766,10 @@ extern int select_branching_node(); extern void search_domset(); extern int fast_search_initial_solution(); extern void solve_subproblems(); -extern struct Result* emos_main(int* edges, int n, int nb_edge); -extern int* get_dominating_set(); -extern int get_set_size(); -extern double get_exec_time(); +extern struct Result* emos_main(unsigned int* edges, int n, int nb_edge); +extern int* get_dominating_set(struct Result* result); +extern int get_set_size(struct Result* result); +extern double get_exec_time(struct Result* result); extern void free_results(struct Result* result); // Declare global variables as extern diff --git a/radius_clustering/utils/mds_core.cpp b/radius_clustering/utils/mds_core.cpp index e261c86..039888e 100644 --- a/radius_clustering/utils/mds_core.cpp +++ b/radius_clustering/utils/mds_core.cpp @@ -8,12 +8,10 @@ */ #include #include -#include // Add this line #include #include #include #include -#include #include #include #include "random_manager.h" @@ -92,20 +90,8 @@ class Instance { std::unordered_set supportNodes; std::unordered_set leavesNodes; std::unordered_set unSelectedNodes; - int optimum; const bool supportAndLeafNodes = true; - /* - void constructAdjacencyList(const std::vector>& adjacency_matrix) { - for (int i = 0; i < numNodes; ++i) { - for (int j = 0; j < numNodes; ++j) { - if (adjacency_matrix[i][j] == 1) { - adjacencyList[i].push_back(j); - } - } - } - }*/ - void constructAdjacencyList(const std::vector& edge_list, int nb_edges) { for (int i = 0; i < 2 * nb_edges; i+=2) { int u = edge_list[i];