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.
@@ -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,6 @@
1
+ from .population import Population
2
+ from .kmeans_spatial_sampling import KMeansSpatialSampling
3
+ from .random_sampling import RandomSampling
4
+
5
+
6
+ __all__ = ["Population", "KMeansSpatialSampling", "RandomSampling"]
@@ -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