graphical-sampling 0.1.0__py3-none-any.whl
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.
- graphical_sampling/__init__.py +14 -0
- graphical_sampling/clustering/__init__.py +13 -0
- graphical_sampling/clustering/aggregate.py +213 -0
- graphical_sampling/clustering/dubly_balanced_clustering.py +209 -0
- graphical_sampling/clustering/one_boundary.py +233 -0
- graphical_sampling/clustering/soft_balanced_kmeans.py +161 -0
- graphical_sampling/criteria/__init__.py +4 -0
- graphical_sampling/criteria/criteria.py +15 -0
- graphical_sampling/criteria/var_nht.py +26 -0
- graphical_sampling/design.py +128 -0
- graphical_sampling/measure/__init__.py +4 -0
- graphical_sampling/measure/density.py +94 -0
- graphical_sampling/random/__init__.py +4 -0
- graphical_sampling/random/generator.py +251 -0
- graphical_sampling/red_black_tree.py +475 -0
- graphical_sampling/sampling/__init__.py +6 -0
- graphical_sampling/sampling/kmeans_spatial_sampling.py +61 -0
- graphical_sampling/sampling/population.py +234 -0
- graphical_sampling/sampling/random_sampling.py +21 -0
- graphical_sampling/search/__init__.py +4 -0
- graphical_sampling/search/astar.py +119 -0
- graphical_sampling/structs.py +94 -0
- graphical_sampling/type.py +17 -0
- graphical_sampling-0.1.0.dist-info/METADATA +85 -0
- graphical_sampling-0.1.0.dist-info/RECORD +27 -0
- graphical_sampling-0.1.0.dist-info/WHEEL +4 -0
- graphical_sampling-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
from itertools import pairwise
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
from matplotlib.patches import Polygon
|
|
8
|
+
from scipy.spatial import ConvexHull
|
|
9
|
+
|
|
10
|
+
from ..clustering import DublyBalancedKMeans
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class Zone:
|
|
15
|
+
units: NDArray
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class Cluster:
|
|
20
|
+
units: NDArray
|
|
21
|
+
zones: list[Zone]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Population:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
coordinate: NDArray,
|
|
28
|
+
inclusion_probability: NDArray,
|
|
29
|
+
*,
|
|
30
|
+
n_clusters: int,
|
|
31
|
+
n_zones: tuple[int, int],
|
|
32
|
+
tolerance: int,
|
|
33
|
+
) -> None:
|
|
34
|
+
self.coords = coordinate
|
|
35
|
+
self.probs = inclusion_probability
|
|
36
|
+
self.n_clusters = n_clusters
|
|
37
|
+
self.n_zones = n_zones
|
|
38
|
+
self.tolerance = tolerance
|
|
39
|
+
|
|
40
|
+
self.clusters = self._generate_clusters()
|
|
41
|
+
|
|
42
|
+
def _generate_clusters(self) -> list[Cluster]:
|
|
43
|
+
# kmeans = SoftBalancedKMeans(self.n_clusters, tolerance=self.tolerance)
|
|
44
|
+
# kmeans.fit(self.coords, self.probs)
|
|
45
|
+
|
|
46
|
+
# agg = AggregateBalancedKMeans(k=self.n_clusters, tolerance=self.tolerance)
|
|
47
|
+
# agg.fit(self.coords, self.probs.reshape(-1, 1), np.array([1]))
|
|
48
|
+
|
|
49
|
+
dbk = DublyBalancedKMeans(k=self.n_clusters)
|
|
50
|
+
dbk.fit(self.coords, self.probs)
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
Cluster(units=units, zones=self._generate_zones(units))
|
|
54
|
+
# for units in agg.get_clusters()
|
|
55
|
+
for units in dbk.clusters
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
def _generate_zones(self, units) -> list[Zone]:
|
|
59
|
+
vertical_zones = self._sweep(
|
|
60
|
+
units[np.argsort(units[:, 1])], 1 / self.n_zones[0]
|
|
61
|
+
)
|
|
62
|
+
zones = []
|
|
63
|
+
for zone in vertical_zones:
|
|
64
|
+
units_of_basic_zones = self._sweep(
|
|
65
|
+
zone[np.argsort(zone[:, 2])], 1 / (np.prod(self.n_zones))
|
|
66
|
+
)
|
|
67
|
+
for units in units_of_basic_zones:
|
|
68
|
+
units[:, 3] = self._numerical_stabilizer(units[:, 3])
|
|
69
|
+
zones.append(Zone(units=units))
|
|
70
|
+
return zones
|
|
71
|
+
|
|
72
|
+
def _sweep(
|
|
73
|
+
self, units: NDArray, threshold: float
|
|
74
|
+
) -> tuple[list[NDArray], list[int]]:
|
|
75
|
+
boarder_units_remainings, zones_indices = self._generate_boarders_and_indices(
|
|
76
|
+
units, threshold
|
|
77
|
+
)
|
|
78
|
+
swept_zones = []
|
|
79
|
+
for indices in pairwise(zones_indices):
|
|
80
|
+
zone, boarder_units_remainings = self._sweep_zone(
|
|
81
|
+
units, boarder_units_remainings, indices, threshold
|
|
82
|
+
)
|
|
83
|
+
swept_zones.append(zone)
|
|
84
|
+
return swept_zones
|
|
85
|
+
|
|
86
|
+
def _generate_boarders_and_indices(self, units: NDArray, threshold: float):
|
|
87
|
+
thresholds = np.arange(
|
|
88
|
+
threshold, np.sum(units[:, 3]) - threshold / 2, threshold
|
|
89
|
+
)
|
|
90
|
+
indices = np.concatenate(
|
|
91
|
+
(
|
|
92
|
+
[0],
|
|
93
|
+
np.searchsorted(units[:, 3].cumsum(), thresholds, side="right"),
|
|
94
|
+
[units.shape[0] - 1],
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
boarder_units = {index: units[index][3] for index in np.unique(indices)}
|
|
98
|
+
return boarder_units, indices
|
|
99
|
+
|
|
100
|
+
def _sweep_zone(
|
|
101
|
+
self,
|
|
102
|
+
units: NDArray,
|
|
103
|
+
boarder_units_remainings: NDArray,
|
|
104
|
+
indices: tuple[NDArray, NDArray],
|
|
105
|
+
threshold: float,
|
|
106
|
+
) -> NDArray:
|
|
107
|
+
zone, start_remainder = self._sweep_boarder_unit(
|
|
108
|
+
np.array([]).reshape(0, 4),
|
|
109
|
+
units[indices[0]],
|
|
110
|
+
boarder_units_remainings[indices[0]],
|
|
111
|
+
threshold,
|
|
112
|
+
)
|
|
113
|
+
boarder_units_remainings[indices[0]] = start_remainder
|
|
114
|
+
|
|
115
|
+
zone = np.concatenate([zone, units[indices[0] + 1 : indices[1]]])
|
|
116
|
+
|
|
117
|
+
zone, stop_remainder = self._sweep_boarder_unit(
|
|
118
|
+
zone,
|
|
119
|
+
units[indices[1]],
|
|
120
|
+
boarder_units_remainings[indices[1]],
|
|
121
|
+
threshold - np.sum(zone[:, 3]),
|
|
122
|
+
)
|
|
123
|
+
boarder_units_remainings[indices[1]] = stop_remainder
|
|
124
|
+
|
|
125
|
+
return zone, boarder_units_remainings
|
|
126
|
+
|
|
127
|
+
def _sweep_boarder_unit(
|
|
128
|
+
self, zone: NDArray, unit: NDArray, probability: float, threshold: float
|
|
129
|
+
) -> tuple[NDArray, float]:
|
|
130
|
+
if probability < 10**-self.tolerance:
|
|
131
|
+
return zone, 0
|
|
132
|
+
if threshold < 10**-self.tolerance:
|
|
133
|
+
return zone, probability
|
|
134
|
+
if probability < threshold - 10**-self.tolerance:
|
|
135
|
+
return np.concatenate(
|
|
136
|
+
[zone, np.append(unit[:3], probability).reshape(1, -1)]
|
|
137
|
+
), 0
|
|
138
|
+
elif probability > threshold + 10**-self.tolerance:
|
|
139
|
+
return np.concatenate(
|
|
140
|
+
[zone, np.append(unit[:3], threshold).reshape(1, -1)]
|
|
141
|
+
), probability - threshold
|
|
142
|
+
return np.concatenate([zone, np.append(unit[:3], threshold).reshape(1, -1)]), 0
|
|
143
|
+
|
|
144
|
+
def _numerical_stabilizer(self, probs: NDArray) -> NDArray:
|
|
145
|
+
probs_stabled = np.round(probs, self.tolerance)
|
|
146
|
+
probs_stabled *= 1 / (np.sum(probs_stabled) * np.prod(self.n_zones))
|
|
147
|
+
return probs_stabled
|
|
148
|
+
|
|
149
|
+
def plot(self, ax=None, figsize: tuple[int, int] = (8, 6)) -> None:
|
|
150
|
+
if ax is None:
|
|
151
|
+
fig, ax = plt.subplots(figsize=figsize)
|
|
152
|
+
|
|
153
|
+
def plot_convex_hull(
|
|
154
|
+
points, ax, color, alpha=0.3, edge_color="black", line_width=1.0
|
|
155
|
+
):
|
|
156
|
+
if len(points) < 3:
|
|
157
|
+
return ax, None
|
|
158
|
+
hull = ConvexHull(points)
|
|
159
|
+
polygon = Polygon(
|
|
160
|
+
points[hull.vertices],
|
|
161
|
+
closed=True,
|
|
162
|
+
facecolor=color,
|
|
163
|
+
alpha=alpha,
|
|
164
|
+
edgecolor=edge_color,
|
|
165
|
+
lw=line_width,
|
|
166
|
+
)
|
|
167
|
+
ax.add_patch(polygon)
|
|
168
|
+
return ax, hull
|
|
169
|
+
|
|
170
|
+
for cluster_idx, cluster in enumerate(self.clusters):
|
|
171
|
+
cluster_points = cluster.units[:, 1:3]
|
|
172
|
+
cluster_color = plt.cm.tab10(cluster_idx % 10)
|
|
173
|
+
ax, _ = plot_convex_hull(cluster_points, ax, color=cluster_color, alpha=0.2)
|
|
174
|
+
ax.scatter(
|
|
175
|
+
cluster_points[:, 0],
|
|
176
|
+
cluster_points[:, 1],
|
|
177
|
+
color=cluster_color,
|
|
178
|
+
label=f"Cluster {cluster_idx+1}",
|
|
179
|
+
alpha=0.8,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
for zone_idx, zone in enumerate(cluster.zones):
|
|
183
|
+
zone_points = zone.units[:, 1:3]
|
|
184
|
+
zone_color = cluster_color
|
|
185
|
+
ax, hull = plot_convex_hull(
|
|
186
|
+
zone_points,
|
|
187
|
+
ax,
|
|
188
|
+
color=zone_color,
|
|
189
|
+
alpha=0.4,
|
|
190
|
+
edge_color="gray",
|
|
191
|
+
line_width=0.8,
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
hull_center = np.mean(
|
|
195
|
+
zone_points if hull is None else zone_points[hull.vertices], axis=0
|
|
196
|
+
)
|
|
197
|
+
ax.text(
|
|
198
|
+
hull_center[0],
|
|
199
|
+
hull_center[1],
|
|
200
|
+
f"{zone_idx+1}",
|
|
201
|
+
color="black",
|
|
202
|
+
fontsize=16,
|
|
203
|
+
alpha=0.3,
|
|
204
|
+
ha="center",
|
|
205
|
+
va="center",
|
|
206
|
+
weight="bold",
|
|
207
|
+
)
|
|
208
|
+
return ax
|
|
209
|
+
|
|
210
|
+
def plot_with_samples(self, samples: NDArray, max_cols: int = 4) -> None:
|
|
211
|
+
n_samples = len(samples)
|
|
212
|
+
n_cols = min(max_cols, n_samples)
|
|
213
|
+
n_rows = (n_samples + n_cols - 1) // n_cols
|
|
214
|
+
figsize = (5 * n_cols, 5 * n_rows)
|
|
215
|
+
|
|
216
|
+
fig, axes = plt.subplots(n_rows, n_cols, figsize=figsize)
|
|
217
|
+
axes = axes.flatten() if n_samples > 1 else [axes]
|
|
218
|
+
|
|
219
|
+
for sample_idx, sample in enumerate(samples):
|
|
220
|
+
ax = axes[sample_idx]
|
|
221
|
+
ax = self.plot(ax)
|
|
222
|
+
ax.scatter(
|
|
223
|
+
self.coords[sample][:, 0],
|
|
224
|
+
self.coords[sample][:, 1],
|
|
225
|
+
color="black",
|
|
226
|
+
marker="X",
|
|
227
|
+
alpha=0.8,
|
|
228
|
+
s=200,
|
|
229
|
+
label=f"Sample {sample_idx+1}",
|
|
230
|
+
)
|
|
231
|
+
ax.set_title(f"Sample {sample_idx+1}")
|
|
232
|
+
|
|
233
|
+
for ax in axes[n_samples:]:
|
|
234
|
+
fig.delaxes(ax)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy.typing import NDArray
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class RandomSampling:
|
|
6
|
+
def __init__(
|
|
7
|
+
self,
|
|
8
|
+
coordinate: NDArray,
|
|
9
|
+
inclusion_probability: NDArray,
|
|
10
|
+
n: int,
|
|
11
|
+
) -> None:
|
|
12
|
+
self.coords = coordinate
|
|
13
|
+
self.probs = inclusion_probability
|
|
14
|
+
self.n = n
|
|
15
|
+
self.rng = np.random.default_rng()
|
|
16
|
+
|
|
17
|
+
def sample(self, n_samples: int):
|
|
18
|
+
return np.array(
|
|
19
|
+
[self.rng.integers(0, self.probs.shape[0], size=self.n) for _ in range(n_samples)],
|
|
20
|
+
dtype=int
|
|
21
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Generator, Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..criteria.criteria import Criteria
|
|
7
|
+
from ..design import Design
|
|
8
|
+
from ..red_black_tree import RedBlackTree
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass(frozen=True, order=False, eq=False)
|
|
12
|
+
class Node:
|
|
13
|
+
criteria_value: float
|
|
14
|
+
design: Design
|
|
15
|
+
|
|
16
|
+
def __lt__(self, other: Any) -> bool:
|
|
17
|
+
if not isinstance(other, Node):
|
|
18
|
+
return NotImplemented
|
|
19
|
+
return self.criteria_value < other.criteria_value
|
|
20
|
+
|
|
21
|
+
def __eq__(self, other: Any) -> bool:
|
|
22
|
+
if not isinstance(other, Node):
|
|
23
|
+
return NotImplemented
|
|
24
|
+
return self.criteria_value == other.criteria_value
|
|
25
|
+
|
|
26
|
+
def __le__(self, other: Any) -> bool:
|
|
27
|
+
return self < other or self == other
|
|
28
|
+
|
|
29
|
+
def __ge__(self, other: Any) -> bool:
|
|
30
|
+
return not self < other
|
|
31
|
+
|
|
32
|
+
def __gt__(self, other: Any) -> bool:
|
|
33
|
+
return other < self
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AStar:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
initial_design: Design,
|
|
40
|
+
criteria: Criteria,
|
|
41
|
+
*,
|
|
42
|
+
switch_coefficient: float = 0.5,
|
|
43
|
+
random_pull: bool = False,
|
|
44
|
+
threshold: float = 1e-2,
|
|
45
|
+
rng: np.random.Generator = np.random.default_rng(),
|
|
46
|
+
) -> None:
|
|
47
|
+
self.initial_design = initial_design
|
|
48
|
+
self.criteria = criteria
|
|
49
|
+
self.switch_coefficient = switch_coefficient
|
|
50
|
+
self.random_pull = random_pull
|
|
51
|
+
self.threshold = threshold
|
|
52
|
+
self.rng = rng
|
|
53
|
+
|
|
54
|
+
self.initial_criteria_value = self.criteria(self.initial_design)
|
|
55
|
+
self.best_design = self.initial_design
|
|
56
|
+
self.best_criteria_value = self.initial_criteria_value
|
|
57
|
+
|
|
58
|
+
def iterate_design(self, design: Design, num_changes: int) -> Design:
|
|
59
|
+
new_design = design.copy()
|
|
60
|
+
for _ in range(num_changes):
|
|
61
|
+
new_design.iterate(
|
|
62
|
+
random_pull=self.random_pull,
|
|
63
|
+
switch_coefficient=self.switch_coefficient,
|
|
64
|
+
)
|
|
65
|
+
return new_design
|
|
66
|
+
|
|
67
|
+
def neighbors(
|
|
68
|
+
self,
|
|
69
|
+
design: Design,
|
|
70
|
+
num_new_nodes: int,
|
|
71
|
+
num_changes: int,
|
|
72
|
+
) -> Generator[Design, None, None]:
|
|
73
|
+
for _ in range(num_new_nodes):
|
|
74
|
+
yield self.iterate_design(design, num_changes)
|
|
75
|
+
|
|
76
|
+
def run(
|
|
77
|
+
self,
|
|
78
|
+
max_iterations: int,
|
|
79
|
+
num_new_nodes: int,
|
|
80
|
+
max_open_set_size: int,
|
|
81
|
+
num_changes: int,
|
|
82
|
+
):
|
|
83
|
+
closed_set = set()
|
|
84
|
+
open_set = RedBlackTree[Node]()
|
|
85
|
+
open_set.insert(Node(self.initial_criteria_value, self.initial_design))
|
|
86
|
+
|
|
87
|
+
for it in range(max_iterations):
|
|
88
|
+
if not open_set:
|
|
89
|
+
break
|
|
90
|
+
mn = open_set.get_min()
|
|
91
|
+
if not mn:
|
|
92
|
+
break
|
|
93
|
+
current_design = mn.design
|
|
94
|
+
if current_design in closed_set:
|
|
95
|
+
continue
|
|
96
|
+
closed_set.add(current_design)
|
|
97
|
+
for new_design in self.neighbors(
|
|
98
|
+
current_design, num_new_nodes, num_changes
|
|
99
|
+
):
|
|
100
|
+
new_criteria_value = self.criteria(new_design)
|
|
101
|
+
|
|
102
|
+
if new_design in closed_set:
|
|
103
|
+
continue
|
|
104
|
+
if len(open_set) < max_open_set_size:
|
|
105
|
+
open_set.insert(Node(new_criteria_value, new_design))
|
|
106
|
+
else:
|
|
107
|
+
mx = open_set.get_max()
|
|
108
|
+
if mx is None or mx.criteria_value > new_criteria_value:
|
|
109
|
+
if mx is not None:
|
|
110
|
+
open_set.remove(mx)
|
|
111
|
+
open_set.insert(Node(new_criteria_value, new_design))
|
|
112
|
+
|
|
113
|
+
if new_criteria_value < self.best_criteria_value:
|
|
114
|
+
self.best_design = new_design
|
|
115
|
+
self.best_criteria_value = new_criteria_value
|
|
116
|
+
|
|
117
|
+
if self.best_criteria_value < self.threshold:
|
|
118
|
+
return it
|
|
119
|
+
return max_iterations
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import heapq
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Iterator, Generic, Collection, Optional, Any
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .type import ComparableNegatable
|
|
10
|
+
|
|
11
|
+
T = TypeVar("T", bound=ComparableNegatable)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MaxHeap(Generic[T]):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
initial_heap: Optional[Collection[T]] = None,
|
|
18
|
+
rng: np.random.Generator = np.random.default_rng(),
|
|
19
|
+
):
|
|
20
|
+
self.heap: list[T] = []
|
|
21
|
+
if initial_heap is not None:
|
|
22
|
+
self.heap = [-item for item in initial_heap]
|
|
23
|
+
heapq.heapify(self.heap)
|
|
24
|
+
self.rng = rng
|
|
25
|
+
|
|
26
|
+
def push(self, item: T):
|
|
27
|
+
heapq.heappush(self.heap, -item)
|
|
28
|
+
|
|
29
|
+
def pop(self) -> T:
|
|
30
|
+
return -heapq.heappop(self.heap)
|
|
31
|
+
|
|
32
|
+
def peek(self) -> T:
|
|
33
|
+
return -self.heap[0]
|
|
34
|
+
|
|
35
|
+
def randompop(self) -> T:
|
|
36
|
+
idx = self.rng.integers(len(self.heap))
|
|
37
|
+
val = -self.heap[idx]
|
|
38
|
+
self.heap[idx] = self.heap[-1]
|
|
39
|
+
self.heap.pop()
|
|
40
|
+
if idx < len(self.heap):
|
|
41
|
+
heapq._siftup(self.heap, idx) # type: ignore
|
|
42
|
+
heapq._siftdown(self.heap, 0, idx) # type: ignore
|
|
43
|
+
return val
|
|
44
|
+
|
|
45
|
+
def copy(self) -> MaxHeap[T]:
|
|
46
|
+
new_heap = MaxHeap[T]()
|
|
47
|
+
new_heap.heap = self.heap[:]
|
|
48
|
+
new_heap.rng = self.rng
|
|
49
|
+
return new_heap
|
|
50
|
+
|
|
51
|
+
def __len__(self) -> int:
|
|
52
|
+
return len(self.heap)
|
|
53
|
+
|
|
54
|
+
def __bool__(self) -> bool:
|
|
55
|
+
return bool(self.heap)
|
|
56
|
+
|
|
57
|
+
def __iter__(self) -> Iterator[T]:
|
|
58
|
+
return map(lambda x: -x, self.heap)
|
|
59
|
+
|
|
60
|
+
def __str__(self):
|
|
61
|
+
return str(list(map(lambda x: -x, self.heap)))
|
|
62
|
+
|
|
63
|
+
def __hash__(self) -> int:
|
|
64
|
+
return hash(tuple(self.heap))
|
|
65
|
+
|
|
66
|
+
def __eq__(self, other: object) -> bool:
|
|
67
|
+
if not isinstance(other, MaxHeap):
|
|
68
|
+
return NotImplemented
|
|
69
|
+
return self.heap == other.heap
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass(order=False)
|
|
73
|
+
class Sample:
|
|
74
|
+
probability: float
|
|
75
|
+
ids: frozenset[int]
|
|
76
|
+
|
|
77
|
+
def almost_zero(self) -> bool:
|
|
78
|
+
return self.probability < 1e-9
|
|
79
|
+
|
|
80
|
+
def __eq__(self, other: Any) -> bool:
|
|
81
|
+
if not isinstance(other, Sample):
|
|
82
|
+
return NotImplemented
|
|
83
|
+
return self.probability == other.probability and self.ids == other.ids
|
|
84
|
+
|
|
85
|
+
def __lt__(self, other: Any) -> bool:
|
|
86
|
+
if not isinstance(other, Sample):
|
|
87
|
+
return NotImplemented
|
|
88
|
+
return self.probability < other.probability
|
|
89
|
+
|
|
90
|
+
def __neg__(self) -> Sample:
|
|
91
|
+
return Sample(-self.probability, self.ids)
|
|
92
|
+
|
|
93
|
+
def __hash__(self):
|
|
94
|
+
return hash(self.ids)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from abc import abstractmethod
|
|
2
|
+
from typing import Protocol, Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Comparable(Protocol):
|
|
6
|
+
@abstractmethod
|
|
7
|
+
def __eq__(self, other: Any) -> bool: ...
|
|
8
|
+
|
|
9
|
+
@abstractmethod
|
|
10
|
+
def __lt__(self, other: Any) -> bool: ...
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Negatable(Protocol):
|
|
14
|
+
def __neg__(self) -> "Negatable": ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ComparableNegatable(Comparable, Negatable, Protocol): ...
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: graphical-sampling
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python package for Graphical Sampling Method
|
|
5
|
+
Author-email: Bardia Panahbehagh <bardia.panah@gmail.com>, Mehdi Mohebbi <mehdi.mohebbi23@gmail.com>, AmirMohammad HosseiniNasab <awmirhn@gmail.com>, Mehdi Hosseini Moghadam <m.h.moghadam1996@gmail.com>
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: k-means-constrained>=0.7.5
|
|
9
|
+
Requires-Dist: matplotlib
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: package-sampling>=0.3.0
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: scikit-learn
|
|
14
|
+
Requires-Dist: scipy
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Graphical Sampling Method - Python Package
|
|
19
|
+
|
|
20
|
+
The Graphical Sampling Method, introduced by Panahbehagh (2025), presents an innovative approach to finite population sampling based on a unique graphical framework. This method allows researchers to visually depict first-order inclusion probabilities (FIP) as bars on a two-dimensional graph. By adjusting the positions of these bars, users can explore a wide range of sampling designs while controlling second-order inclusion probabilities (SIP).
|
|
21
|
+
|
|
22
|
+
This package, `graphical_sampling`, provides tools for implementing the Graphical Sampling Method and is available on PyPI.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
- Create various unequal probability sampling designs with a fixed FIP.
|
|
28
|
+
- Control and explore second-order inclusion probabilities (SIP) through visual manipulation.
|
|
29
|
+
- Incorporate search algorithms, such as A* and Genetic Algorithm, to optimize sampling designs for specific needs (e.g., well-spread or optimal sampling).
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Install the package via pip:
|
|
34
|
+
```bash
|
|
35
|
+
pip install graphical_sampling
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How to Use
|
|
39
|
+
|
|
40
|
+
The package includes core classes and methods to facilitate sampling design creation and optimization. Below is a basic example demonstrating the API.
|
|
41
|
+
|
|
42
|
+
### Example Usage
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import graphical_sampling as gs
|
|
46
|
+
import numpy as np
|
|
47
|
+
|
|
48
|
+
# Set up random generator and sample data
|
|
49
|
+
rng = np.random.default_rng()
|
|
50
|
+
N = 50 # Population size
|
|
51
|
+
x = rng.random(size=N) # Auxiliary variable
|
|
52
|
+
n = 5 # Sample size
|
|
53
|
+
|
|
54
|
+
# Generate initial inclusion probabilities
|
|
55
|
+
inclusion = rng.random(N)
|
|
56
|
+
inclusion *= n / inclusion.sum()
|
|
57
|
+
|
|
58
|
+
# Define initial sampling design and evaluation criteria
|
|
59
|
+
initial_design = gs.Design(inclusion)
|
|
60
|
+
nht = gs.criteria.VarNHT(x, inclusion)
|
|
61
|
+
astar = gs.search.AStar(initial_design, nht, switch_coefficient=1)
|
|
62
|
+
|
|
63
|
+
# Display initial criteria and design
|
|
64
|
+
print("Initial criteria value:", astar.initial_criteria_value)
|
|
65
|
+
astar.initial_design.show()
|
|
66
|
+
|
|
67
|
+
# Run the A* search algorithm to optimize the design
|
|
68
|
+
astar.run(max_iterations=2000, num_new_nodes=10, max_open_set_size=10000, num_changes=1)
|
|
69
|
+
|
|
70
|
+
# Display results after optimization
|
|
71
|
+
print("Best criteria value:", astar.best_criteria_value)
|
|
72
|
+
astar.best_design.show()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Maintainers
|
|
77
|
+
|
|
78
|
+
- Bardia Panahbehagh - [bardia.panah@gmail.com](mailto:bardia.panah@gmail.com)
|
|
79
|
+
- Mehdi Mohebbi - [mehdi.mohebbi23@gmail.com](mailto:mehdi.mohebbi23@gmail.com)
|
|
80
|
+
- AmirMohammad HosseiniNasab - [awmirhn@gmail.com](mailto:awmirhn@gmail.com)
|
|
81
|
+
- Mehdi Hosseini Moghadam - [m.h.moghadam1996@gmail.com](mailto:m.h.moghadam1996@gmail.com)
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
For more details, consult the official paper: Panahbehagh, B. (2025). Graphical Sampling Method.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
graphical_sampling/__init__.py,sha256=bb5HqAY4pgCI8x__PSHsHnadxOTmxkJUzim6clFZLAU,340
|
|
2
|
+
graphical_sampling/design.py,sha256=BtuCSp6tpXB54T_WppN-1JiCUj59cgghHUagj1EfyG8,4083
|
|
3
|
+
graphical_sampling/red_black_tree.py,sha256=IPrlUfSLhXL0VXuPraCboskD7u7vizKX-wDVF5gWlDs,16633
|
|
4
|
+
graphical_sampling/structs.py,sha256=ZDRG-DmH_DDeLzW10dP_nC5AkLc3dAQ3hGFPwedZXYQ,2533
|
|
5
|
+
graphical_sampling/type.py,sha256=TECZp9fU_ydavkKOE-Lds4sx5f2C6WgcWfZ5WKZz8Xo,364
|
|
6
|
+
graphical_sampling/clustering/__init__.py,sha256=K8F84-uCEZYt1iui9vTrUkiKhDYO9dPRN2vwSVcPcMA,588
|
|
7
|
+
graphical_sampling/clustering/aggregate.py,sha256=uVd8Mmp8qFt2C9t9IUk6sYDGihK51T_8onmeQ_XvswM,8037
|
|
8
|
+
graphical_sampling/clustering/dubly_balanced_clustering.py,sha256=2LbNzpZnqoehC1Ou6dEfl8J_75hEj8Y0e6tKYKfZjlI,7791
|
|
9
|
+
graphical_sampling/clustering/one_boundary.py,sha256=pA9nuF8QW-GsJ1UW0Nuuof9iudP9KeH1AZasi8Gp9Y0,9532
|
|
10
|
+
graphical_sampling/clustering/soft_balanced_kmeans.py,sha256=LAEEZ6iFxmQukgS3KalFwKwE_lMVcC58rBahuh1gl-M,6143
|
|
11
|
+
graphical_sampling/criteria/__init__.py,sha256=ruthywH9WgPKcHuPrPDztPzofv8KrriLyrdZsdGgUHg,51
|
|
12
|
+
graphical_sampling/criteria/criteria.py,sha256=ImL0fvutHVhUsMbk7N8RTF1V8VV0o-WGzv5AABWACmw,298
|
|
13
|
+
graphical_sampling/criteria/var_nht.py,sha256=8L15HBfo_Zz1VgF5LQTqpeq-sIAULSAs9We-awL_u6g,700
|
|
14
|
+
graphical_sampling/measure/__init__.py,sha256=2E0yd8sxUso8_fYB7SC4DO0dpfMw7Fz531lSeQbdLHQ,53
|
|
15
|
+
graphical_sampling/measure/density.py,sha256=mGSrdhJWsLZ1VYof7ISQ6E6v5IA_PEAfzXeHuy9eJp0,3882
|
|
16
|
+
graphical_sampling/random/__init__.py,sha256=K8MGYYz9svNRV_gwqXungGFd4f6gA6I1NKxMnJSyRAQ,47
|
|
17
|
+
graphical_sampling/random/generator.py,sha256=ZrWizUpJiqaiK4FXiA-TZcWceDhjC95I_owtPwtF2mQ,10926
|
|
18
|
+
graphical_sampling/sampling/__init__.py,sha256=nTweLFsRqf5klvbowJiVDn0gVax80i69KMUmgP6tkHI,208
|
|
19
|
+
graphical_sampling/sampling/kmeans_spatial_sampling.py,sha256=b6InONfN6tphlyt37RiQok4K4E2otymVXT1_PZYN0hs,2062
|
|
20
|
+
graphical_sampling/sampling/population.py,sha256=-wmXYJr6Ren6r7LAe4fB4ogxL_lp-KHVaxVkXHUrT7k,7885
|
|
21
|
+
graphical_sampling/sampling/random_sampling.py,sha256=JARWmciRuVObVM0WQqj-uAr2D2Lj0IMjgr0Ti0yaQro,538
|
|
22
|
+
graphical_sampling/search/__init__.py,sha256=Ag3JJcApUbE_Lct9qExrTYvHmxjlpq2x_yEXtwRwI2s,47
|
|
23
|
+
graphical_sampling/search/astar.py,sha256=wvMRGjjMkBC8kqyJ2wgc5OJh3snjPs0-SBSHmUdgkyM,3815
|
|
24
|
+
graphical_sampling-0.1.0.dist-info/METADATA,sha256=Ec2nHamNHbsIvgLgCr7AI2AuFIQID4HZmAKW5P-KHH0,3220
|
|
25
|
+
graphical_sampling-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
26
|
+
graphical_sampling-0.1.0.dist-info/licenses/LICENSE,sha256=1xF-ubUdk0b37lCULLgZ_WltcT3__rvChYoNxtwMGJE,1070
|
|
27
|
+
graphical_sampling-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Mehdi Mohebbi
|
|
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.
|