connected-k-center 0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- connected_k_center-0.1/LICENSE +21 -0
- connected_k_center-0.1/PKG-INFO +30 -0
- connected_k_center-0.1/README.md +9 -0
- connected_k_center-0.1/build_extension.py +69 -0
- connected_k_center-0.1/connected_k_center/__init__.py +4 -0
- connected_k_center-0.1/connected_k_center/_core.cpp +107 -0
- connected_k_center-0.1/connected_k_center/core.py +153 -0
- connected_k_center-0.1/connected_k_center/cpp/clustering.cpp +138 -0
- connected_k_center-0.1/connected_k_center/cpp/connected_k_center/clustering.hpp +46 -0
- connected_k_center-0.1/connected_k_center/cpp/connected_k_center/geometry.hpp +27 -0
- connected_k_center-0.1/connected_k_center/cpp/connected_k_center/graph.hpp +46 -0
- connected_k_center-0.1/connected_k_center/cpp/connected_k_center/solver.hpp +28 -0
- connected_k_center-0.1/connected_k_center/cpp/connected_k_center/types.hpp +35 -0
- connected_k_center-0.1/connected_k_center/cpp/geometry.cpp +69 -0
- connected_k_center-0.1/connected_k_center/cpp/graph.cpp +48 -0
- connected_k_center-0.1/connected_k_center/cpp/solver.cpp +72 -0
- connected_k_center-0.1/connected_k_center/io.py +54 -0
- connected_k_center-0.1/pyproject.toml +43 -0
- connected_k_center-0.1/setup.py +31 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Algorithms and Data Structures Group of the Heinrich Heine University Düsseldorf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: connected-k-center
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: Implementations of algorithms for connected k-center on path graphs.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Lukas Drexler
|
|
8
|
+
Author-email: lukas.drexler@hhu.de
|
|
9
|
+
Requires-Python: >=3.10,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Requires-Dist: numpy (>=1.26.4,<2.0.0)
|
|
18
|
+
Requires-Dist: scikit-learn (>=1.6.1,<2.0.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Connected Path Graph Clustering
|
|
22
|
+
|
|
23
|
+
A library for algorithms for the connected k-center problem (as described in [1]). In this problem setting, the input consists of a point set $P$ and a desired number of centers $k$, along with a *connectivity graph* $G = (P,E)$. The goal is to partition $P$ into (at most) $k$ *clusters* $C_1, \ldots, C_k$, such that, for every $i$, the subgraph of $G$ induced by $C_i$ is connected.
|
|
24
|
+
|
|
25
|
+
As of now, only an algorithm for path graphs is implemented. A path graph is a graph whose connected components are simple paths. This algorithm was developed by Johanna Hillebrand and implemented by Julius Mann.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
### References
|
|
29
|
+
[1] Drexler, L., Eube, J., Luo, K., Reineccius, D., Röglin, H., Schmidt, M., & Wargalla, J. (2024). Connected k-center and k-diameter clustering. Algorithmica, 86(11), 3425-3464.
|
|
30
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Connected Path Graph Clustering
|
|
2
|
+
|
|
3
|
+
A library for algorithms for the connected k-center problem (as described in [1]). In this problem setting, the input consists of a point set $P$ and a desired number of centers $k$, along with a *connectivity graph* $G = (P,E)$. The goal is to partition $P$ into (at most) $k$ *clusters* $C_1, \ldots, C_k$, such that, for every $i$, the subgraph of $G$ induced by $C_i$ is connected.
|
|
4
|
+
|
|
5
|
+
As of now, only an algorithm for path graphs is implemented. A path graph is a graph whose connected components are simple paths. This algorithm was developed by Johanna Hillebrand and implemented by Julius Mann.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### References
|
|
9
|
+
[1] Drexler, L., Eube, J., Luo, K., Reineccius, D., Röglin, H., Schmidt, M., & Wargalla, J. (2024). Connected k-center and k-diameter clustering. Algorithmica, 86(11), 3425-3464.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Any, Dict, List
|
|
3
|
+
|
|
4
|
+
from setuptools import Extension
|
|
5
|
+
from setuptools.command.build_ext import build_ext
|
|
6
|
+
from setuptools.errors import CompileError
|
|
7
|
+
|
|
8
|
+
extension = Extension(
|
|
9
|
+
name="connected_k_center._core",
|
|
10
|
+
sources=[
|
|
11
|
+
"connected_k_center/_core.cpp",
|
|
12
|
+
"connected_k_center/cpp/solver.cpp",
|
|
13
|
+
"connected_k_center/cpp/clustering.cpp",
|
|
14
|
+
"connected_k_center/cpp/geometry.cpp",
|
|
15
|
+
"connected_k_center/cpp/graph.cpp",
|
|
16
|
+
],
|
|
17
|
+
|
|
18
|
+
include_dirs=["connected_k_center/cpp"],
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class BuildExt(build_ext):
|
|
23
|
+
"""Compiler-aware build: select C++17 and OpenMP flags per toolchain.
|
|
24
|
+
|
|
25
|
+
OpenMP parallelises the pairwise-distance and per-component computations.
|
|
26
|
+
The way it is enabled differs by compiler:
|
|
27
|
+
* GCC / Linux clang: ``-fopenmp`` for both compile and link.
|
|
28
|
+
* Apple clang (macOS): ``-Xpreprocessor -fopenmp`` and link ``libomp``
|
|
29
|
+
(its headers/libs come from Homebrew via CPPFLAGS/LDFLAGS).
|
|
30
|
+
* MSVC (Windows): ``/openmp`` and ``/std:c++17`` (it ignores ``-std=``).
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def build_extensions(self) -> None:
|
|
34
|
+
if self.compiler.compiler_type == "msvc":
|
|
35
|
+
compile_args = ["/std:c++17", "/openmp", "/EHsc"]
|
|
36
|
+
link_args: List[str] = []
|
|
37
|
+
elif sys.platform == "darwin":
|
|
38
|
+
compile_args = ["-std=c++17", "-Xpreprocessor", "-fopenmp"]
|
|
39
|
+
link_args = ["-lomp"]
|
|
40
|
+
else:
|
|
41
|
+
compile_args = ["-std=c++17", "-fopenmp"]
|
|
42
|
+
link_args = ["-fopenmp"]
|
|
43
|
+
|
|
44
|
+
for extension in self.extensions:
|
|
45
|
+
extension.extra_compile_args = (
|
|
46
|
+
list(extension.extra_compile_args) + compile_args
|
|
47
|
+
)
|
|
48
|
+
extension.extra_link_args = (
|
|
49
|
+
list(extension.extra_link_args) + link_args
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
build_ext.build_extensions(self)
|
|
54
|
+
except CompileError:
|
|
55
|
+
# Workaround Issue #2.
|
|
56
|
+
# '-stdlib=libc++' is added so the code can compile on macOS with
|
|
57
|
+
# Anaconda. Irrelevant (and unsupported) for MSVC.
|
|
58
|
+
if self.compiler.compiler_type == "msvc":
|
|
59
|
+
raise
|
|
60
|
+
for extension in self.extensions:
|
|
61
|
+
extension.extra_compile_args.append("-stdlib=libc++")
|
|
62
|
+
extension.extra_link_args.append("-stdlib=libc++")
|
|
63
|
+
build_ext.build_extensions(self)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def build(setup_kwargs: Dict[str, Any]) -> None:
|
|
67
|
+
setup_kwargs.update(
|
|
68
|
+
{"ext_modules": [extension], "cmdclass": {"build_ext": BuildExt}}
|
|
69
|
+
)
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#include <Python.h>
|
|
2
|
+
|
|
3
|
+
#include <vector>
|
|
4
|
+
|
|
5
|
+
#include "connected_k_center/types.hpp"
|
|
6
|
+
#include "connected_k_center/solver.hpp"
|
|
7
|
+
|
|
8
|
+
typedef unsigned int uint;
|
|
9
|
+
|
|
10
|
+
// Build the list of ckc::Point from a flat, row-major coordinate array.
|
|
11
|
+
// Each point's id is its row index, matching the indexing used for the
|
|
12
|
+
// adjacency list and the output labels.
|
|
13
|
+
static std::vector<ckc::Point> array_to_points(const double *coords, uint n, uint d)
|
|
14
|
+
{
|
|
15
|
+
std::vector<ckc::Point> points;
|
|
16
|
+
points.reserve(n);
|
|
17
|
+
for (uint i = 0; i < n; ++i)
|
|
18
|
+
{
|
|
19
|
+
ckc::Point p;
|
|
20
|
+
p.id = static_cast<int>(i);
|
|
21
|
+
p.coords.assign(coords + i * d, coords + i * d + d);
|
|
22
|
+
points.push_back(std::move(p));
|
|
23
|
+
}
|
|
24
|
+
return points;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Rebuild the path-graph adjacency from per-point component ids. Points that
|
|
28
|
+
// share a component id are chained in array order, reproducing the implicit
|
|
29
|
+
// path structure of the original CSV format (consecutive rows linked, a new
|
|
30
|
+
// component id starting a fresh path).
|
|
31
|
+
static ckc::AdjacencyList component_ids_to_adjacency(const int *component_ids, uint n)
|
|
32
|
+
{
|
|
33
|
+
ckc::AdjacencyList adj(n);
|
|
34
|
+
for (uint i = 1; i < n; ++i)
|
|
35
|
+
{
|
|
36
|
+
if (component_ids[i] == component_ids[i - 1])
|
|
37
|
+
{
|
|
38
|
+
adj[i].push_back(static_cast<int>(i - 1));
|
|
39
|
+
adj[i - 1].push_back(static_cast<int>(i));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return adj;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
extern "C"
|
|
46
|
+
{
|
|
47
|
+
// "__declspec(dllexport)" causes the function to be exported when compiling on Windows.
|
|
48
|
+
// Otherwise the symbol is not exported and ctypes raises
|
|
49
|
+
// "AttributeError: function 'connected_k_center_c' not found".
|
|
50
|
+
#if defined(_WIN32) || defined(__CYGWIN__)
|
|
51
|
+
__declspec(dllexport)
|
|
52
|
+
#endif
|
|
53
|
+
double
|
|
54
|
+
connected_k_center_c(double *coords, // input points, row-major (n x d)
|
|
55
|
+
int *component_ids, // length n, path membership (array order = path order)
|
|
56
|
+
uint n, // number of points
|
|
57
|
+
uint d, // number of features
|
|
58
|
+
int k, // cluster budget (result uses at most k)
|
|
59
|
+
int metric, // distance metric used by the algorithm (0 for RMSE, 1 for Euclidean, 2 for Manhattan)
|
|
60
|
+
int *out_labels, // length n, filled with the assigned center id per point
|
|
61
|
+
int *out_num_centers) // scalar, filled with the total number of centers used
|
|
62
|
+
{
|
|
63
|
+
std::vector<ckc::Point> points = array_to_points(coords, n, d);
|
|
64
|
+
ckc::AdjacencyList adj = component_ids_to_adjacency(component_ids, n);
|
|
65
|
+
|
|
66
|
+
ckc::SolverResult result = ckc::connected_k_center(k, points, adj, static_cast<ckc::Metric>(metric));
|
|
67
|
+
|
|
68
|
+
// optimal_radius < 0 signals that no feasible clustering was found.
|
|
69
|
+
if (result.optimal_radius < 0.0)
|
|
70
|
+
{
|
|
71
|
+
*out_num_centers = 0;
|
|
72
|
+
return -1.0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Default to -1 so any point left unassigned is detectable on the Python side.
|
|
76
|
+
for (uint i = 0; i < n; ++i)
|
|
77
|
+
{
|
|
78
|
+
out_labels[i] = -1;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
int num_centers = 0;
|
|
82
|
+
for (const auto &component : result.components)
|
|
83
|
+
{
|
|
84
|
+
num_centers += component.num_centers;
|
|
85
|
+
for (const auto &assignment : component.assignments)
|
|
86
|
+
{
|
|
87
|
+
out_labels[assignment.point_id] = assignment.center_id;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
*out_num_centers = num_centers;
|
|
92
|
+
return result.optimal_radius;
|
|
93
|
+
}
|
|
94
|
+
} // extern "C"
|
|
95
|
+
|
|
96
|
+
static struct PyModuleDef _coremodule = {
|
|
97
|
+
PyModuleDef_HEAD_INIT,
|
|
98
|
+
"connected_k_center._core",
|
|
99
|
+
NULL,
|
|
100
|
+
-1,
|
|
101
|
+
NULL,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
PyMODINIT_FUNC PyInit__core(void)
|
|
105
|
+
{
|
|
106
|
+
return PyModule_Create(&_coremodule);
|
|
107
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
from typing import Any, Optional, Sequence
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from sklearn.base import BaseEstimator, ClusterMixin
|
|
6
|
+
from sklearn.utils.validation import validate_data
|
|
7
|
+
|
|
8
|
+
import connected_k_center._core # type: ignore
|
|
9
|
+
|
|
10
|
+
_DLL = ctypes.cdll.LoadLibrary(connected_k_center._core.__file__)
|
|
11
|
+
|
|
12
|
+
_METRIC_CODES = {
|
|
13
|
+
"rmse": 0,
|
|
14
|
+
"euclidean": 1,
|
|
15
|
+
"manhattan": 2,
|
|
16
|
+
} # for passing metric parameter down to c++
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PathCKC(ClusterMixin, BaseEstimator):
|
|
20
|
+
"""Connected k-center clustering on path graphs.
|
|
21
|
+
|
|
22
|
+
The algorithm performs a binary search over all pairwise distances and
|
|
23
|
+
finds the minimum radius for which the points can be covered by at most
|
|
24
|
+
``n_clusters`` connected clusters. The graph is assumed to be a disjoint
|
|
25
|
+
union of paths, supplied via ``component_ids``: points sharing a component
|
|
26
|
+
id are linked, in the order in which they appear in ``X``, to form a path.
|
|
27
|
+
|
|
28
|
+
This estimator is transductive -- it clusters a fixed, structured input and
|
|
29
|
+
does not learn a reusable predictor -- so it only exposes ``fit`` /
|
|
30
|
+
``fit_predict`` and does not implement ``predict``, ``transform`` or
|
|
31
|
+
``score``.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, n_clusters: int = 8, metric: str = "rmse"):
|
|
35
|
+
self.n_clusters = n_clusters
|
|
36
|
+
self.metric = metric
|
|
37
|
+
|
|
38
|
+
def fit(
|
|
39
|
+
self,
|
|
40
|
+
X: Sequence[Sequence[float]],
|
|
41
|
+
y: Any = None,
|
|
42
|
+
component_ids: Optional[Sequence[int]] = None,
|
|
43
|
+
) -> "PathCKC":
|
|
44
|
+
self._validate_params()
|
|
45
|
+
|
|
46
|
+
_X = validate_data(
|
|
47
|
+
self,
|
|
48
|
+
X,
|
|
49
|
+
reset=True,
|
|
50
|
+
accept_sparse=False,
|
|
51
|
+
dtype=np.float64,
|
|
52
|
+
order="C",
|
|
53
|
+
accept_large_sparse=False,
|
|
54
|
+
)
|
|
55
|
+
assert isinstance(_X, np.ndarray), type(_X)
|
|
56
|
+
_X = np.ascontiguousarray(_X)
|
|
57
|
+
|
|
58
|
+
n_samples = _X.shape[0]
|
|
59
|
+
self.n_features_in_ = _X.shape[1]
|
|
60
|
+
|
|
61
|
+
if n_samples < self.n_clusters:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"n_samples={n_samples} should be >= n_clusters={self.n_clusters}."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_component_ids = _validate_component_ids(component_ids, n_samples)
|
|
67
|
+
|
|
68
|
+
# Declare c types
|
|
69
|
+
c_coords = _X.ctypes.data_as(ctypes.POINTER(ctypes.c_double))
|
|
70
|
+
c_component_ids = _component_ids.ctypes.data_as(ctypes.POINTER(ctypes.c_int))
|
|
71
|
+
c_n = ctypes.c_uint(n_samples)
|
|
72
|
+
c_d = ctypes.c_uint(self.n_features_in_)
|
|
73
|
+
c_k = ctypes.c_int(self.n_clusters)
|
|
74
|
+
c_metric = ctypes.c_int(_METRIC_CODES[self.metric])
|
|
75
|
+
|
|
76
|
+
labels = np.empty(n_samples, dtype=np.int32, order="C")
|
|
77
|
+
c_labels = labels.ctypes.data_as(ctypes.POINTER(ctypes.c_int))
|
|
78
|
+
c_num_centers = ctypes.c_int()
|
|
79
|
+
|
|
80
|
+
_DLL.connected_k_center_c.argtypes = [
|
|
81
|
+
ctypes.POINTER(ctypes.c_double), # coords
|
|
82
|
+
ctypes.POINTER(ctypes.c_int), # component_ids
|
|
83
|
+
ctypes.c_uint, # n_samples
|
|
84
|
+
ctypes.c_uint, # n_features_in_
|
|
85
|
+
ctypes.c_int, # n_clusters (k)
|
|
86
|
+
ctypes.c_int, # metric
|
|
87
|
+
ctypes.POINTER(ctypes.c_int), # labels
|
|
88
|
+
ctypes.POINTER(ctypes.c_int), # num_centers
|
|
89
|
+
]
|
|
90
|
+
_DLL.connected_k_center_c.restype = ctypes.c_double
|
|
91
|
+
|
|
92
|
+
radius = _DLL.connected_k_center_c(
|
|
93
|
+
c_coords,
|
|
94
|
+
c_component_ids,
|
|
95
|
+
c_n,
|
|
96
|
+
c_d,
|
|
97
|
+
c_k,
|
|
98
|
+
c_metric,
|
|
99
|
+
c_labels,
|
|
100
|
+
ctypes.byref(c_num_centers),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
if radius < 0:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"No feasible connected clustering exists for n_clusters="
|
|
106
|
+
f"{self.n_clusters}."
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
self.optimal_radius_ = radius
|
|
110
|
+
self.labels_ = labels
|
|
111
|
+
self.cluster_centers_indices_ = np.unique(labels)
|
|
112
|
+
self.n_clusters_used_ = c_num_centers.value
|
|
113
|
+
|
|
114
|
+
return self
|
|
115
|
+
|
|
116
|
+
def fit_predict(
|
|
117
|
+
self,
|
|
118
|
+
X: Sequence[Sequence[float]],
|
|
119
|
+
y: Any = None,
|
|
120
|
+
component_ids: Optional[Sequence[int]] = None,
|
|
121
|
+
) -> np.ndarray:
|
|
122
|
+
return self.fit(X, component_ids=component_ids).labels_
|
|
123
|
+
|
|
124
|
+
def _validate_params(self) -> None:
|
|
125
|
+
if not isinstance(self.n_clusters, (int, np.integer)):
|
|
126
|
+
raise TypeError(
|
|
127
|
+
f"n_clusters must be an integer, got {type(self.n_clusters).__name__}"
|
|
128
|
+
)
|
|
129
|
+
if self.n_clusters < 1:
|
|
130
|
+
raise ValueError(f"n_clusters must be >= 1, got {self.n_clusters}")
|
|
131
|
+
|
|
132
|
+
if self.metric not in _METRIC_CODES:
|
|
133
|
+
raise ValueError(
|
|
134
|
+
f"metric must be one of {sorted(_METRIC_CODES)}, got {self.metric!r}"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _validate_component_ids(
|
|
139
|
+
component_ids: Optional[Sequence[int]],
|
|
140
|
+
n_samples: int,
|
|
141
|
+
) -> np.ndarray:
|
|
142
|
+
# Default: a single path over all points, in the given order.
|
|
143
|
+
if component_ids is None:
|
|
144
|
+
return np.zeros(n_samples, dtype=np.int32)
|
|
145
|
+
|
|
146
|
+
ids = np.ascontiguousarray(component_ids, dtype=np.int32)
|
|
147
|
+
if ids.ndim != 1:
|
|
148
|
+
raise ValueError(f"component_ids must be 1D, got {ids.ndim}D")
|
|
149
|
+
if ids.shape[0] != n_samples:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"component_ids has length {ids.shape[0]}, expected {n_samples}."
|
|
152
|
+
)
|
|
153
|
+
return ids
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#include "connected_k_center/clustering.hpp"
|
|
2
|
+
#include "connected_k_center/geometry.hpp"
|
|
3
|
+
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <limits>
|
|
6
|
+
#include <iterator>
|
|
7
|
+
|
|
8
|
+
namespace ckc {
|
|
9
|
+
|
|
10
|
+
// Intersection of two SORTED vectors
|
|
11
|
+
static std::vector<int> intersect(const std::vector<int>& x, const std::vector<int>& y) {
|
|
12
|
+
std::vector<int> result;
|
|
13
|
+
std::set_intersection(x.begin(), x.end(), y.begin(), y.end(),
|
|
14
|
+
std::back_inserter(result));
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Radius membership: M[i] = all centers that could cover point i within radius r *contiguously*
|
|
19
|
+
std::vector<std::vector<int>> get_memberships_for_path(const std::vector<int>& path, const std::vector<Point>& points, double r, int n, Metric metric) {
|
|
20
|
+
std::vector<std::vector<int>> M(n);
|
|
21
|
+
const int m = static_cast<int>(path.size());
|
|
22
|
+
|
|
23
|
+
for (int center_idx = 0; center_idx < m; ++center_idx) {
|
|
24
|
+
const int center_id = path[center_idx];
|
|
25
|
+
|
|
26
|
+
// Extend to the left along the path
|
|
27
|
+
for (int i = center_idx; i >= 0; --i) {
|
|
28
|
+
const int target_id = path[i];
|
|
29
|
+
if (dist(points[target_id], points[center_id], metric) <= r) {
|
|
30
|
+
M[target_id].push_back(center_id);
|
|
31
|
+
} else {
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Extend to the right along the path
|
|
37
|
+
for (int i = center_idx + 1; i < m; ++i) {
|
|
38
|
+
const int target_id = path[i];
|
|
39
|
+
if (dist(points[target_id], points[center_id], metric) <= r) {
|
|
40
|
+
M[target_id].push_back(center_id);
|
|
41
|
+
} else {
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Sort (precondition for intersect())
|
|
48
|
+
for (int i = 0; i < n; ++i) {
|
|
49
|
+
std::sort(M[i].begin(), M[i].end());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return M;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// Find the optimal clustering of a connected component
|
|
57
|
+
ComponentResult solve_dp_forward(const std::vector<int>& path_indices, const std::vector<std::vector<int>>& M, const std::vector<Point>& points) {
|
|
58
|
+
const int n = static_cast<int>(path_indices.size());
|
|
59
|
+
|
|
60
|
+
// Initialize DP matrix
|
|
61
|
+
std::vector<int> dp_num_centers(n, std::numeric_limits<int>::max());
|
|
62
|
+
|
|
63
|
+
// Avoid explicit backtracking; store results directly instead
|
|
64
|
+
std::vector<std::vector<int>> partial_centers(n);
|
|
65
|
+
std::vector<std::vector<ClusterAssignment>> partial_assignments(n);
|
|
66
|
+
|
|
67
|
+
// Right interval boundary
|
|
68
|
+
for (int i = 0; i < n; ++i) {
|
|
69
|
+
std::vector<int> current_valid_centers;
|
|
70
|
+
|
|
71
|
+
// Backward search for the (left) start point j of the 'current' cluster
|
|
72
|
+
for (int j = i; j >= 0; --j) {
|
|
73
|
+
const int u = path_indices[j];
|
|
74
|
+
|
|
75
|
+
if (j == i) {
|
|
76
|
+
current_valid_centers = M[u];
|
|
77
|
+
} else {
|
|
78
|
+
current_valid_centers = intersect(current_valid_centers, M[u]); // Only centers valid for ALL
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (current_valid_centers.empty()) break; // No suitable common center exists
|
|
82
|
+
|
|
83
|
+
// Center must lie within the interval [j...i]
|
|
84
|
+
int chosen_center = -1;
|
|
85
|
+
for (int c : current_valid_centers) {
|
|
86
|
+
for (int idx = j; idx <= i; ++idx) {
|
|
87
|
+
if (path_indices[idx] == c) {
|
|
88
|
+
chosen_center = c;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (chosen_center != -1) break; // Take the first valid one
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// None of the centers lies WITHIN the interval
|
|
96
|
+
if (chosen_center == -1) continue;
|
|
97
|
+
|
|
98
|
+
// Combine the found cluster with those for [0,...,j-1]
|
|
99
|
+
const int prev_num_centers = (j == 0) ? 0 : dp_num_centers[j - 1];
|
|
100
|
+
if (prev_num_centers != std::numeric_limits<int>::max()) {
|
|
101
|
+
if (prev_num_centers + 1 < dp_num_centers[i]) {
|
|
102
|
+
dp_num_centers[i] = prev_num_centers + 1; // Interval [j,...,i] has ONE additional cluster
|
|
103
|
+
|
|
104
|
+
if (j > 0) {
|
|
105
|
+
partial_centers[i] = partial_centers[j - 1];
|
|
106
|
+
partial_assignments[i] = partial_assignments[j - 1];
|
|
107
|
+
} else {
|
|
108
|
+
partial_centers[i].clear();
|
|
109
|
+
partial_assignments[i].clear();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Append the new center and the point assignments in the interval
|
|
113
|
+
partial_centers[i].push_back(points[chosen_center].id);
|
|
114
|
+
for (int idx = j; idx <= i; ++idx) {
|
|
115
|
+
partial_assignments[i].push_back(
|
|
116
|
+
{points[path_indices[idx]].id, points[chosen_center].id}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Build the ComponentResult if a solution exists
|
|
125
|
+
ComponentResult result;
|
|
126
|
+
if (dp_num_centers[n - 1] == std::numeric_limits<int>::max()) {
|
|
127
|
+
result.feasible = false;
|
|
128
|
+
result.num_centers = 0;
|
|
129
|
+
} else {
|
|
130
|
+
result.feasible = true;
|
|
131
|
+
result.num_centers = dp_num_centers[n - 1];
|
|
132
|
+
result.centers = partial_centers[n - 1];
|
|
133
|
+
result.assignments = partial_assignments[n - 1];
|
|
134
|
+
}
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
} // namespace ckc
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "types.hpp"
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
namespace ckc {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Computes the radius membership for a path.
|
|
10
|
+
*
|
|
11
|
+
* M[i] contains all center IDs that could cover point i at radius r
|
|
12
|
+
* contiguously (i.e. with no gaps along the path).
|
|
13
|
+
*
|
|
14
|
+
* @param path Ordered path as a vector of point IDs.
|
|
15
|
+
* @param points All points.
|
|
16
|
+
* @param r Search radius.
|
|
17
|
+
* @param n Total number of points.
|
|
18
|
+
* @param metric Distance metric to use.
|
|
19
|
+
* @return Membership vector of size n.
|
|
20
|
+
*/
|
|
21
|
+
std::vector<std::vector<int>> get_memberships_for_path(
|
|
22
|
+
const std::vector<int>& path,
|
|
23
|
+
const std::vector<Point>& points,
|
|
24
|
+
double r,
|
|
25
|
+
int n,
|
|
26
|
+
Metric metric
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Solves the 1D k-center problem on a path via forward DP.
|
|
31
|
+
*
|
|
32
|
+
* Finds the minimum number of clusters such that every point lies within
|
|
33
|
+
* radius r of its center and the center lies in the same interval.
|
|
34
|
+
*
|
|
35
|
+
* @param path_indices Ordered point IDs of the path.
|
|
36
|
+
* @param M Membership vector from get_memberships_for_path().
|
|
37
|
+
* @param points All points.
|
|
38
|
+
* @return ComponentResult (feasibility, centers and assignments).
|
|
39
|
+
*/
|
|
40
|
+
ComponentResult solve_dp_forward(
|
|
41
|
+
const std::vector<int>& path_indices,
|
|
42
|
+
const std::vector<std::vector<int>>& M,
|
|
43
|
+
const std::vector<Point>& points
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
} // namespace ckc
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "types.hpp"
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
namespace ckc {
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Computes the distance between two points.
|
|
10
|
+
* (e.g. root mean squared error over all coordinate dimensions)
|
|
11
|
+
*
|
|
12
|
+
* @return Distance, or 0.0 if a point has no coordinates.
|
|
13
|
+
*/
|
|
14
|
+
double dist(const Point& a, const Point& b, Metric metric);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Computes all pairwise distances between the given points.
|
|
18
|
+
* The result is sorted and deduplicated.
|
|
19
|
+
* The computation is (optionally) parallelized (OpenMP).
|
|
20
|
+
*
|
|
21
|
+
* @param points Set of input points.
|
|
22
|
+
* @param metric Distance metric to use.
|
|
23
|
+
* @return Sorted, deduplicated vector of all pairwise distances.
|
|
24
|
+
*/
|
|
25
|
+
std::vector<double> get_all_distances(const std::vector<Point>& points, Metric metric);
|
|
26
|
+
|
|
27
|
+
} // namespace ckc
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "types.hpp"
|
|
4
|
+
#include<numeric>
|
|
5
|
+
#include<vector>
|
|
6
|
+
|
|
7
|
+
namespace ckc{
|
|
8
|
+
class Graph{
|
|
9
|
+
public:
|
|
10
|
+
|
|
11
|
+
std::vector<int> vertices; // list of vertices
|
|
12
|
+
std::vector<int> degrees; // list of vertex degrees
|
|
13
|
+
AdjacencyList adj; // list of lists, where adj[i] contains all ids of neighbors of vertex i
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
explicit Graph(int n): vertices(n), degrees(n,0), adj(n){ // constructor (creates graph with n vertices and no edges)
|
|
17
|
+
std::iota(vertices.begin(), vertices.end(), 0);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Graph(AdjacencyList adjacency): vertices(adjacency.size()), degrees(adjacency.size()), adj(std::move(adjacency)){
|
|
21
|
+
std::iota(vertices.begin(), vertices.end(), 0);
|
|
22
|
+
for (std::size_t i = 0; i < adj.size(); ++i)
|
|
23
|
+
{
|
|
24
|
+
degrees[i] = static_cast<int>(adj[i].size());
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
void add_edge(int u, int v); //adds edge {u,v} to graph
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Determines all connected components (sub-paths) from the adjacency list.
|
|
34
|
+
*
|
|
35
|
+
* Each component is returned as an ordered path (vector of point IDs),
|
|
36
|
+
* starting at an endpoint (degree 0 or 1).
|
|
37
|
+
*
|
|
38
|
+
* @param adj Adjacency list of the graph.
|
|
39
|
+
* @param n Number of nodes.
|
|
40
|
+
* @return Vector of paths; each path is a vector of point IDs.
|
|
41
|
+
*/
|
|
42
|
+
std::vector<std::vector<int>> extract_paths();
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include "types.hpp"
|
|
4
|
+
#include <vector>
|
|
5
|
+
|
|
6
|
+
namespace ckc {
|
|
7
|
+
|
|
8
|
+
/// Return value of connected_k_center().
|
|
9
|
+
struct SolverResult {
|
|
10
|
+
double optimal_radius = -1.0; ///< -1.0 = no solution found
|
|
11
|
+
std::vector<ComponentResult> components; ///< result per connected component
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Solves the connected k-center problem.
|
|
16
|
+
*
|
|
17
|
+
* Performs a binary search over all pairwise distances and determines the
|
|
18
|
+
* minimum radius r* for which a covering with at most k clusters is possible.
|
|
19
|
+
*
|
|
20
|
+
* @param k Maximum number of allowed cluster centers.
|
|
21
|
+
* @param points Input points.
|
|
22
|
+
* @param adj Adjacency list (i.e. graph structure).
|
|
23
|
+
* @param metric Distance metric to use.
|
|
24
|
+
* @return SolverResult with the optimal radius and per-component results.
|
|
25
|
+
*/
|
|
26
|
+
SolverResult connected_k_center(int k, const std::vector<Point>& points, const AdjacencyList& adj, Metric metric = Metric::RMSE);
|
|
27
|
+
|
|
28
|
+
} // namespace ckc
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <vector>
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
namespace ckc {
|
|
9
|
+
|
|
10
|
+
enum class Metric { RMSE = 0, Euclidean = 1, Manhattan = 2 };
|
|
11
|
+
|
|
12
|
+
/// A point has an ID + a multi-dimensional coordinate
|
|
13
|
+
struct Point {
|
|
14
|
+
int id;
|
|
15
|
+
std::vector<double> coords;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/// Assignment: point to a cluster center
|
|
19
|
+
struct ClusterAssignment {
|
|
20
|
+
int point_id;
|
|
21
|
+
int center_id;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/// Result of the k-center algorithm for ONE connected component
|
|
25
|
+
struct ComponentResult {
|
|
26
|
+
bool feasible;
|
|
27
|
+
int num_centers;
|
|
28
|
+
std::vector<int> centers;
|
|
29
|
+
std::vector<ClusterAssignment> assignments;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/// Adjacency list: adj[i] ~> neighbor IDs of point i
|
|
33
|
+
using AdjacencyList = std::vector<std::vector<int>>;
|
|
34
|
+
|
|
35
|
+
} // namespace ckc
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#include "connected_k_center/geometry.hpp"
|
|
2
|
+
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
|
|
6
|
+
namespace ckc {
|
|
7
|
+
|
|
8
|
+
// Distance function
|
|
9
|
+
double dist(const Point& a, const Point& b, Metric metric) {
|
|
10
|
+
const int d = static_cast<int>(a.coords.size());
|
|
11
|
+
if (d == 0) return 0.0;
|
|
12
|
+
|
|
13
|
+
double sum = 0.0;
|
|
14
|
+
|
|
15
|
+
switch(metric) {
|
|
16
|
+
case Metric::RMSE: //sqrt(mean of squares)
|
|
17
|
+
for (int i = 0; i < d; ++i)
|
|
18
|
+
{
|
|
19
|
+
double diff = a.coords[i] - b.coords[i];
|
|
20
|
+
sum += diff * diff;
|
|
21
|
+
}
|
|
22
|
+
return std::sqrt(sum / d);
|
|
23
|
+
|
|
24
|
+
case Metric::Euclidean: //standard Euclidean distance
|
|
25
|
+
for (int i = 0; i < d; ++i)
|
|
26
|
+
{
|
|
27
|
+
double diff = a.coords[i] - b.coords[i];
|
|
28
|
+
sum += diff * diff;
|
|
29
|
+
}
|
|
30
|
+
return std::sqrt(sum);
|
|
31
|
+
|
|
32
|
+
case Metric::Manhattan: //Manhattan aka l_1 metric
|
|
33
|
+
for (int i = 0; i < d; ++i)
|
|
34
|
+
{
|
|
35
|
+
sum += std::abs(a.coords[i] - b.coords[i]);
|
|
36
|
+
}
|
|
37
|
+
return sum;
|
|
38
|
+
}
|
|
39
|
+
return 0.0;
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// Compute the sorted list of all distinct pairwise distances
|
|
45
|
+
std::vector<double> get_all_distances(const std::vector<Point>& points, Metric metric) {
|
|
46
|
+
const int n = static_cast<int>(points.size());
|
|
47
|
+
if (n < 2) return {0.0};
|
|
48
|
+
|
|
49
|
+
// Pre-allocate (required for parallel writes)
|
|
50
|
+
const int num_pairs = n * (n - 1) / 2;
|
|
51
|
+
std::vector<double> distances(num_pairs + 1);
|
|
52
|
+
distances[0] = 0.0;
|
|
53
|
+
|
|
54
|
+
// Computation can be parallelized
|
|
55
|
+
#pragma omp parallel for schedule(dynamic) default(none) shared(n, points, distances, metric)
|
|
56
|
+
for (int i = 0; i < n; ++i) {
|
|
57
|
+
for (int j = i + 1; j < n; ++j) {
|
|
58
|
+
int idx = 1 + i * n - (i * (i + 1)) / 2 + (j - i - 1);
|
|
59
|
+
distances[idx] = dist(points[i], points[j], metric);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Remove duplicates
|
|
64
|
+
std::sort(distances.begin(), distances.end());
|
|
65
|
+
distances.erase(std::unique(distances.begin(), distances.end()), distances.end());
|
|
66
|
+
return distances;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
} // namespace ckc
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#include "connected_k_center/graph.hpp"
|
|
2
|
+
|
|
3
|
+
namespace ckc{
|
|
4
|
+
|
|
5
|
+
void Graph::add_edge(int u, int v){
|
|
6
|
+
adj[u].push_back(v);
|
|
7
|
+
adj[v].push_back(u);
|
|
8
|
+
degrees[u]++;
|
|
9
|
+
degrees[v]++;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
// Extract connected components as ordered paths
|
|
14
|
+
std::vector<std::vector<int>> Graph::extract_paths() {
|
|
15
|
+
|
|
16
|
+
std::vector<std::vector<int>> paths;
|
|
17
|
+
std::vector<bool> visited(vertices.size(), false);
|
|
18
|
+
|
|
19
|
+
// Use path endpoints (degree 0 or 1) as start nodes
|
|
20
|
+
for (std::size_t i = 0; i < vertices.size(); ++i) {
|
|
21
|
+
if (!visited[i] && degrees[i] <= 1) {
|
|
22
|
+
std::vector<int> path;
|
|
23
|
+
int curr = i;
|
|
24
|
+
int prev = -1; // Predecessor node on the path
|
|
25
|
+
|
|
26
|
+
// Follow edges
|
|
27
|
+
while (curr != -1) {
|
|
28
|
+
path.push_back(curr);
|
|
29
|
+
visited[curr] = true;
|
|
30
|
+
|
|
31
|
+
int next = -1;
|
|
32
|
+
for (int nb : adj[curr]) {
|
|
33
|
+
if (nb != prev) {
|
|
34
|
+
next = nb;
|
|
35
|
+
break; // In a path graph there is at most one such neighbor
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
prev = curr;
|
|
40
|
+
curr = next;
|
|
41
|
+
}
|
|
42
|
+
paths.push_back(path);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return paths;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#include "connected_k_center/solver.hpp"
|
|
2
|
+
#include "connected_k_center/geometry.hpp"
|
|
3
|
+
#include "connected_k_center/graph.hpp"
|
|
4
|
+
#include "connected_k_center/clustering.hpp"
|
|
5
|
+
|
|
6
|
+
#include <vector>
|
|
7
|
+
|
|
8
|
+
namespace ckc {
|
|
9
|
+
|
|
10
|
+
/*///////////////////////////////////////////////////
|
|
11
|
+
//////////////////// Overall logic //////////////////
|
|
12
|
+
///////////////////////////////////////////////////*/
|
|
13
|
+
|
|
14
|
+
SolverResult connected_k_center(int k, const std::vector<Point>& points, const AdjacencyList& adj, Metric metric) {
|
|
15
|
+
SolverResult result;
|
|
16
|
+
const int n = static_cast<int>(points.size());
|
|
17
|
+
if (n == 0) return result;
|
|
18
|
+
|
|
19
|
+
/// [Step 0] Compute pairwise distances
|
|
20
|
+
|
|
21
|
+
std::vector<double> distances = get_all_distances(points, metric);
|
|
22
|
+
|
|
23
|
+
/// [Step 1] Build the graph and extract paths (connected components)
|
|
24
|
+
Graph G(adj);
|
|
25
|
+
std::vector<std::vector<int>> paths = G.extract_paths();
|
|
26
|
+
|
|
27
|
+
/// [Step 2] Binary search over distances
|
|
28
|
+
int low_idx = 0;
|
|
29
|
+
int high_idx = static_cast<int>(distances.size()) - 1;
|
|
30
|
+
|
|
31
|
+
while (low_idx <= high_idx) {
|
|
32
|
+
const int mid_idx = low_idx + (high_idx - low_idx) / 2;
|
|
33
|
+
const double r = distances[mid_idx];
|
|
34
|
+
|
|
35
|
+
int total_clusters = 0;
|
|
36
|
+
bool possible = true;
|
|
37
|
+
std::vector<ComponentResult> current_results;
|
|
38
|
+
|
|
39
|
+
const int num_paths = static_cast<int>(paths.size());
|
|
40
|
+
#pragma omp parallel for schedule(dynamic) default(none) shared(n, points, paths, r, possible, total_clusters, current_results, metric, num_paths)
|
|
41
|
+
for (int pi = 0; pi < num_paths; ++pi) {
|
|
42
|
+
const auto& path = paths[pi];
|
|
43
|
+
|
|
44
|
+
auto M = get_memberships_for_path(path, points, r, n, metric);
|
|
45
|
+
|
|
46
|
+
ComponentResult res = solve_dp_forward(path, M, points);
|
|
47
|
+
|
|
48
|
+
if (!res.feasible) {
|
|
49
|
+
possible = false; // No valid clustering
|
|
50
|
+
// break;
|
|
51
|
+
} else {
|
|
52
|
+
#pragma omp critical
|
|
53
|
+
{
|
|
54
|
+
total_clusters += res.num_centers;
|
|
55
|
+
current_results.push_back(res);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (possible && total_clusters <= k) {
|
|
61
|
+
result.optimal_radius = r;
|
|
62
|
+
result.components = current_results;
|
|
63
|
+
high_idx = mid_idx - 1;
|
|
64
|
+
} else {
|
|
65
|
+
low_idx = mid_idx + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
} // namespace ckc
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from typing import Tuple, Union
|
|
2
|
+
from os import PathLike
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def read_instance(
|
|
8
|
+
path: Union[str, "PathLike[str]"],
|
|
9
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
10
|
+
"""Read a path-graph clustering instance from a CSV/text file.
|
|
11
|
+
|
|
12
|
+
File format (as produced for the sanity-check instances):
|
|
13
|
+
* one point per non-empty line, given as its comma-separated coordinates
|
|
14
|
+
(a single value per line for the 1D case);
|
|
15
|
+
* an empty line starts a new connected component;
|
|
16
|
+
* the order of the lines encodes the order of the points along each path.
|
|
17
|
+
|
|
18
|
+
Returns ``(X, component_ids)`` ready to pass to
|
|
19
|
+
:meth:`PathCKC.fit`: ``X`` has shape ``(n_points, n_features)``
|
|
20
|
+
and ``component_ids`` is the length-``n_points`` path-membership array.
|
|
21
|
+
"""
|
|
22
|
+
coords = []
|
|
23
|
+
component_ids = []
|
|
24
|
+
component = 0
|
|
25
|
+
seen_in_component = False
|
|
26
|
+
|
|
27
|
+
with open(path, "r") as f:
|
|
28
|
+
for line in f:
|
|
29
|
+
stripped = line.strip()
|
|
30
|
+
|
|
31
|
+
# Empty line -> boundary between components (ignore leading/repeated
|
|
32
|
+
# blanks so they don't create empty components).
|
|
33
|
+
if stripped == "":
|
|
34
|
+
if seen_in_component:
|
|
35
|
+
component += 1
|
|
36
|
+
seen_in_component = False
|
|
37
|
+
continue
|
|
38
|
+
|
|
39
|
+
values = [v for v in stripped.replace(",", " ").split() if v]
|
|
40
|
+
coords.append([float(v) for v in values])
|
|
41
|
+
component_ids.append(component)
|
|
42
|
+
seen_in_component = True
|
|
43
|
+
|
|
44
|
+
if not coords:
|
|
45
|
+
raise ValueError(f"No points found in instance file: {path}")
|
|
46
|
+
|
|
47
|
+
n_features = len(coords[0])
|
|
48
|
+
if any(len(row) != n_features for row in coords):
|
|
49
|
+
raise ValueError(
|
|
50
|
+
f"Inconsistent point dimensionality in instance file: {path}"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
X = np.asarray(coords, dtype=np.float64)
|
|
54
|
+
return X, np.asarray(component_ids, dtype=np.int32)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "connected-k-center"
|
|
3
|
+
version = "0.1"
|
|
4
|
+
description = "Implementations of algorithms for connected k-center on path graphs."
|
|
5
|
+
authors = ["Lukas Drexler <lukas.drexler@hhu.de>", "Julius Mann"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
packages = [
|
|
9
|
+
{ include = "connected_k_center" }
|
|
10
|
+
]
|
|
11
|
+
include = ["images"]
|
|
12
|
+
exclude = [
|
|
13
|
+
"connected_k_center/**/*.so",
|
|
14
|
+
"connected_k_center/**/*.pyd",
|
|
15
|
+
"connected_k_center/**/__pycache__",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[tool.poetry.dependencies]
|
|
19
|
+
python = "^3.10"
|
|
20
|
+
scikit-learn = "^1.6.1"
|
|
21
|
+
numpy = "^1.26.4"
|
|
22
|
+
|
|
23
|
+
[tool.poetry.group.dev.dependencies]
|
|
24
|
+
black = {extras = ["jupyter"], version = "^24.4.2"}
|
|
25
|
+
pre-commit = "^2.14.0"
|
|
26
|
+
flake8 = "^7.0.0"
|
|
27
|
+
mypy = "^1.10.0"
|
|
28
|
+
tqdm = "^4.66.4"
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.test.dependencies]
|
|
31
|
+
pandas = "^2.2.2"
|
|
32
|
+
|
|
33
|
+
[tool.poetry.build]
|
|
34
|
+
script = "build_extension.py"
|
|
35
|
+
generate-setup-file = true
|
|
36
|
+
|
|
37
|
+
[build-system]
|
|
38
|
+
requires = ["poetry-core", "setuptools"]
|
|
39
|
+
build-backend = "poetry.core.masonry.api"
|
|
40
|
+
|
|
41
|
+
[tool.isort]
|
|
42
|
+
profile = "black"
|
|
43
|
+
line_length = 88
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
from setuptools import setup
|
|
3
|
+
|
|
4
|
+
packages = \
|
|
5
|
+
['connected_k_center']
|
|
6
|
+
|
|
7
|
+
package_data = \
|
|
8
|
+
{'': ['*'], 'connected_k_center': ['cpp/*', 'cpp/connected_k_center/*']}
|
|
9
|
+
|
|
10
|
+
install_requires = \
|
|
11
|
+
['numpy>=1.26.4,<2.0.0', 'scikit-learn>=1.6.1,<2.0.0']
|
|
12
|
+
|
|
13
|
+
setup_kwargs = {
|
|
14
|
+
'name': 'connected-k-center',
|
|
15
|
+
'version': '0.1',
|
|
16
|
+
'description': 'Implementations of algorithms for connected k-center on path graphs.',
|
|
17
|
+
'long_description': '# Connected Path Graph Clustering\n\nA library for algorithms for the connected k-center problem (as described in [1]). In this problem setting, the input consists of a point set $P$ and a desired number of centers $k$, along with a *connectivity graph* $G = (P,E)$. The goal is to partition $P$ into (at most) $k$ *clusters* $C_1, \\ldots, C_k$, such that, for every $i$, the subgraph of $G$ induced by $C_i$ is connected.\n\nAs of now, only an algorithm for path graphs is implemented. A path graph is a graph whose connected components are simple paths. This algorithm was developed by Johanna Hillebrand and implemented by Julius Mann.\n\n\n### References\n[1] Drexler, L., Eube, J., Luo, K., Reineccius, D., Röglin, H., Schmidt, M., & Wargalla, J. (2024). Connected k-center and k-diameter clustering. Algorithmica, 86(11), 3425-3464.\n',
|
|
18
|
+
'author': 'Lukas Drexler',
|
|
19
|
+
'author_email': 'lukas.drexler@hhu.de',
|
|
20
|
+
'maintainer': 'None',
|
|
21
|
+
'maintainer_email': 'None',
|
|
22
|
+
'url': 'None',
|
|
23
|
+
'packages': packages,
|
|
24
|
+
'package_data': package_data,
|
|
25
|
+
'install_requires': install_requires,
|
|
26
|
+
'python_requires': '>=3.10,<4.0',
|
|
27
|
+
}
|
|
28
|
+
from build_extension import *
|
|
29
|
+
build(setup_kwargs)
|
|
30
|
+
|
|
31
|
+
setup(**setup_kwargs)
|