python-sat 1.8.dev23__tar.gz → 1.8.dev25__tar.gz
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.
- {python_sat-1.8.dev23/python_sat.egg-info → python_sat-1.8.dev25}/PKG-INFO +1 -1
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/bbscan.py +91 -97
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/__init__.py +1 -1
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/_fileio.py +1 -1
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/formula.py +28 -3
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/pb.py +33 -12
- {python_sat-1.8.dev23 → python_sat-1.8.dev25/python_sat.egg-info}/PKG-INFO +1 -1
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/python_sat.egg-info/SOURCES.txt +2 -0
- python_sat-1.8.dev25/tests/test_encode_pb_conditional.py +54 -0
- python_sat-1.8.dev25/tests/test_idpool.py +27 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/LICENSE.txt +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/MANIFEST.in +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/README.rst +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/allies/__init__.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/allies/approxmc.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/allies/unigen.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/bitwise.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/card.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/clset.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/common.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/itot.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/ladder.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/mto.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/pairwise.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/ptypes.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/pycard.cc +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/seqcounter.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/sortcard.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/cardenc/utils.hh +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/__init__.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/bica.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/fm.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/genhard.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/hitman.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/lbx.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/lsu.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/mcsls.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/models.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/musx.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/optux.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/primer.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/rc2.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/examples/usage.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/_utils.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/card.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/engines.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/process.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/pysat/solvers.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/python_sat.egg-info/dependency_links.txt +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/python_sat.egg-info/requires.txt +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/python_sat.egg-info/top_level.txt +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/requirements.txt +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/setup.cfg +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/setup.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/cadical103.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/cadical153.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/cadical195.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/glucose30.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/glucose41.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/glucose421.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/lingeling.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/maplechrono.zip +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/maplecm.zip +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/maplesat.zip +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/mergesat3.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/minicard.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/minisat22.tar.gz +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/minisatgh.zip +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/cadical103.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/cadical153.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/cadical195.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/glucose30.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/glucose41.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/glucose421.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/gluecard30.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/gluecard41.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/lingeling.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/maplechrono.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/maplecm.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/maplesat.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/mergesat3.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/minicard.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/minisat22.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/patches/minisatgh.patch +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/prepare.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/solvers/pysolvers.cc +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_accum_stats.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_atmost.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_atmost1.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_atmostk.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_boolengine.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_clausification.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_cnf.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_equals1.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_formula_unique.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_process.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_propagate.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_unique_model.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_unique_mus.py +0 -0
- {python_sat-1.8.dev23 → python_sat-1.8.dev25}/tests/test_warmstart.py +0 -0
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
>>> cnf = CNF(from_file='formula.wcnf.xz')
|
|
74
74
|
>>>
|
|
75
75
|
>>> # creating BBScan and running it
|
|
76
|
-
>>> with BBScan(cnf, solver='g3',
|
|
76
|
+
>>> with BBScan(cnf, solver='g3', rotate=True) as bbscan:
|
|
77
77
|
... bbone = bbscan.compute(algorithm='core')
|
|
78
78
|
...
|
|
79
79
|
... # outputting the results
|
|
@@ -81,10 +81,10 @@
|
|
|
81
81
|
... print(bbone)
|
|
82
82
|
[1, -3]
|
|
83
83
|
|
|
84
|
-
Each of the available algorithms can be augmented with
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
Each of the available algorithms can be augmented with a simple heuristic
|
|
85
|
+
aiming at reducing satisfying assignments and, thus, filtering out
|
|
86
|
+
unnecessary literals: namely, filtering rotatable literals. The heuristic
|
|
87
|
+
is described in the aforementioned paper.
|
|
88
88
|
|
|
89
89
|
Note that most of the methods of the class :class:`BBScan` are made
|
|
90
90
|
private. Therefore, the tool provides a minimalistic interface via which a
|
|
@@ -113,44 +113,42 @@ class BBScan:
|
|
|
113
113
|
"""
|
|
114
114
|
A solver for computing all backbone literals of a given Boolean
|
|
115
115
|
formula. It implements 6 algorithms for backbone computation described
|
|
116
|
-
in [1]_ augmented with
|
|
117
|
-
process.
|
|
116
|
+
in [1]_ augmented with a heuristic that can be speed up the process.
|
|
118
117
|
|
|
119
118
|
Note that the input formula can be a :class:`.CNF` object but also any
|
|
120
119
|
object of class :class:`.Formula`, thus the tool can used for
|
|
121
120
|
computing backbone literals of non-clausal formulas.
|
|
122
121
|
|
|
123
122
|
A user can select one of the SAT solvers available at hand
|
|
124
|
-
(``'glucose3'`` is used by default). The optional
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
(``'glucose3'`` is used by default). The optional heuristic can be
|
|
124
|
+
also specified as a Boolean input arguments ``rotate`` (turned off by
|
|
125
|
+
default).
|
|
127
126
|
|
|
128
127
|
The list of initialiser's arguments is as follows:
|
|
129
128
|
|
|
130
129
|
:param formula: input formula whose backbone is sought
|
|
131
130
|
:param solver: SAT oracle name
|
|
132
|
-
:param lift: apply literal lifting heuristic
|
|
133
131
|
:param rotate: apply rotatable literal filtering
|
|
134
132
|
:param verbose: verbosity level
|
|
135
133
|
|
|
136
134
|
:type formula: :class:`.Formula` or :class:`.CNF`
|
|
137
135
|
:type solver: str
|
|
138
|
-
:type lift: bool
|
|
139
136
|
:type rotate: bool
|
|
140
137
|
:type verbose: int
|
|
141
138
|
"""
|
|
142
139
|
|
|
143
|
-
def __init__(self, formula, solver='g3',
|
|
140
|
+
def __init__(self, formula, solver='g3', rotate=False, verbose=0):
|
|
144
141
|
"""
|
|
145
142
|
Constructor.
|
|
146
143
|
"""
|
|
147
144
|
|
|
148
145
|
self.formula = formula
|
|
146
|
+
self.focuscl = list(range(len(formula.clauses))) if rotate else []
|
|
149
147
|
self.verbose = verbose
|
|
150
148
|
self.oracle = None
|
|
151
149
|
|
|
152
150
|
# implicant reduction heuristics
|
|
153
|
-
self.
|
|
151
|
+
self.rotate = rotate
|
|
154
152
|
|
|
155
153
|
# basic stats
|
|
156
154
|
self.calls, self.filtered = 0, 0
|
|
@@ -230,12 +228,26 @@ class BBScan:
|
|
|
230
228
|
"""
|
|
231
229
|
|
|
232
230
|
self.calls += 1
|
|
231
|
+
trivial = []
|
|
233
232
|
|
|
234
233
|
if self.oracle.solve():
|
|
235
|
-
self.model = self.oracle.get_model()
|
|
234
|
+
self.model = set(self.oracle.get_model())
|
|
236
235
|
if focus_on is not None:
|
|
237
|
-
model
|
|
238
|
-
|
|
236
|
+
self.model &= set(focus_on)
|
|
237
|
+
|
|
238
|
+
# if filtering rotatable literals is requested then
|
|
239
|
+
# we should be clever about the clauses to traverse
|
|
240
|
+
if self.rotate:
|
|
241
|
+
self.focuscl = []
|
|
242
|
+
for i, cl in enumerate(self.formula):
|
|
243
|
+
for l in cl:
|
|
244
|
+
if l in self.model:
|
|
245
|
+
if len(cl) == 1:
|
|
246
|
+
trivial.append(l)
|
|
247
|
+
self.model.remove(l)
|
|
248
|
+
else:
|
|
249
|
+
self.focuscl.append(i)
|
|
250
|
+
break
|
|
239
251
|
else:
|
|
240
252
|
raise ValueError('Unsatisfiable formula')
|
|
241
253
|
|
|
@@ -258,50 +270,36 @@ class BBScan:
|
|
|
258
270
|
else:
|
|
259
271
|
assert 0, f'Unknown algorithm: {algorithm}'
|
|
260
272
|
|
|
261
|
-
return sorted(result, key=lambda l: abs(l))
|
|
262
|
-
|
|
263
|
-
def _get_implicant(self, model):
|
|
264
|
-
"""
|
|
265
|
-
Simple literal lifting used to reduce a given model.
|
|
266
|
-
"""
|
|
267
|
-
|
|
268
|
-
res, model = set(), set(model)
|
|
269
|
-
|
|
270
|
-
# traversing the clauses and collecting all literals
|
|
271
|
-
# that satisfy at least one clause of the formula
|
|
272
|
-
for cl in self.formula:
|
|
273
|
-
res |= set([l for l in cl if l in model])
|
|
274
|
-
|
|
275
|
-
return list(res)
|
|
273
|
+
return sorted(trivial + result, key=lambda l: abs(l))
|
|
276
274
|
|
|
277
275
|
def _filter_rotatable(self, model):
|
|
278
276
|
"""
|
|
279
277
|
Filter out rotatable literals.
|
|
280
278
|
"""
|
|
281
279
|
|
|
282
|
-
|
|
280
|
+
def get_unit(cl):
|
|
281
|
+
# this is supposed to be a faster alternative to the
|
|
282
|
+
# complete clause traversal with a conditional inside
|
|
283
|
+
unit = None
|
|
284
|
+
for l in cl:
|
|
285
|
+
if l in model:
|
|
286
|
+
if unit:
|
|
287
|
+
return
|
|
288
|
+
else:
|
|
289
|
+
unit = l
|
|
290
|
+
return unit
|
|
291
|
+
|
|
292
|
+
# result of this procedure
|
|
293
|
+
units = set([])
|
|
283
294
|
|
|
284
295
|
# determining unit literals
|
|
285
|
-
for
|
|
286
|
-
|
|
287
|
-
if
|
|
288
|
-
units.add(
|
|
296
|
+
for i in self.focuscl:
|
|
297
|
+
unit = get_unit(self.formula.clauses[i])
|
|
298
|
+
if unit:
|
|
299
|
+
units.add(unit)
|
|
289
300
|
|
|
290
301
|
self.filtered += len(model) - len(units)
|
|
291
|
-
return
|
|
292
|
-
|
|
293
|
-
def _process_model(self, model):
|
|
294
|
-
"""
|
|
295
|
-
Heuristically reduce a model.
|
|
296
|
-
"""
|
|
297
|
-
|
|
298
|
-
if self.lift:
|
|
299
|
-
model = self._get_implicant(model)
|
|
300
|
-
|
|
301
|
-
if self.rotate:
|
|
302
|
-
model = self._filter_rotatable(model)
|
|
303
|
-
|
|
304
|
-
return model
|
|
302
|
+
return units
|
|
305
303
|
|
|
306
304
|
def _compute_enum(self, focus_on=None):
|
|
307
305
|
"""
|
|
@@ -312,7 +310,7 @@ class BBScan:
|
|
|
312
310
|
print('c using enumeration-based algorithm')
|
|
313
311
|
|
|
314
312
|
# initial backbone estimate contains all literals
|
|
315
|
-
bbone =
|
|
313
|
+
bbone = self.model if focus_on is None else focus_on
|
|
316
314
|
|
|
317
315
|
while bbone:
|
|
318
316
|
# counting the number of calls
|
|
@@ -322,15 +320,14 @@ class BBScan:
|
|
|
322
320
|
if not self.oracle.solve():
|
|
323
321
|
break
|
|
324
322
|
|
|
325
|
-
coex = set(self.oracle.get_model())
|
|
326
|
-
self.model = [l for l in bbone if l in coex]
|
|
327
|
-
self.model = self._process_model(self.model)
|
|
328
|
-
|
|
329
323
|
# updating backbone estimate - intersection
|
|
330
|
-
bbone &= set(self.
|
|
324
|
+
bbone &= set(self.oracle.get_model())
|
|
325
|
+
|
|
326
|
+
if self.rotate:
|
|
327
|
+
bbone = self._filter_rotatable(bbone)
|
|
331
328
|
|
|
332
329
|
# blocking the previously found implicant
|
|
333
|
-
self.oracle.add_clause([-l for l in
|
|
330
|
+
self.oracle.add_clause([-l for l in bbone])
|
|
334
331
|
|
|
335
332
|
return list(bbone)
|
|
336
333
|
|
|
@@ -342,6 +339,8 @@ class BBScan:
|
|
|
342
339
|
if self.verbose:
|
|
343
340
|
print('c using iterative algorithm')
|
|
344
341
|
|
|
342
|
+
# initial estimate
|
|
343
|
+
# using sets for model and assumps to save filtering time
|
|
345
344
|
bbone, model = [], self.model if focus_on is None else focus_on
|
|
346
345
|
|
|
347
346
|
while model:
|
|
@@ -357,9 +356,10 @@ class BBScan:
|
|
|
357
356
|
else:
|
|
358
357
|
# it isn't and we've got a counterexample
|
|
359
358
|
# => using it to filter out more literals
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
359
|
+
model &= set(self.oracle.get_model())
|
|
360
|
+
|
|
361
|
+
if self.rotate:
|
|
362
|
+
model = self._filter_rotatable(model)
|
|
363
363
|
|
|
364
364
|
return bbone
|
|
365
365
|
|
|
@@ -372,7 +372,7 @@ class BBScan:
|
|
|
372
372
|
print('c using complement of backbone estimate algorithm')
|
|
373
373
|
|
|
374
374
|
# initial estimate
|
|
375
|
-
bbone =
|
|
375
|
+
bbone = self.model if focus_on is None else focus_on
|
|
376
376
|
|
|
377
377
|
# iterating until we find the backbone or determine there is none
|
|
378
378
|
while bbone:
|
|
@@ -385,23 +385,23 @@ class BBScan:
|
|
|
385
385
|
if self.oracle.solve() == False:
|
|
386
386
|
break
|
|
387
387
|
else:
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
388
|
+
bbone &= set(self.oracle.get_model())
|
|
389
|
+
|
|
390
|
+
if self.rotate:
|
|
391
|
+
bbone = self._filter_rotatable(bbone)
|
|
392
392
|
|
|
393
393
|
return list(bbone)
|
|
394
394
|
|
|
395
395
|
def _compute_chunking(self, chunk_size=100, focus_on=None):
|
|
396
396
|
"""
|
|
397
|
-
|
|
397
|
+
Chunking algorithm.
|
|
398
398
|
"""
|
|
399
399
|
|
|
400
400
|
if self.verbose:
|
|
401
401
|
print('c using chunking algorithm, with chunk size:', chunk_size)
|
|
402
402
|
|
|
403
403
|
# initial estimate
|
|
404
|
-
bbone, model = [], self.model if focus_on is None else focus_on
|
|
404
|
+
bbone, model = [], list(self.model if focus_on is None else focus_on)
|
|
405
405
|
|
|
406
406
|
# we are going to use clause selectors
|
|
407
407
|
vpool = IDPool(start_from=self.formula.nv + 1)
|
|
@@ -427,7 +427,9 @@ class BBScan:
|
|
|
427
427
|
else:
|
|
428
428
|
coex = set(self.oracle.get_model())
|
|
429
429
|
model = [l for l in model if l in coex]
|
|
430
|
-
|
|
430
|
+
|
|
431
|
+
if self.rotate:
|
|
432
|
+
model = list(self._filter_rotatable(set(model)))
|
|
431
433
|
|
|
432
434
|
return bbone
|
|
433
435
|
|
|
@@ -440,21 +442,24 @@ class BBScan:
|
|
|
440
442
|
print('c using core-based algorithm')
|
|
441
443
|
|
|
442
444
|
# initial estimate
|
|
445
|
+
# using sets for model and assumps to save filtering time
|
|
443
446
|
bbone, model = [], self.model if focus_on is None else focus_on
|
|
444
447
|
|
|
445
448
|
# iterating until we find the backbone or determine there is none
|
|
446
449
|
while model:
|
|
447
450
|
# flipping all the literals
|
|
448
|
-
assumps =
|
|
451
|
+
assumps = {-l for l in model}
|
|
449
452
|
|
|
450
453
|
# getting unsatisfiable cores with them
|
|
451
454
|
while True:
|
|
452
455
|
self.calls += 1
|
|
453
456
|
|
|
454
457
|
if self.oracle.solve(assumptions=assumps):
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
+
model &= set(self.oracle.get_model())
|
|
459
|
+
|
|
460
|
+
if self.rotate:
|
|
461
|
+
model = self._filter_rotatable(model)
|
|
462
|
+
|
|
458
463
|
break
|
|
459
464
|
|
|
460
465
|
else:
|
|
@@ -464,18 +469,12 @@ class BBScan:
|
|
|
464
469
|
self.oracle.add_clause([-core[0]])
|
|
465
470
|
|
|
466
471
|
# remove from the working model
|
|
467
|
-
|
|
468
|
-
if indx < len(model) - 1:
|
|
469
|
-
model[indx] = model.pop()
|
|
470
|
-
else:
|
|
471
|
-
model.pop()
|
|
472
|
+
model.remove(-core[0])
|
|
472
473
|
|
|
473
474
|
# filtering out unnecessary flipped literals
|
|
474
|
-
|
|
475
|
-
assumps = [l for l in assumps if l not in core]
|
|
475
|
+
assumps -= set(core)
|
|
476
476
|
|
|
477
477
|
if not assumps:
|
|
478
|
-
# resorting to the iterative traversal algorithm
|
|
479
478
|
self.model = model
|
|
480
479
|
return bbone + self._compute_iter()
|
|
481
480
|
|
|
@@ -490,7 +489,7 @@ class BBScan:
|
|
|
490
489
|
print('c using core-based chunking, with chunk size:', chunk_size)
|
|
491
490
|
|
|
492
491
|
# initial estimate
|
|
493
|
-
bbone, model = [], self.model if focus_on is None else focus_on
|
|
492
|
+
bbone, model = [], list(self.model if focus_on is None else focus_on)
|
|
494
493
|
|
|
495
494
|
# we are going to use clause selectors
|
|
496
495
|
vpool = IDPool(start_from=self.formula.nv + 1)
|
|
@@ -501,7 +500,7 @@ class BBScan:
|
|
|
501
500
|
size = min(chunk_size, len(model))
|
|
502
501
|
|
|
503
502
|
# flipping all the literals
|
|
504
|
-
assumps, skipped =
|
|
503
|
+
assumps, skipped = {-model.pop() for i in range(size)}, set()
|
|
505
504
|
|
|
506
505
|
# getting unsatisfiable cores with them
|
|
507
506
|
while True:
|
|
@@ -510,7 +509,9 @@ class BBScan:
|
|
|
510
509
|
if self.oracle.solve(assumptions=assumps):
|
|
511
510
|
coex = set(self.oracle.get_model())
|
|
512
511
|
model = [l for l in model if l in coex]
|
|
513
|
-
|
|
512
|
+
|
|
513
|
+
if self.rotate:
|
|
514
|
+
model = list(self._filter_rotatable(set(model)))
|
|
514
515
|
|
|
515
516
|
if skipped:
|
|
516
517
|
bbone += self._compute_iter(focus_on=skipped)
|
|
@@ -526,11 +527,10 @@ class BBScan:
|
|
|
526
527
|
self.oracle.add_clause([-core[0]])
|
|
527
528
|
else:
|
|
528
529
|
# all removed literals are going to be tested later
|
|
529
|
-
skipped
|
|
530
|
+
skipped |= {-l for l in core}
|
|
530
531
|
|
|
531
532
|
# filtering out unnecessary flipped literals
|
|
532
|
-
|
|
533
|
-
assumps = [l for l in assumps if l not in core]
|
|
533
|
+
assumps -= set(core)
|
|
534
534
|
|
|
535
535
|
if not assumps:
|
|
536
536
|
# resorting to the iterative traversal algorithm
|
|
@@ -561,11 +561,10 @@ def parse_options():
|
|
|
561
561
|
|
|
562
562
|
try:
|
|
563
563
|
opts, args = getopt.getopt(sys.argv[1:],
|
|
564
|
-
'a:c:
|
|
564
|
+
'a:c:hrs:v',
|
|
565
565
|
['algo=',
|
|
566
566
|
'chunk=',
|
|
567
567
|
'help',
|
|
568
|
-
'lift',
|
|
569
568
|
'rotate',
|
|
570
569
|
'solver=',
|
|
571
570
|
'verbose'])
|
|
@@ -576,7 +575,6 @@ def parse_options():
|
|
|
576
575
|
|
|
577
576
|
algo = 'iter'
|
|
578
577
|
chunk = 100
|
|
579
|
-
lift = False
|
|
580
578
|
rotate = False
|
|
581
579
|
solver = 'g3'
|
|
582
580
|
verbose = 0
|
|
@@ -594,8 +592,6 @@ def parse_options():
|
|
|
594
592
|
elif opt in ('-h', '--help'):
|
|
595
593
|
usage()
|
|
596
594
|
sys.exit(0)
|
|
597
|
-
elif opt in ('-l', '--lift'):
|
|
598
|
-
lift = True
|
|
599
595
|
elif opt in ('-r', '--rotate'):
|
|
600
596
|
rotate = True
|
|
601
597
|
elif opt in ('-s', '--solver'):
|
|
@@ -605,7 +601,7 @@ def parse_options():
|
|
|
605
601
|
else:
|
|
606
602
|
assert False, 'Unhandled option: {0} {1}'.format(opt, arg)
|
|
607
603
|
|
|
608
|
-
return algo, chunk,
|
|
604
|
+
return algo, chunk, rotate, solver, verbose, args
|
|
609
605
|
|
|
610
606
|
|
|
611
607
|
#
|
|
@@ -622,7 +618,6 @@ def usage():
|
|
|
622
618
|
print(' -c, --chunk=<int,float> Chunk size for chunking algorithms')
|
|
623
619
|
print(' Available values: [1 .. INT_MAX] or (0 .. 1] (default: 100)')
|
|
624
620
|
print(' -h, --help Show this message')
|
|
625
|
-
print(' -l, --lift Apply literal lifting for heuristic model reduction')
|
|
626
621
|
print(' -r, --rotate Heuristically filter out rotatable literals')
|
|
627
622
|
print(' -s, --solver=<string> SAT solver to use')
|
|
628
623
|
print(' Available values: cd15, cd19, g3, g4, lgl, mcb, mcm, mpl, m22, mc, mgh (default: g3)')
|
|
@@ -632,7 +627,7 @@ def usage():
|
|
|
632
627
|
#
|
|
633
628
|
#==============================================================================
|
|
634
629
|
if __name__ == '__main__':
|
|
635
|
-
algo, chunk,
|
|
630
|
+
algo, chunk, rotate, solver, verbose, files = parse_options()
|
|
636
631
|
|
|
637
632
|
if files:
|
|
638
633
|
# read CNF from file
|
|
@@ -645,8 +640,7 @@ if __name__ == '__main__':
|
|
|
645
640
|
len(formula.clauses)))
|
|
646
641
|
|
|
647
642
|
# computing the backbone
|
|
648
|
-
with BBScan(formula, solver=solver,
|
|
649
|
-
verbose=verbose) as bbscan:
|
|
643
|
+
with BBScan(formula, solver=solver, rotate=rotate, verbose=verbose) as bbscan:
|
|
650
644
|
try:
|
|
651
645
|
bbone = bbscan.compute(algorithm=algo, chunk_size=chunk)
|
|
652
646
|
|
|
@@ -297,19 +297,28 @@ class IDPool(object):
|
|
|
297
297
|
necessary the top variable ID can be accessed directly using the
|
|
298
298
|
``top`` variable.
|
|
299
299
|
|
|
300
|
+
The final parameter ``with_neg``, if set to ``True``, indicates that
|
|
301
|
+
the *negation* of an object assigned a variable ID ``n`` is to be
|
|
302
|
+
represented using the negative integer ``-n``. For this to work, the
|
|
303
|
+
object must have the method ``__neg__()`` implemented. This behaviour
|
|
304
|
+
is disabled by default.
|
|
305
|
+
|
|
300
306
|
:param start_from: the smallest ID to assign.
|
|
301
307
|
:param occupied: a list of occupied intervals.
|
|
308
|
+
:param with_neg: whether to support automatic negation handling
|
|
302
309
|
|
|
303
310
|
:type start_from: int
|
|
304
311
|
:type occupied: list(list(int))
|
|
312
|
+
:type with_neg: bool
|
|
305
313
|
"""
|
|
306
314
|
|
|
307
|
-
def __init__(self, start_from=1, occupied=[]):
|
|
315
|
+
def __init__(self, start_from=1, occupied=[], with_neg=False):
|
|
308
316
|
"""
|
|
309
317
|
Constructor.
|
|
310
318
|
"""
|
|
311
319
|
|
|
312
|
-
self.restart(start_from=start_from, occupied=occupied
|
|
320
|
+
self.restart(start_from=start_from, occupied=occupied,
|
|
321
|
+
with_neg=with_neg)
|
|
313
322
|
|
|
314
323
|
def __repr__(self):
|
|
315
324
|
"""
|
|
@@ -318,7 +327,7 @@ class IDPool(object):
|
|
|
318
327
|
|
|
319
328
|
return f'IDPool(start_from={self.top+1}, occupied={self._occupied})'
|
|
320
329
|
|
|
321
|
-
def restart(self, start_from=1, occupied=[]):
|
|
330
|
+
def restart(self, start_from=1, occupied=[], with_neg=False):
|
|
322
331
|
"""
|
|
323
332
|
Restart the manager from scratch. The arguments replicate those of
|
|
324
333
|
the constructor of :class:`IDPool`.
|
|
@@ -337,6 +346,10 @@ class IDPool(object):
|
|
|
337
346
|
# (if for whatever reason necessary)
|
|
338
347
|
self.id2obj = {}
|
|
339
348
|
|
|
349
|
+
# flag to indicate whether this IDPool object
|
|
350
|
+
# should support automatic negation handling
|
|
351
|
+
self.with_neg = with_neg
|
|
352
|
+
|
|
340
353
|
def id(self, obj=None):
|
|
341
354
|
"""
|
|
342
355
|
The method is to be used to assign an integer variable ID for a
|
|
@@ -385,6 +398,11 @@ class IDPool(object):
|
|
|
385
398
|
|
|
386
399
|
if vid not in self.id2obj:
|
|
387
400
|
self.id2obj[vid] = obj
|
|
401
|
+
|
|
402
|
+
# adding the object's negation, if required and supported
|
|
403
|
+
if self.with_neg and hasattr(obj, '__neg__'):
|
|
404
|
+
self.obj2id[-obj] = -vid
|
|
405
|
+
self.id2obj[-vid] = -obj
|
|
388
406
|
else:
|
|
389
407
|
# no object is provided => simply return a new ID
|
|
390
408
|
vid = self._next()
|
|
@@ -1022,6 +1040,13 @@ class Formula(object):
|
|
|
1022
1040
|
return Neg(self)
|
|
1023
1041
|
return self.subformula
|
|
1024
1042
|
|
|
1043
|
+
def __neg__(self):
|
|
1044
|
+
"""
|
|
1045
|
+
Negation operator. Takes the same effect as ``__invert__()``.
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
return self.__invert__()
|
|
1049
|
+
|
|
1025
1050
|
def __and__(self, other):
|
|
1026
1051
|
"""
|
|
1027
1052
|
Logical conjunction operator overloaded for class
|
|
@@ -137,6 +137,8 @@ class EncType(object):
|
|
|
137
137
|
cases, this invokes the ``bdd`` encoder).
|
|
138
138
|
"""
|
|
139
139
|
|
|
140
|
+
assert pblib_present, 'Package \'pypblib\' is unavailable. Check your installation.'
|
|
141
|
+
|
|
140
142
|
best = 0
|
|
141
143
|
bdd = 1
|
|
142
144
|
seqcounter = 2
|
|
@@ -242,7 +244,7 @@ class PBEnc(object):
|
|
|
242
244
|
|
|
243
245
|
@classmethod
|
|
244
246
|
def _encode(cls, lits, weights=None, bound=1, top_id=None, vpool=None,
|
|
245
|
-
encoding=EncType.best, comparator='<'):
|
|
247
|
+
encoding=EncType.best, comparator='<', conditionals=None):
|
|
246
248
|
"""
|
|
247
249
|
This is the method that wraps the encoder of PyPBLib. Although the
|
|
248
250
|
method can be invoked directly, a user is expected to call one of
|
|
@@ -310,7 +312,9 @@ class PBEnc(object):
|
|
|
310
312
|
top_id = vpool.top
|
|
311
313
|
|
|
312
314
|
# choosing the maximum id among the current top and the list of literals
|
|
313
|
-
|
|
315
|
+
if conditionals is None:
|
|
316
|
+
conditionals = []
|
|
317
|
+
top_id = max(map(lambda x: abs(x), conditionals + lits + [top_id if top_id != None else 0]))
|
|
314
318
|
|
|
315
319
|
# native representation
|
|
316
320
|
if encoding == 6:
|
|
@@ -331,8 +335,13 @@ class PBEnc(object):
|
|
|
331
335
|
# pseudo-Boolean constraint and variable manager
|
|
332
336
|
constr = pblib.PBConstraint([pblib.WeightedLit(*wl) for wl in wlits],
|
|
333
337
|
EncType._to_pbcmp[comparator], bound)
|
|
338
|
+
|
|
334
339
|
varmgr = pblib.AuxVarManager(top_id + 1)
|
|
335
340
|
|
|
341
|
+
# add optional conditionals
|
|
342
|
+
if len(conditionals) > 0:
|
|
343
|
+
constr.add_conditionals(conditionals)
|
|
344
|
+
|
|
336
345
|
# encoder configuration
|
|
337
346
|
config = pblib.PBConfig()
|
|
338
347
|
config.set_PB_Encoder(EncType._to_pbenc[encoding])
|
|
@@ -358,7 +367,7 @@ class PBEnc(object):
|
|
|
358
367
|
|
|
359
368
|
@classmethod
|
|
360
369
|
def leq(cls, lits, weights=None, bound=1, top_id=None, vpool=None,
|
|
361
|
-
encoding=EncType.best):
|
|
370
|
+
encoding=EncType.best, conditionals=None):
|
|
362
371
|
"""
|
|
363
372
|
This method can be used for creating a CNF encoding of a LEQ
|
|
364
373
|
(weighted AtMostK) constraint, i.e. of
|
|
@@ -374,12 +383,18 @@ class PBEnc(object):
|
|
|
374
383
|
``EncType.best``, i.e. it is up to the PBLib encoder to choose the
|
|
375
384
|
encoding type.
|
|
376
385
|
|
|
386
|
+
The final optional argument is ``conditionals``, where a list of
|
|
387
|
+
literals can be passed to be used as the antecedent for the PB
|
|
388
|
+
constraint, which makes it *"reified"*. If the argument is set to
|
|
389
|
+
``None``, the constraint won't be reified.
|
|
390
|
+
|
|
377
391
|
:param lits: a list of literals in the sum.
|
|
378
392
|
:param weights: a list of weights
|
|
379
393
|
:param bound: the value of bound :math:`k`.
|
|
380
394
|
:param top_id: top variable identifier used so far.
|
|
381
395
|
:param vpool: variable pool for counting the number of variables.
|
|
382
396
|
:param encoding: identifier of the encoding to use.
|
|
397
|
+
:param conditionals: a list of variables that imply this constraint.
|
|
383
398
|
|
|
384
399
|
:type lits: iterable(int)
|
|
385
400
|
:type weights: iterable(int)
|
|
@@ -387,26 +402,29 @@ class PBEnc(object):
|
|
|
387
402
|
:type top_id: integer or None
|
|
388
403
|
:type vpool: :class:`.IDPool`
|
|
389
404
|
:type encoding: integer
|
|
405
|
+
:type conditionals: list(int) or None
|
|
390
406
|
|
|
391
407
|
:rtype: :class:`pysat.formula.CNFPlus`
|
|
392
408
|
"""
|
|
393
409
|
|
|
394
410
|
return cls._encode(lits, weights=weights, bound=bound, top_id=top_id,
|
|
395
|
-
|
|
411
|
+
vpool=vpool, encoding=encoding, comparator='<',
|
|
412
|
+
conditionals=conditionals)
|
|
396
413
|
|
|
397
414
|
@classmethod
|
|
398
415
|
def atmost(cls, lits, weights=None, bound=1, top_id=None, vpool=None,
|
|
399
|
-
encoding=EncType.best):
|
|
416
|
+
encoding=EncType.best, conditionals=None):
|
|
400
417
|
"""
|
|
401
418
|
A synonim for :meth:`PBEnc.leq`.
|
|
402
419
|
"""
|
|
403
420
|
|
|
404
421
|
return cls.leq(lits, weights=weights, bound=bound, top_id=top_id,
|
|
405
|
-
|
|
422
|
+
vpool=vpool, encoding=encoding,
|
|
423
|
+
conditionals=conditionals)
|
|
406
424
|
|
|
407
425
|
@classmethod
|
|
408
426
|
def geq(cls, lits, weights=None, bound=1, top_id=None, vpool=None,
|
|
409
|
-
encoding=EncType.best):
|
|
427
|
+
encoding=EncType.best, conditionals=None):
|
|
410
428
|
"""
|
|
411
429
|
This method can be used for creating a CNF encoding of a GEQ
|
|
412
430
|
(weighted AtLeastK) constraint, i.e. of
|
|
@@ -416,21 +434,23 @@ class PBEnc(object):
|
|
|
416
434
|
"""
|
|
417
435
|
|
|
418
436
|
return cls._encode(lits, weights=weights, bound=bound, top_id=top_id,
|
|
419
|
-
|
|
437
|
+
vpool=vpool, encoding=encoding, comparator='>',
|
|
438
|
+
conditionals=conditionals)
|
|
420
439
|
|
|
421
440
|
@classmethod
|
|
422
441
|
def atleast(cls, lits, weights=None, bound=1, top_id=None, vpool=None,
|
|
423
|
-
encoding=EncType.best):
|
|
442
|
+
encoding=EncType.best, conditionals=None):
|
|
424
443
|
"""
|
|
425
444
|
A synonym for :meth:`PBEnc.geq`.
|
|
426
445
|
"""
|
|
427
446
|
|
|
428
447
|
return cls.geq(lits, weights=weights, bound=bound, top_id=top_id,
|
|
429
|
-
|
|
448
|
+
vpool=vpool, encoding=encoding,
|
|
449
|
+
conditionals=conditionals)
|
|
430
450
|
|
|
431
451
|
@classmethod
|
|
432
452
|
def equals(cls, lits, weights=None, bound=1, top_id=None, vpool=None,
|
|
433
|
-
encoding=EncType.best):
|
|
453
|
+
encoding=EncType.best, conditionals=None):
|
|
434
454
|
"""
|
|
435
455
|
This method can be used for creating a CNF encoding of a weighted
|
|
436
456
|
EqualsK constraint, i.e. of :math:`\\sum_{i=1}^{n}{a_i\\cdot x_i}=
|
|
@@ -439,4 +459,5 @@ class PBEnc(object):
|
|
|
439
459
|
"""
|
|
440
460
|
|
|
441
461
|
return cls._encode(lits, weights=weights, bound=bound, top_id=top_id,
|
|
442
|
-
|
|
462
|
+
vpool=vpool, encoding=encoding, comparator='=',
|
|
463
|
+
conditionals=conditionals)
|
|
@@ -88,8 +88,10 @@ tests/test_atmostk.py
|
|
|
88
88
|
tests/test_boolengine.py
|
|
89
89
|
tests/test_clausification.py
|
|
90
90
|
tests/test_cnf.py
|
|
91
|
+
tests/test_encode_pb_conditional.py
|
|
91
92
|
tests/test_equals1.py
|
|
92
93
|
tests/test_formula_unique.py
|
|
94
|
+
tests/test_idpool.py
|
|
93
95
|
tests/test_process.py
|
|
94
96
|
tests/test_propagate.py
|
|
95
97
|
tests/test_unique_model.py
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from pysat.formula import IDPool
|
|
2
|
+
from pysat.card import CardEnc
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
@pytest.mark.skip(reason="PBLIB not installed in CI")
|
|
6
|
+
def test_pbenc_conditional():
|
|
7
|
+
from pysat.pb import PBEnc, EncType
|
|
8
|
+
L = 3
|
|
9
|
+
LITS = list(range(1, L + 1)) # Variables x1, x2, x3, x4
|
|
10
|
+
WEIGHTS = [2, 1, 1] # Coefficients: 2*x1 + 3*x2 + 5*x3 + 8*x4
|
|
11
|
+
bound = 10 # Constraint: ... <= 10
|
|
12
|
+
|
|
13
|
+
CONDS = [L+1]
|
|
14
|
+
|
|
15
|
+
# We need a vpool even if we don't use it, to handle PySAT internal structure
|
|
16
|
+
vpool = IDPool()
|
|
17
|
+
|
|
18
|
+
# Generate the CNF clauses
|
|
19
|
+
cnf = PBEnc.atleast(
|
|
20
|
+
lits=LITS,
|
|
21
|
+
weights=WEIGHTS,
|
|
22
|
+
bound=bound,
|
|
23
|
+
vpool=vpool,
|
|
24
|
+
conditionals=CONDS
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
assert cnf.clauses == [[-4]] # without PR to pblib this will be [[3, -4], [1, -4], [2, -4], [-4]]
|
|
28
|
+
|
|
29
|
+
bound = 2
|
|
30
|
+
|
|
31
|
+
# Generate the CNF clauses
|
|
32
|
+
cnf = PBEnc.atleast(
|
|
33
|
+
lits=LITS,
|
|
34
|
+
weights=WEIGHTS,
|
|
35
|
+
bound=bound,
|
|
36
|
+
vpool=vpool,
|
|
37
|
+
conditionals=CONDS
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert cnf.clauses == [[5], [-5, 6], [3, 6], [-5, 3, 7], [2, 6], [-5, 2, 7], [3, 2, 7], [-5, 3, 2, 8], [1, 9], [-7, 9], [1, -7, 10], [-10, -4]]
|
|
41
|
+
|
|
42
|
+
bound = 0
|
|
43
|
+
|
|
44
|
+
# Generate the CNF clauses
|
|
45
|
+
cnf = PBEnc.atleast(
|
|
46
|
+
lits=LITS,
|
|
47
|
+
weights=WEIGHTS,
|
|
48
|
+
bound=bound,
|
|
49
|
+
vpool=vpool,
|
|
50
|
+
conditionals=CONDS
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
assert cnf.clauses == []
|
|
54
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from pysat.formula import IDPool
|
|
2
|
+
|
|
3
|
+
def test_idpool_neg():
|
|
4
|
+
pool = IDPool(with_neg=True)
|
|
5
|
+
|
|
6
|
+
pool.id('hello')
|
|
7
|
+
pool.id(+25)
|
|
8
|
+
pool.id(-42)
|
|
9
|
+
pool.id('world')
|
|
10
|
+
|
|
11
|
+
assert pool.id('hello') == 1
|
|
12
|
+
assert pool.id(25) == -pool.id(-25) == +2
|
|
13
|
+
assert pool.id(42) == -pool.id(-42) == -3
|
|
14
|
+
assert pool.id('world') == 4
|
|
15
|
+
|
|
16
|
+
def test_idpool_occupied():
|
|
17
|
+
pool = IDPool(start_from=5, occupied=[[2, 10], [12, 20], [22, 30]], with_neg=True)
|
|
18
|
+
|
|
19
|
+
pool.id('hello')
|
|
20
|
+
pool.id(+25)
|
|
21
|
+
pool.id(-42)
|
|
22
|
+
pool.id('world')
|
|
23
|
+
|
|
24
|
+
assert pool.id('hello') == 11
|
|
25
|
+
assert pool.id(25) == -pool.id(-25) == +21
|
|
26
|
+
assert pool.id(42) == -pool.id(-42) == -31
|
|
27
|
+
assert pool.id('world') == 32
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|