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 +12 -0
- tinto/configs.py +431 -0
- tinto/estimate.py +804 -0
- tinto/exact.py +311 -0
- tinto/experiments.py +1132 -0
- tinto/graphs.py +604 -0
- tinto/guards.py +276 -0
- tinto/io.py +52 -0
- tinto/oracle.py +295 -0
- tinto/runners.py +239 -0
- tinto/schedulers.py +516 -0
- tinto/solvers.py +414 -0
- tinto/starters.py +460 -0
- tinto/theory/__init__.py +28 -0
- tinto/theory/_egf.py +111 -0
- tinto/theory/bipartite.py +182 -0
- tinto/theory/cayley.py +319 -0
- tinto/theory/erdos_renyi.py +203 -0
- tinto/theory/general.py +374 -0
- tinto/theory/path_cycle.py +282 -0
- tinto/theory/star.py +88 -0
- tinto-0.1.0.dist-info/METADATA +130 -0
- tinto-0.1.0.dist-info/RECORD +25 -0
- tinto-0.1.0.dist-info/WHEEL +4 -0
- tinto-0.1.0.dist-info/licenses/AUTHORS.md +9 -0
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
|