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,475 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from collections.abc import Iterator
|
|
3
|
+
from typing import TypeVar, Generic
|
|
4
|
+
|
|
5
|
+
from .type import Comparable
|
|
6
|
+
|
|
7
|
+
C = TypeVar("C", bound=Comparable)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RedBlackTree(Generic[C]):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
label: C | None = None,
|
|
14
|
+
color: int = 0,
|
|
15
|
+
parent: RedBlackTree | None = None,
|
|
16
|
+
left: RedBlackTree | None = None,
|
|
17
|
+
right: RedBlackTree | None = None,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Initialize a new Red-Black Tree node with the given values:
|
|
20
|
+
label: The value associated with this node
|
|
21
|
+
color: 0 if black, 1 if red
|
|
22
|
+
parent: The parent to this node
|
|
23
|
+
left: This node's left child
|
|
24
|
+
right: This node's right child
|
|
25
|
+
"""
|
|
26
|
+
self.label = label
|
|
27
|
+
self.parent = parent
|
|
28
|
+
self.left = left
|
|
29
|
+
self.right = right
|
|
30
|
+
self.color = color
|
|
31
|
+
|
|
32
|
+
# Here are functions which are specific to red-black trees
|
|
33
|
+
|
|
34
|
+
def rotate_left(self) -> RedBlackTree:
|
|
35
|
+
"""Rotate the subtree rooted at this node to the left and
|
|
36
|
+
returns the new root to this subtree.
|
|
37
|
+
Performing one rotation can be done in O(1).
|
|
38
|
+
"""
|
|
39
|
+
parent = self.parent
|
|
40
|
+
right = self.right
|
|
41
|
+
if right is None:
|
|
42
|
+
return self
|
|
43
|
+
self.right = right.left
|
|
44
|
+
if self.right:
|
|
45
|
+
self.right.parent = self
|
|
46
|
+
self.parent = right
|
|
47
|
+
right.left = self
|
|
48
|
+
if parent is not None:
|
|
49
|
+
if parent.left == self:
|
|
50
|
+
parent.left = right
|
|
51
|
+
else:
|
|
52
|
+
parent.right = right
|
|
53
|
+
right.parent = parent
|
|
54
|
+
return right
|
|
55
|
+
|
|
56
|
+
def rotate_right(self) -> RedBlackTree:
|
|
57
|
+
"""Rotate the subtree rooted at this node to the right and
|
|
58
|
+
returns the new root to this subtree.
|
|
59
|
+
Performing one rotation can be done in O(1).
|
|
60
|
+
"""
|
|
61
|
+
if self.left is None:
|
|
62
|
+
return self
|
|
63
|
+
parent = self.parent
|
|
64
|
+
left = self.left
|
|
65
|
+
self.left = left.right
|
|
66
|
+
if self.left:
|
|
67
|
+
self.left.parent = self
|
|
68
|
+
self.parent = left
|
|
69
|
+
left.right = self
|
|
70
|
+
if parent is not None:
|
|
71
|
+
if parent.right is self:
|
|
72
|
+
parent.right = left
|
|
73
|
+
else:
|
|
74
|
+
parent.left = left
|
|
75
|
+
left.parent = parent
|
|
76
|
+
return left
|
|
77
|
+
|
|
78
|
+
def insert(self, label: C) -> RedBlackTree:
|
|
79
|
+
"""Inserts label into the subtree rooted at self, performs any
|
|
80
|
+
rotations necessary to maintain balance, and then returns the
|
|
81
|
+
new root to this subtree (likely self).
|
|
82
|
+
This is guaranteed to run in O(log(n)) time.
|
|
83
|
+
"""
|
|
84
|
+
if self.label is None:
|
|
85
|
+
# Only possible with an empty tree
|
|
86
|
+
self.label = label
|
|
87
|
+
return self
|
|
88
|
+
if self.label == label:
|
|
89
|
+
return self
|
|
90
|
+
elif self.label > label:
|
|
91
|
+
if self.left:
|
|
92
|
+
self.left.insert(label)
|
|
93
|
+
else:
|
|
94
|
+
self.left = RedBlackTree(label, 1, self)
|
|
95
|
+
self.left._insert_repair()
|
|
96
|
+
elif self.right:
|
|
97
|
+
self.right.insert(label)
|
|
98
|
+
else:
|
|
99
|
+
self.right = RedBlackTree(label, 1, self)
|
|
100
|
+
self.right._insert_repair()
|
|
101
|
+
return self.parent or self
|
|
102
|
+
|
|
103
|
+
def _insert_repair(self) -> None:
|
|
104
|
+
"""Repair the coloring from inserting into a tree."""
|
|
105
|
+
if self.parent is None:
|
|
106
|
+
# This node is the root, so it just needs to be black
|
|
107
|
+
self.color = 0
|
|
108
|
+
elif color(self.parent) == 0:
|
|
109
|
+
# If the parent is black, then it just needs to be red
|
|
110
|
+
self.color = 1
|
|
111
|
+
else:
|
|
112
|
+
uncle = self.parent.sibling
|
|
113
|
+
if color(uncle) == 0:
|
|
114
|
+
if self.is_left() and self.parent.is_right():
|
|
115
|
+
self.parent.rotate_right()
|
|
116
|
+
if self.right:
|
|
117
|
+
self.right._insert_repair()
|
|
118
|
+
elif self.is_right() and self.parent.is_left():
|
|
119
|
+
self.parent.rotate_left()
|
|
120
|
+
if self.left:
|
|
121
|
+
self.left._insert_repair()
|
|
122
|
+
elif self.is_left():
|
|
123
|
+
if self.grandparent:
|
|
124
|
+
self.grandparent.rotate_right()
|
|
125
|
+
self.parent.color = 0
|
|
126
|
+
if self.parent.right:
|
|
127
|
+
self.parent.right.color = 1
|
|
128
|
+
else:
|
|
129
|
+
if self.grandparent:
|
|
130
|
+
self.grandparent.rotate_left()
|
|
131
|
+
self.parent.color = 0
|
|
132
|
+
if self.parent.left:
|
|
133
|
+
self.parent.left.color = 1
|
|
134
|
+
else:
|
|
135
|
+
self.parent.color = 0
|
|
136
|
+
if uncle and self.grandparent:
|
|
137
|
+
uncle.color = 0
|
|
138
|
+
self.grandparent.color = 1
|
|
139
|
+
self.grandparent._insert_repair()
|
|
140
|
+
|
|
141
|
+
def remove(self, label: C) -> RedBlackTree:
|
|
142
|
+
"""Remove label from this tree."""
|
|
143
|
+
if self.label == label:
|
|
144
|
+
if self.left and self.right:
|
|
145
|
+
# It's easier to balance a node with at most one child,
|
|
146
|
+
# so we replace this node with the greatest one less than
|
|
147
|
+
# it and remove that.
|
|
148
|
+
value = self.left.get_max()
|
|
149
|
+
if value is not None:
|
|
150
|
+
self.label = value
|
|
151
|
+
self.left.remove(value)
|
|
152
|
+
else:
|
|
153
|
+
# This node has at most one non-None child, so we don't
|
|
154
|
+
# need to replace
|
|
155
|
+
child = self.left or self.right
|
|
156
|
+
if self.color == 1:
|
|
157
|
+
# This node is red, and its child is black
|
|
158
|
+
# The only way this happens to a node with one child
|
|
159
|
+
# is if both children are None leaves.
|
|
160
|
+
# We can just remove this node and call it a day.
|
|
161
|
+
if self.parent:
|
|
162
|
+
if self.is_left():
|
|
163
|
+
self.parent.left = None
|
|
164
|
+
else:
|
|
165
|
+
self.parent.right = None
|
|
166
|
+
# The node is black
|
|
167
|
+
elif child is None:
|
|
168
|
+
# This node and its child are black
|
|
169
|
+
if self.parent is None:
|
|
170
|
+
# The tree is now empty
|
|
171
|
+
return RedBlackTree(None)
|
|
172
|
+
else:
|
|
173
|
+
self._remove_repair()
|
|
174
|
+
if self.is_left():
|
|
175
|
+
self.parent.left = None
|
|
176
|
+
else:
|
|
177
|
+
self.parent.right = None
|
|
178
|
+
self.parent = None
|
|
179
|
+
else:
|
|
180
|
+
# This node is black and its child is red
|
|
181
|
+
# Move the child node here and make it black
|
|
182
|
+
self.label = child.label
|
|
183
|
+
self.left = child.left
|
|
184
|
+
self.right = child.right
|
|
185
|
+
if self.left:
|
|
186
|
+
self.left.parent = self
|
|
187
|
+
if self.right:
|
|
188
|
+
self.right.parent = self
|
|
189
|
+
elif self.label is not None and self.label > label:
|
|
190
|
+
if self.left:
|
|
191
|
+
self.left.remove(label)
|
|
192
|
+
elif self.right:
|
|
193
|
+
self.right.remove(label)
|
|
194
|
+
return self.parent or self
|
|
195
|
+
|
|
196
|
+
def _remove_repair(self) -> None:
|
|
197
|
+
"""Repair the coloring of the tree that may have been messed up."""
|
|
198
|
+
if (
|
|
199
|
+
self.parent is None
|
|
200
|
+
or self.sibling is None
|
|
201
|
+
or self.parent.sibling is None
|
|
202
|
+
or self.grandparent is None
|
|
203
|
+
):
|
|
204
|
+
return
|
|
205
|
+
if color(self.sibling) == 1:
|
|
206
|
+
self.sibling.color = 0
|
|
207
|
+
self.parent.color = 1
|
|
208
|
+
if self.is_left():
|
|
209
|
+
self.parent.rotate_left()
|
|
210
|
+
else:
|
|
211
|
+
self.parent.rotate_right()
|
|
212
|
+
if (
|
|
213
|
+
color(self.parent) == 0
|
|
214
|
+
and color(self.sibling) == 0
|
|
215
|
+
and color(self.sibling.left) == 0
|
|
216
|
+
and color(self.sibling.right) == 0
|
|
217
|
+
):
|
|
218
|
+
self.sibling.color = 1
|
|
219
|
+
self.parent._remove_repair()
|
|
220
|
+
return
|
|
221
|
+
if (
|
|
222
|
+
color(self.parent) == 1
|
|
223
|
+
and color(self.sibling) == 0
|
|
224
|
+
and color(self.sibling.left) == 0
|
|
225
|
+
and color(self.sibling.right) == 0
|
|
226
|
+
):
|
|
227
|
+
self.sibling.color = 1
|
|
228
|
+
self.parent.color = 0
|
|
229
|
+
return
|
|
230
|
+
if (
|
|
231
|
+
self.is_left()
|
|
232
|
+
and color(self.sibling) == 0
|
|
233
|
+
and color(self.sibling.right) == 0
|
|
234
|
+
and color(self.sibling.left) == 1
|
|
235
|
+
):
|
|
236
|
+
self.sibling.rotate_right()
|
|
237
|
+
self.sibling.color = 0
|
|
238
|
+
if self.sibling.right:
|
|
239
|
+
self.sibling.right.color = 1
|
|
240
|
+
if (
|
|
241
|
+
self.is_right()
|
|
242
|
+
and color(self.sibling) == 0
|
|
243
|
+
and color(self.sibling.right) == 1
|
|
244
|
+
and color(self.sibling.left) == 0
|
|
245
|
+
):
|
|
246
|
+
self.sibling.rotate_left()
|
|
247
|
+
self.sibling.color = 0
|
|
248
|
+
if self.sibling.left:
|
|
249
|
+
self.sibling.left.color = 1
|
|
250
|
+
if (
|
|
251
|
+
self.is_left()
|
|
252
|
+
and color(self.sibling) == 0
|
|
253
|
+
and color(self.sibling.right) == 1
|
|
254
|
+
):
|
|
255
|
+
self.parent.rotate_left()
|
|
256
|
+
self.grandparent.color = self.parent.color
|
|
257
|
+
self.parent.color = 0
|
|
258
|
+
self.parent.sibling.color = 0
|
|
259
|
+
if (
|
|
260
|
+
self.is_right()
|
|
261
|
+
and color(self.sibling) == 0
|
|
262
|
+
and color(self.sibling.left) == 1
|
|
263
|
+
):
|
|
264
|
+
self.parent.rotate_right()
|
|
265
|
+
self.grandparent.color = self.parent.color
|
|
266
|
+
self.parent.color = 0
|
|
267
|
+
self.parent.sibling.color = 0
|
|
268
|
+
|
|
269
|
+
def check_coloring(self) -> bool:
|
|
270
|
+
"""A helper function to recursively check Property 4 of a
|
|
271
|
+
Red-Black Tree. See check_color_properties for more info.
|
|
272
|
+
"""
|
|
273
|
+
if self.color == 1 and 1 in (color(self.left), color(self.right)):
|
|
274
|
+
return False
|
|
275
|
+
if self.left and not self.left.check_coloring():
|
|
276
|
+
return False
|
|
277
|
+
return not (self.right and not self.right.check_coloring())
|
|
278
|
+
|
|
279
|
+
def black_height(self) -> int | None:
|
|
280
|
+
"""Returns the number of black nodes from this node to the
|
|
281
|
+
leaves of the tree, or None if there isn't one such value (the
|
|
282
|
+
tree is color incorrectly).
|
|
283
|
+
"""
|
|
284
|
+
if self is None or self.left is None or self.right is None:
|
|
285
|
+
# If we're already at a leaf, there is no path
|
|
286
|
+
return 1
|
|
287
|
+
left = RedBlackTree.black_height(self.left)
|
|
288
|
+
right = RedBlackTree.black_height(self.right)
|
|
289
|
+
if left is None or right is None:
|
|
290
|
+
# There are issues with coloring below children nodes
|
|
291
|
+
return None
|
|
292
|
+
if left != right:
|
|
293
|
+
# The two children have unequal depths
|
|
294
|
+
return None
|
|
295
|
+
# Return the black depth of children, plus one if this node is
|
|
296
|
+
# black
|
|
297
|
+
return left + (1 - self.color)
|
|
298
|
+
|
|
299
|
+
# Here are functions which are general to all binary search trees
|
|
300
|
+
|
|
301
|
+
def __contains__(self, label: C) -> bool:
|
|
302
|
+
"""Search through the tree for label, returning True iff it is
|
|
303
|
+
found somewhere in the tree.
|
|
304
|
+
Guaranteed to run in O(log(n)) time.
|
|
305
|
+
"""
|
|
306
|
+
return self.search(label) is not None
|
|
307
|
+
|
|
308
|
+
def search(self, label: C) -> RedBlackTree | None:
|
|
309
|
+
"""Search through the tree for label, returning its node if
|
|
310
|
+
it's found, and None otherwise.
|
|
311
|
+
This method is guaranteed to run in O(log(n)) time.
|
|
312
|
+
"""
|
|
313
|
+
if self.label == label:
|
|
314
|
+
return self
|
|
315
|
+
elif self.label is not None and label > self.label:
|
|
316
|
+
if self.right is None:
|
|
317
|
+
return None
|
|
318
|
+
else:
|
|
319
|
+
return self.right.search(label)
|
|
320
|
+
elif self.left is None:
|
|
321
|
+
return None
|
|
322
|
+
else:
|
|
323
|
+
return self.left.search(label)
|
|
324
|
+
|
|
325
|
+
def floor(self, label: C) -> C | None:
|
|
326
|
+
"""Returns the largest element in this tree which is at most label.
|
|
327
|
+
This method is guaranteed to run in O(log(n)) time."""
|
|
328
|
+
if self.label == label:
|
|
329
|
+
return self.label
|
|
330
|
+
elif self.label is not None and self.label > label:
|
|
331
|
+
if self.left:
|
|
332
|
+
return self.left.floor(label)
|
|
333
|
+
else:
|
|
334
|
+
return None
|
|
335
|
+
else:
|
|
336
|
+
if self.right:
|
|
337
|
+
attempt = self.right.floor(label)
|
|
338
|
+
if attempt is not None:
|
|
339
|
+
return attempt
|
|
340
|
+
return self.label
|
|
341
|
+
|
|
342
|
+
def ceil(self, label: C) -> C | None:
|
|
343
|
+
"""Returns the smallest element in this tree which is at least label.
|
|
344
|
+
This method is guaranteed to run in O(log(n)) time.
|
|
345
|
+
"""
|
|
346
|
+
if self.label == label:
|
|
347
|
+
return self.label
|
|
348
|
+
elif self.label is not None and self.label < label:
|
|
349
|
+
if self.right:
|
|
350
|
+
return self.right.ceil(label)
|
|
351
|
+
else:
|
|
352
|
+
return None
|
|
353
|
+
else:
|
|
354
|
+
if self.left:
|
|
355
|
+
attempt = self.left.ceil(label)
|
|
356
|
+
if attempt is not None:
|
|
357
|
+
return attempt
|
|
358
|
+
return self.label
|
|
359
|
+
|
|
360
|
+
def get_max(self) -> C | None:
|
|
361
|
+
"""Returns the largest element in this tree.
|
|
362
|
+
This method is guaranteed to run in O(log(n)) time.
|
|
363
|
+
"""
|
|
364
|
+
if self.right:
|
|
365
|
+
# Go as far right as possible
|
|
366
|
+
return self.right.get_max()
|
|
367
|
+
else:
|
|
368
|
+
return self.label
|
|
369
|
+
|
|
370
|
+
def get_min(self) -> C | None:
|
|
371
|
+
"""Returns the smallest element in this tree.
|
|
372
|
+
This method is guaranteed to run in O(log(n)) time.
|
|
373
|
+
"""
|
|
374
|
+
if self.left:
|
|
375
|
+
# Go as far left as possible
|
|
376
|
+
return self.left.get_min()
|
|
377
|
+
else:
|
|
378
|
+
return self.label
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def grandparent(self) -> RedBlackTree | None:
|
|
382
|
+
"""Get the current node's grandparent, or None if it doesn't exist."""
|
|
383
|
+
if self.parent is None:
|
|
384
|
+
return None
|
|
385
|
+
else:
|
|
386
|
+
return self.parent.parent
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def sibling(self) -> RedBlackTree | None:
|
|
390
|
+
"""Get the current node's sibling, or None if it doesn't exist."""
|
|
391
|
+
if self.parent is None:
|
|
392
|
+
return None
|
|
393
|
+
elif self.parent.left is self:
|
|
394
|
+
return self.parent.right
|
|
395
|
+
else:
|
|
396
|
+
return self.parent.left
|
|
397
|
+
|
|
398
|
+
def is_left(self) -> bool:
|
|
399
|
+
"""Returns true iff this node is the left child of its parent."""
|
|
400
|
+
if self.parent is None:
|
|
401
|
+
return False
|
|
402
|
+
return self.parent.left is self
|
|
403
|
+
|
|
404
|
+
def is_right(self) -> bool:
|
|
405
|
+
"""Returns true iff this node is the right child of its parent."""
|
|
406
|
+
if self.parent is None:
|
|
407
|
+
return False
|
|
408
|
+
return self.parent.right is self
|
|
409
|
+
|
|
410
|
+
def __bool__(self) -> bool:
|
|
411
|
+
return True
|
|
412
|
+
|
|
413
|
+
def __len__(self) -> int:
|
|
414
|
+
"""
|
|
415
|
+
Return the number of nodes in this tree.
|
|
416
|
+
"""
|
|
417
|
+
ln = 1
|
|
418
|
+
if self.left:
|
|
419
|
+
ln += len(self.left)
|
|
420
|
+
if self.right:
|
|
421
|
+
ln += len(self.right)
|
|
422
|
+
return ln
|
|
423
|
+
|
|
424
|
+
def preorder_traverse(self) -> Iterator[C | None]:
|
|
425
|
+
yield self.label
|
|
426
|
+
if self.left:
|
|
427
|
+
yield from self.left.preorder_traverse()
|
|
428
|
+
if self.right:
|
|
429
|
+
yield from self.right.preorder_traverse()
|
|
430
|
+
|
|
431
|
+
def inorder_traverse(self) -> Iterator[C | None]:
|
|
432
|
+
if self.left:
|
|
433
|
+
yield from self.left.inorder_traverse()
|
|
434
|
+
yield self.label
|
|
435
|
+
if self.right:
|
|
436
|
+
yield from self.right.inorder_traverse()
|
|
437
|
+
|
|
438
|
+
def postorder_traverse(self) -> Iterator[C | None]:
|
|
439
|
+
if self.left:
|
|
440
|
+
yield from self.left.postorder_traverse()
|
|
441
|
+
if self.right:
|
|
442
|
+
yield from self.right.postorder_traverse()
|
|
443
|
+
yield self.label
|
|
444
|
+
|
|
445
|
+
def __repr__(self) -> str:
|
|
446
|
+
from pprint import pformat
|
|
447
|
+
|
|
448
|
+
if self.left is None and self.right is None:
|
|
449
|
+
return f"'{self.label} {(self.color and 'red') or 'blk'}'"
|
|
450
|
+
return pformat(
|
|
451
|
+
{
|
|
452
|
+
f"{self.label} {(self.color and 'red') or 'blk'}": (
|
|
453
|
+
self.left,
|
|
454
|
+
self.right,
|
|
455
|
+
)
|
|
456
|
+
},
|
|
457
|
+
indent=1,
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def __eq__(self, other: object) -> bool:
|
|
461
|
+
"""Test if two trees are equal."""
|
|
462
|
+
if not isinstance(other, RedBlackTree):
|
|
463
|
+
return NotImplemented
|
|
464
|
+
if self.label == other.label:
|
|
465
|
+
return self.left == other.left and self.right == other.right
|
|
466
|
+
else:
|
|
467
|
+
return False
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def color(node: RedBlackTree | None) -> int:
|
|
471
|
+
"""Returns the color of a node, allowing for None leaves."""
|
|
472
|
+
if node is None:
|
|
473
|
+
return 0
|
|
474
|
+
else:
|
|
475
|
+
return node.color
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy.typing import NDArray
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from . import Population
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class KMeansSpatialSampling:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
coordinate: NDArray,
|
|
12
|
+
inclusion_probability: NDArray,
|
|
13
|
+
*,
|
|
14
|
+
n: int,
|
|
15
|
+
n_zones: int | tuple[int, int],
|
|
16
|
+
tolerance: int,
|
|
17
|
+
) -> None:
|
|
18
|
+
self.coords = coordinate
|
|
19
|
+
self.probs = inclusion_probability
|
|
20
|
+
self.n = n
|
|
21
|
+
self.n_zones = self._pair(n_zones)
|
|
22
|
+
self.tolerance = tolerance
|
|
23
|
+
|
|
24
|
+
self.population = Population(
|
|
25
|
+
self.coords,
|
|
26
|
+
self.probs,
|
|
27
|
+
n_clusters=self.n,
|
|
28
|
+
n_zones=self.n_zones,
|
|
29
|
+
tolerance=self.tolerance,
|
|
30
|
+
)
|
|
31
|
+
self.rng = np.random.default_rng()
|
|
32
|
+
|
|
33
|
+
def _pair(self, n: int | tuple[int, int]) -> tuple[int, int]:
|
|
34
|
+
if isinstance(n, int):
|
|
35
|
+
return (n, n)
|
|
36
|
+
else:
|
|
37
|
+
return n
|
|
38
|
+
|
|
39
|
+
def sample(self, n_samples: int):
|
|
40
|
+
samples = np.zeros((n_samples, self.n), dtype=int)
|
|
41
|
+
for i in range(n_samples):
|
|
42
|
+
random_number = self.rng.random()
|
|
43
|
+
zone_index = np.searchsorted(
|
|
44
|
+
np.arange(1, step=round(1 / np.prod(self.n_zones), self.tolerance)),
|
|
45
|
+
random_number,
|
|
46
|
+
side="right",
|
|
47
|
+
)
|
|
48
|
+
for j, cluster in enumerate(self.population.clusters):
|
|
49
|
+
unit_index = np.searchsorted(
|
|
50
|
+
cluster.zones[zone_index - 1].units[:, 3].cumsum()
|
|
51
|
+
+ (zone_index - 1)
|
|
52
|
+
* round(1 / np.prod(self.n_zones), self.tolerance),
|
|
53
|
+
random_number,
|
|
54
|
+
side="right",
|
|
55
|
+
)
|
|
56
|
+
if np.prod(self.n_zones) != len(cluster.zones):
|
|
57
|
+
warning_msg = (f"Warning: Cluster {j} has {len(cluster.zones)} zones, "
|
|
58
|
+
f"but expected {np.prod(self.n_zones)} based on n_zones={self.n_zones}")
|
|
59
|
+
print(warning_msg)
|
|
60
|
+
samples[i, j] = cluster.zones[zone_index - 1].units[unit_index, 0]
|
|
61
|
+
return samples
|