tinto 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.
tinto/__init__.py ADDED
@@ -0,0 +1,12 @@
1
+ """Top-level package for Tinto."""
2
+
3
+ from importlib.metadata import metadata
4
+
5
+ from tinto.experiments import Experiment, Iterator, ProductIterator
6
+
7
+ infos = metadata(__name__)
8
+ __version__ = infos["Version"]
9
+ __author__ = "Fabien Mathieu"
10
+ __email__ = "fabien.mathieu@normalesup.org"
11
+
12
+ __all__ = ["Experiment", "Iterator", "ProductIterator"]
tinto/configs.py ADDED
@@ -0,0 +1,431 @@
1
+ """Coloring configuration of a :class:`~tinto.graphs.Graph`.
2
+
3
+ This module defines :func:`make`, a factory that builds a :mod:`numba`
4
+ :class:`~numba.experimental.jitclass` container. A :class:`Config` tracks the
5
+ color assigned to each node of a graph and maintains, in sync, the set of
6
+ nodes for which a user-supplied *guard* predicate currently fires.
7
+ """
8
+
9
+ from functools import lru_cache
10
+
11
+ from numba.experimental import jitclass
12
+ from numba import int64, bool
13
+ import numpy as np
14
+
15
+ from tinto.guards import color_clash
16
+ from tinto.graphs import Graph
17
+
18
+
19
+ def make(guard=None):
20
+ """Build a :class:`Config` jitclass wired to `guard`.
21
+
22
+ Because :mod:`numba` requires every :func:`~numba.experimental.jitclass`
23
+ method to be specialized at class-definition time, the guard predicate is
24
+ captured here as a closure rather than passed at instantiation.
25
+
26
+ Parameters
27
+ ----------
28
+ guard: callable or ``None``, default=``None``
29
+ Predicate ``guard(config, i) -> bool`` deciding whether node `i` is
30
+ *enabled*. When ``None``, :func:`~tinto.guards.color_clash` is
31
+ used, which enables a node iff it shares its color with a neighbor.
32
+
33
+ Returns
34
+ -------
35
+ type
36
+ A jitclass implementing the API documented on :class:`Config`.
37
+
38
+ Notes
39
+ -----
40
+ Two calls with the same (normalized) guard return the same compiled
41
+ jitclass — the class is built only once per distinct guard. Guards
42
+ are keyed by identity; ``None`` and :func:`~tinto.guards.color_clash`
43
+ are treated as the same key. Use
44
+ ``tinto.configs._make_config.cache_clear()`` to drop the cache in the
45
+ rare cases it is needed.
46
+
47
+ Examples
48
+ --------
49
+ >>> from tinto.graphs import line
50
+ >>> from tinto.configs import make
51
+ >>> Config = make()
52
+ >>> c = Config(line(4))
53
+ >>> c.total
54
+ 4
55
+
56
+ Passing an explicit guard wires the returned class to it rather
57
+ than to :func:`~tinto.guards.color_clash`:
58
+
59
+ >>> from tinto.guards import color_min
60
+ >>> ConfigMin = make(color_min)
61
+ >>> cm = ConfigMin(line(4))
62
+ >>> cm.total > 0
63
+ True
64
+
65
+ The number of available colors is adjustable post-construction:
66
+
67
+ >>> c.set_k(5)
68
+ >>> c.k
69
+ 5
70
+ >>> c.local_count.shape
71
+ (5,)
72
+
73
+ And :meth:`Config.shuffle_ids` reseeds the per-Config id permutation
74
+ without rebuilding the graph — useful between replicas when a guard
75
+ breaks ties on :attr:`ids`. With the default guard
76
+ (:func:`~tinto.guards.color_clash`, which ignores ids), :attr:`ids`
77
+ starts empty and is populated on demand:
78
+
79
+ >>> c.ids.tolist()
80
+ []
81
+ >>> c.shuffle_ids()
82
+ >>> sorted(c.ids.tolist())
83
+ [0, 1, 2, 3]
84
+
85
+ A guard that reads ids (e.g. :func:`~tinto.guards.color_clash_tie`)
86
+ flips the :attr:`Config.uses_ids` flag and makes
87
+ :class:`Config.__init__` draw a fresh permutation directly, so that
88
+ :meth:`Config.check_all` sees a valid tie-breaking order on the
89
+ first call:
90
+
91
+ >>> from tinto.guards import color_clash_tie
92
+ >>> ConfigTie = make(color_clash_tie)
93
+ >>> ConfigTie.uses_ids
94
+ True
95
+ >>> ct = ConfigTie(line(4))
96
+ >>> sorted(ct.ids.tolist())
97
+ [0, 1, 2, 3]
98
+
99
+ Repeated calls with the same (normalized) guard hit the cache:
100
+
101
+ >>> make() is make()
102
+ True
103
+ >>> make() is make(color_clash)
104
+ True
105
+ >>> make(color_clash) is make(color_min)
106
+ False
107
+ """
108
+ if guard is None:
109
+ guard = color_clash
110
+ return _make_config(guard)
111
+
112
+
113
+ @lru_cache(maxsize=None)
114
+ def _make_config(guard):
115
+ uses_ids = getattr(guard, "uses_ids", True)
116
+
117
+ @jitclass
118
+ class Config:
119
+ """Dynamic coloring configuration of a :class:`~tinto.graphs.Graph`.
120
+
121
+ A node is said to be *enabled* when the guard predicate captured at
122
+ :func:`make` time fires on it. :class:`Config` keeps the
123
+ enabled mask :attr:`enabled` and the enabled count :attr:`total` in
124
+ sync with the current coloring :attr:`colors`; they can be recomputed
125
+ from scratch by :meth:`check_all`, or updated one node at a time by
126
+ :meth:`check`.
127
+
128
+ Parameters
129
+ ----------
130
+ graph: :class:`~tinto.graphs.Graph`
131
+ Underlying graph. All nodes start with color ``0``.
132
+ k: :class:`int`, default=0
133
+ Number of available colors. When left at the default, it is set
134
+ to ``graph.max_degree + 1`` so that a proper coloring is always
135
+ reachable by a greedy algorithm. Any strictly positive value
136
+ overrides this default.
137
+
138
+ Attributes
139
+ ----------
140
+ graph: :class:`~tinto.graphs.Graph`
141
+ See the corresponding parameter.
142
+ colors: :class:`~numpy.ndarray` of :class:`int64`, shape (n,)
143
+ Current color of each node. Values lie in ``range(k)``.
144
+ enabled: :class:`~numpy.ndarray` of :class:`bool`, shape (n,)
145
+ Boolean mask: ``enabled[i]`` is ``True`` iff the guard fires on
146
+ node `i` under the current coloring.
147
+ total: :class:`int64`
148
+ Number of enabled nodes, i.e. ``enabled.sum()``.
149
+ k: :class:`int64`
150
+ Number of available colors.
151
+ local_count: :class:`~numpy.ndarray` of :class:`int64`, shape (k,)
152
+ Scratch buffer holding the color histogram of a node's neighbors,
153
+ (re)filled by :meth:`count` and reused by some guards.
154
+
155
+ Examples
156
+ --------
157
+ On a line of 4 nodes all starting at color ``0``, every node clashes
158
+ with a neighbor under the default guard:
159
+
160
+ >>> from tinto.graphs import line
161
+ >>> from tinto.configs import make
162
+ >>> from tinto.io import set_seed
163
+ >>> set_seed(0)
164
+ >>> Config = make()
165
+ >>> c = Config(line(4))
166
+ >>> c.k
167
+ 3
168
+ >>> c.total
169
+ 4
170
+ >>> c.enabled.tolist()
171
+ [True, True, True, True]
172
+ >>> c.ids.tolist()
173
+ []
174
+ >>> c.shuffle_ids()
175
+ >>> sorted(c.ids.tolist())
176
+ [0, 1, 2, 3]
177
+ """
178
+
179
+ graph: Graph
180
+ enabled: bool[:] # type: ignore
181
+ total: int64 # type: ignore
182
+ k: int64 # type: ignore
183
+ colors: int64[:] # type: ignore
184
+ local_count: int64[:] # type: ignore
185
+ ids: int64[:] # type: ignore
186
+ uses_ids: bool # type: ignore
187
+
188
+ def __init__(self, graph, k=0):
189
+ self.graph = graph
190
+ self.uses_ids = uses_ids
191
+ if uses_ids:
192
+ self.ids = np.random.permutation(graph.n)
193
+ else:
194
+ self.ids = np.empty(0, dtype=np.int64)
195
+ self.enabled = np.zeros(graph.n, np.bool)
196
+ self.colors = np.zeros(graph.n, dtype=np.int64)
197
+ self.k = k if k > 0 else graph.max_degree + 1
198
+ self.local_count = np.zeros(self.k, dtype=np.int64)
199
+ self.total = 0
200
+ self.check_all()
201
+
202
+ def shuffle_ids(self):
203
+ """Replace :attr:`ids` by a uniform random permutation of ``range(graph.n)``.
204
+
205
+ Useful between replicas when the guard or colorer relies on
206
+ node IDs to break ties, as in
207
+ :func:`~tinto.guards.color_clash_tie`, so that successive runs
208
+ do not share the same tie-breaking order. When the guard does
209
+ not use ids, :attr:`ids` starts empty (see
210
+ :class:`tinto.configs.Config`); calling this method populates
211
+ it on demand.
212
+
213
+ Examples
214
+ --------
215
+ >>> from tinto.graphs import line
216
+ >>> from tinto.configs import make
217
+ >>> Config = make()
218
+ >>> c = Config(line(4))
219
+ >>> c.ids.tolist()
220
+ []
221
+ >>> c.shuffle_ids()
222
+ >>> sorted(c.ids.tolist())
223
+ [0, 1, 2, 3]
224
+ """
225
+ self.ids = np.random.permutation(self.graph.n)
226
+
227
+ def set_k(self, k):
228
+ """Change the number of available colors and reallocate :attr:`local_count`.
229
+
230
+ Existing values in :attr:`colors` are left untouched, so callers
231
+ should follow up with :meth:`color_uniform` or
232
+ :meth:`color_randomize` to ensure every node stays in ``range(k)``.
233
+
234
+ Parameters
235
+ ----------
236
+ k: :class:`int`
237
+ New number of colors. Must be strictly positive.
238
+
239
+ Examples
240
+ --------
241
+ >>> from tinto.graphs import line
242
+ >>> from tinto.configs import make
243
+ >>> Config = make()
244
+ >>> c = Config(line(4))
245
+ >>> c.k
246
+ 3
247
+ >>> c.set_k(5)
248
+ >>> c.k
249
+ 5
250
+ >>> c.local_count.shape
251
+ (5,)
252
+ """
253
+ self.k = k
254
+ self.local_count = np.zeros(self.k, dtype=np.int64)
255
+
256
+ def count(self, i):
257
+ """Return the color histogram of the neighbors of node `i`.
258
+
259
+ The scratch buffer :attr:`local_count` is zeroed and refilled,
260
+ then returned as a view: read the result before the next call to
261
+ :meth:`count`, which will overwrite it.
262
+
263
+ Parameters
264
+ ----------
265
+ i: :class:`int`
266
+ Node index, with ``0 <= i < graph.n``.
267
+
268
+ Returns
269
+ -------
270
+ :class:`~numpy.ndarray` of :class:`int64`, shape (k,)
271
+ View of :attr:`local_count` where entry `c` holds the number
272
+ of neighbors of `i` currently colored with `c`.
273
+
274
+ Examples
275
+ --------
276
+ >>> from tinto.graphs import line
277
+ >>> from tinto.configs import make
278
+ >>> Config = make()
279
+ >>> c = Config(line(4))
280
+ >>> c.colors[:] = [0, 1, 2, 0]
281
+ >>> c.check_all()
282
+ >>> c.count(1).tolist()
283
+ [1, 0, 1]
284
+ """
285
+ self.local_count[:] = 0
286
+ for j in self.graph.neighbors(i):
287
+ self.local_count[self.colors[j]] += 1
288
+ return self.local_count
289
+
290
+ def check(self, i):
291
+ """Refresh ``enabled[i]`` and adjust :attr:`total` accordingly.
292
+
293
+ Evaluates the guard on node `i`, stores the result in
294
+ ``enabled[i]``, and increments or decrements :attr:`total` to
295
+ reflect any change relative to the previous value.
296
+
297
+ Parameters
298
+ ----------
299
+ i: :class:`int`
300
+ Node index, with ``0 <= i < graph.n``.
301
+
302
+ Returns
303
+ -------
304
+ :class:`bool`
305
+ New value of ``enabled[i]``.
306
+
307
+ Examples
308
+ --------
309
+ >>> from tinto.graphs import line
310
+ >>> from tinto.configs import make
311
+ >>> Config = make()
312
+ >>> c = Config(line(3))
313
+ >>> c.check(0)
314
+ True
315
+ >>> c.colors[0] = 1
316
+ >>> c.check(0)
317
+ False
318
+ """
319
+ old = self.enabled[i]
320
+ new = guard(self, i)
321
+ if new and not old:
322
+ self.total += 1
323
+ elif old and not new:
324
+ self.total -= 1
325
+ self.enabled[i] = new
326
+ return new
327
+
328
+ def check_all(self):
329
+ """Recompute :attr:`enabled` and :attr:`total` from :attr:`colors`.
330
+
331
+ Resets the bookkeeping and calls :meth:`check` on every node. Use
332
+ this after any bulk edit of :attr:`colors` so that the enabled
333
+ mask reflects the current coloring.
334
+
335
+ Examples
336
+ --------
337
+ >>> from tinto.graphs import line
338
+ >>> from tinto.configs import make
339
+ >>> Config = make()
340
+ >>> c = Config(line(3))
341
+ >>> c.colors[:] = [0, 1, 0]
342
+ >>> c.check_all()
343
+ >>> c.total
344
+ 0
345
+ >>> c.enabled.tolist()
346
+ [False, False, False]
347
+ """
348
+ self.total = 0
349
+ self.enabled[:] = False
350
+ for i in range(self.graph.n):
351
+ self.check(i)
352
+
353
+ def color_uniform(self, color=0):
354
+ """Reset every node to `color`, defaulting to ``0``, and refresh the enabled cache.
355
+
356
+ Under the default guard, any two adjacent nodes then share the
357
+ same color and are both enabled.
358
+
359
+ Parameters
360
+ ----------
361
+ color: :class:`int`, default=0
362
+ Color assigned to every node. Must lie in ``range(k)``.
363
+
364
+ Examples
365
+ --------
366
+ >>> from tinto.graphs import line
367
+ >>> from tinto.configs import make
368
+ >>> Config = make()
369
+ >>> c = Config(line(4))
370
+ >>> c.colors[:] = [0, 1, 0, 1]
371
+ >>> c.check_all()
372
+ >>> c.total
373
+ 0
374
+ >>> c.color_uniform()
375
+ >>> c.total
376
+ 4
377
+
378
+ A non-zero color is equally accepted, as long as it stays in
379
+ ``range(k)``:
380
+
381
+ >>> c.color_uniform(color=2)
382
+ >>> c.colors.tolist()
383
+ [2, 2, 2, 2]
384
+ """
385
+ assert 0 <= color < self.k
386
+ self.colors[:] = color
387
+ self.check_all()
388
+
389
+ def color_randomize(self, k_init=0):
390
+ """Assign a uniform random color to every node.
391
+
392
+ Colors are drawn in ``range(k_init)``, the first `k_init` colors
393
+ of the palette, or in ``range(k)`` when ``k_init`` is 0. A start
394
+ with fewer colors than the palette (``k_init < Delta + 1``) is
395
+ how the exact stabilization-time paper samples its initial
396
+ colorings. The enabled cache is refreshed from the new coloring.
397
+
398
+ Parameters
399
+ ----------
400
+ k_init: :class:`int`, default=0
401
+ Number of initial colors, at most :attr:`k`; 0 means all of
402
+ them.
403
+
404
+ Examples
405
+ --------
406
+ >>> import numpy as np
407
+ >>> from tinto.graphs import line
408
+ >>> from tinto.configs import make
409
+ >>> Config = make()
410
+ >>> np.random.seed(0)
411
+ >>> c = Config(line(10))
412
+ >>> c.color_randomize()
413
+ >>> bool(((c.colors >= 0) & (c.colors < c.k)).all())
414
+ True
415
+ >>> c.total == int(c.enabled.sum())
416
+ True
417
+
418
+ Two initial colors out of a palette of three:
419
+
420
+ >>> c.color_randomize(2)
421
+ >>> c.k, sorted(set(c.colors.tolist()))
422
+ (3, [0, 1])
423
+ """
424
+ n_colors = self.k if k_init == 0 else k_init
425
+ assert n_colors <= self.k
426
+ for i in range(self.graph.n):
427
+ self.colors[i] = np.random.randint(n_colors)
428
+ self.check_all()
429
+
430
+ Config.uses_ids = uses_ids
431
+ return Config