Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/sat/converters/polybori.py
8820 views
1
"""
2
An ANF to CNF Converter using a Dense/Sparse Strategy
3
4
This converter is based on two converters. The first one, by Martin Albrecht, was based on [CB07]_,
5
this is the basis of the "dense" part of the converter. It was later improved by Mate Soos. The
6
second one, by Michael Brickenstein, uses a reduced truth table based approach and forms the
7
"sparse" part of the converter.
8
9
AUTHORS:
10
11
- Martin Albrecht - (2008-09) initial version of 'anf2cnf.py'
12
- Michael Brickenstein - (2009) 'cnf.py' for PolyBoRi
13
- Mate Soos - (2010) improved version of 'anf2cnf.py'
14
- Martin Albrecht - (2012) unified and added to Sage
15
16
REFERENCES:
17
18
.. [CB07] Nicolas Courtois, Gregory V. Bard: Algebraic Cryptanalysis of the Data Encryption
19
Standard, In 11-th IMA Conference, Cirencester, UK, 18-20 December 2007, Springer LNCS 4887. See
20
also http://eprint.iacr.org/2006/402/.
21
22
Classes and Methods
23
-------------------
24
"""
25
26
##############################################################################
27
# Copyright (C) 2008-2009 Martin Albrecht <[email protected]>
28
# Copyright (C) 2009 Michael Brickenstein <[email protected]>
29
# Copyright (C) 2010 Mate Soos
30
# Copyright (C) 2012 Martin Albrecht <[email protected]>
31
# Distributed under the terms of the GNU General Public License (GPL)
32
# The full text of the GPL is available at:
33
# http://www.gnu.org/licenses/
34
##############################################################################
35
36
from random import Random
37
from sage.rings.polynomial.pbori import if_then_else as ite
38
from sage.rings.integer_ring import ZZ
39
from sage.functions.other import ceil
40
from sage.misc.cachefunc import cached_method, cached_function
41
from sage.combinat.permutation import Permutations
42
from sage.sat.converters import ANF2CNFConverter
43
44
class CNFEncoder(ANF2CNFConverter):
45
"""
46
ANF to CNF Converter using a Dense/Sparse Strategy. This converter distinguishes two classes of
47
polynomials.
48
49
1. Sparse polynomials are those with at most ``max_vars_sparse`` variables. Those are converted
50
using reduced truth-tables based on PolyBoRi's internal representation.
51
52
2. Polynomials with more variables are converted by introducing new variables for monomials and
53
by converting these linearised polynomials.
54
55
Linearised polynomials are converted either by splitting XOR chains -- into chunks of length
56
``cutting_number`` -- or by constructing XOR clauses if the underlying solver supports it. This
57
behaviour is disabled by passing ``use_xor_clauses=False``.
58
59
.. automethod:: __init__
60
.. automethod:: __call__
61
"""
62
def __init__(self, solver, ring, max_vars_sparse=6, use_xor_clauses=None, cutting_number=6, random_seed=16):
63
"""
64
Construct ANF to CNF converter over ``ring`` passing clauses to ``solver``.
65
66
INPUT:
67
68
- ``solver`` - a SAT-solver instance
69
70
- ``ring`` - a :class:`sage.rins.polynomial.pbori.BooleanPolynomialRing`
71
72
- ``max_vars_sparse`` - maximum number of variables for direct conversion
73
74
- ``use_xor_clauses`` - use XOR clauses; if ``None`` use if
75
``solver`` supports it. (default: ``None``)
76
77
- ``cutting_number`` - maximum length of XOR chains after
78
splitting if XOR clauses are not supported (default: 6)
79
80
- ``random_seed`` - the direct conversion method uses
81
randomness, this sets the seed (default: 16)
82
83
EXAMPLE:
84
85
We compare the sparse and the dense strategies, sparse first::
86
87
sage: B.<a,b,c> = BooleanPolynomialRing()
88
sage: from sage.sat.converters.polybori import CNFEncoder
89
sage: from sage.sat.solvers.dimacs import DIMACS
90
sage: fn = tmp_filename()
91
sage: solver = DIMACS(filename=fn)
92
sage: e = CNFEncoder(solver, B)
93
sage: e.clauses_sparse(a*b + a + 1)
94
sage: _ = solver.write()
95
sage: print open(fn).read()
96
p cnf 3 2
97
1 0
98
-2 0
99
sage: e.phi
100
[None, a, b, c]
101
102
Now, we convert using the dense strategy::
103
104
sage: B.<a,b,c> = BooleanPolynomialRing()
105
sage: from sage.sat.converters.polybori import CNFEncoder
106
sage: from sage.sat.solvers.dimacs import DIMACS
107
sage: fn = tmp_filename()
108
sage: solver = DIMACS(filename=fn)
109
sage: e = CNFEncoder(solver, B)
110
sage: e.clauses_dense(a*b + a + 1)
111
sage: _ = solver.write()
112
sage: print open(fn).read()
113
p cnf 4 5
114
1 -4 0
115
2 -4 0
116
4 -1 -2 0
117
-4 -1 0
118
4 1 0
119
sage: e.phi
120
[None, a, b, c, a*b]
121
122
.. NOTE::
123
124
This constructer generates SAT variables for each Boolean polynomial variable.
125
"""
126
self.random_generator = Random(random_seed)
127
self.one_set = ring.one().set()
128
self.empty_set = ring.zero().set()
129
130
self.solver = solver
131
self.max_vars_sparse = max_vars_sparse
132
self.cutting_number = cutting_number
133
134
if use_xor_clauses is None:
135
use_xor_clauses = hasattr(solver,"add_xor_clause")
136
self.use_xor_clauses = use_xor_clauses
137
138
self.ring = ring
139
140
# If you change this, make sure we are calling m.index()
141
# below, as this relies on phi being sorted like this.
142
self._phi = [None]
143
for x in sorted([x.lm() for x in self.ring.gens()], key=lambda x: x.index()):
144
self.var(x)
145
146
def var(self, m=None, decision=None):
147
"""
148
Return a *new* variable.
149
150
This is a thin wrapper around the SAT-solvers function where
151
we keep track of which SAT variable corresponds to which
152
monomial.
153
154
INPUT:
155
156
- ``m`` - something the new variables maps to, usually a monomial
157
- ``decision`` - is this variable a deicison variable?
158
159
EXAMPLE::
160
161
sage: from sage.sat.converters.polybori import CNFEncoder
162
sage: from sage.sat.solvers.dimacs import DIMACS
163
sage: B.<a,b,c> = BooleanPolynomialRing()
164
sage: ce = CNFEncoder(DIMACS(), B)
165
sage: ce.var()
166
4
167
"""
168
self._phi.append(m)
169
return self.solver.var(decision=decision)
170
171
@property
172
def phi(self):
173
"""
174
Map SAT variables to polynomial variables.
175
176
EXAMPLE::
177
178
sage: from sage.sat.converters.polybori import CNFEncoder
179
sage: from sage.sat.solvers.dimacs import DIMACS
180
sage: B.<a,b,c> = BooleanPolynomialRing()
181
sage: ce = CNFEncoder(DIMACS(), B)
182
sage: ce.var()
183
4
184
sage: ce.phi
185
[None, a, b, c, None]
186
"""
187
return list(self._phi)
188
189
##################################################
190
# Encoding based on polynomial roots
191
##################################################
192
193
def zero_blocks(self, f):
194
"""
195
Divides the zero set of ``f`` into blocks.
196
197
EXAMPLE::
198
199
sage: B.<a,b,c> = BooleanPolynomialRing()
200
sage: from sage.sat.converters.polybori import CNFEncoder
201
sage: from sage.sat.solvers.dimacs import DIMACS
202
sage: e = CNFEncoder(DIMACS(), B)
203
sage: sorted(e.zero_blocks(a*b*c))
204
[{c: 0}, {b: 0}, {a: 0}]
205
206
.. note::
207
208
This function is randomised.
209
"""
210
variables = f.vars_as_monomial()
211
212
space = variables.divisors()
213
variables = list(variables.variables())
214
zeros = f.zeros_in(space)
215
rest = zeros
216
res = list()
217
218
def choose(s):
219
indices = []
220
assert not s.empty()
221
nav = s.navigation()
222
while not nav.constant():
223
e = nav.else_branch()
224
t = nav.then_branch()
225
if e.constant() and not e.terminal_one():
226
indices.append(nav.value())
227
nav = t
228
else:
229
if self.random_generator.randint(0,1):
230
indices.append(nav.value())
231
nav = t
232
233
else:
234
nav = e
235
assert nav.terminal_one()
236
res = self.one_set
237
for i in reversed(indices):
238
res = ite(i, res, self.empty_set)
239
return iter(res).next()
240
241
while not rest.empty():
242
l = choose(rest)
243
l_variables = set(l.variables())
244
block_dict = dict([(v, 1 if v in l_variables else 0) for v in variables])
245
l = l.set()
246
self.random_generator.shuffle(variables)
247
for v in variables:
248
candidate = l.change(v.index())
249
if candidate.diff(zeros).empty():
250
l = l.union(candidate)
251
del block_dict[v]
252
rest = rest.diff(l)
253
res.append(block_dict)
254
return res
255
256
def clauses_sparse(self, f):
257
"""
258
Convert ``f`` using the sparse strategy.
259
260
INPUT:
261
262
- ``f`` - a :class:`sage.rings.polynomial.pbori.BooleanPolynomial`
263
264
265
EXAMPLE::
266
267
sage: B.<a,b,c> = BooleanPolynomialRing()
268
sage: from sage.sat.converters.polybori import CNFEncoder
269
sage: from sage.sat.solvers.dimacs import DIMACS
270
sage: fn = tmp_filename()
271
sage: solver = DIMACS(filename=fn)
272
sage: e = CNFEncoder(solver, B)
273
sage: e.clauses_sparse(a*b + a + 1)
274
sage: _ = solver.write()
275
sage: print open(fn).read()
276
p cnf 3 2
277
1 0
278
-2 0
279
sage: e.phi
280
[None, a, b, c]
281
"""
282
# we form an expression for a var configuration *not* lying in
283
# the block it is evaluated to 0 by f, iff it is not lying in
284
# any zero block of f+1
285
286
blocks = self.zero_blocks(f+1)
287
C = [dict([(variable, 1-value) for (variable, value) in b.iteritems()]) for b in blocks ]
288
289
def to_dimacs_index(v):
290
return v.index()+1
291
292
def clause(c):
293
return [to_dimacs_index(variable) if value == 1 else -to_dimacs_index(variable) for (variable, value) in c.iteritems()]
294
295
for c in C:
296
self.solver.add_clause(clause(c))
297
298
###################################################
299
# Indirect conversion, may add new variables
300
###################################################
301
302
def clauses_dense(self, f):
303
"""
304
Convert ``f`` using the dense strategy.
305
306
INPUT:
307
308
- ``f`` - a :class:`sage.rings.polynomial.pbori.BooleanPolynomial`
309
310
EXAMPLE::
311
312
sage: B.<a,b,c> = BooleanPolynomialRing()
313
sage: from sage.sat.converters.polybori import CNFEncoder
314
sage: from sage.sat.solvers.dimacs import DIMACS
315
sage: fn = tmp_filename()
316
sage: solver = DIMACS(filename=fn)
317
sage: e = CNFEncoder(solver, B)
318
sage: e.clauses_dense(a*b + a + 1)
319
sage: _ = solver.write()
320
sage: print open(fn).read()
321
p cnf 4 5
322
1 -4 0
323
2 -4 0
324
4 -1 -2 0
325
-4 -1 0
326
4 1 0
327
sage: e.phi
328
[None, a, b, c, a*b]
329
"""
330
equal_zero = not bool(f.constant_coefficient())
331
332
f = (f - f.constant_coefficient())
333
f = [self.monomial(m) for m in f]
334
335
if self.use_xor_clauses:
336
self.solver.add_xor_clause(f, equal_zero)
337
elif f > self.cutting_number:
338
for fpart, this_equal_zero in self.split_xor(f, equal_zero):
339
ll = len(fpart)
340
for p in self.permutations(ll, this_equal_zero):
341
self.solver.add_clause([ p[i]*fpart[i] for i in range(ll) ])
342
else:
343
ll = len(f)
344
for p in self.permutations(ll, equal_zero):
345
self.solver.add_clause([ p[i]*f[i] for i in range(ll) ])
346
347
@cached_method
348
def monomial(self, m):
349
"""
350
Return SAT variable for ``m``
351
352
INPUT:
353
354
- ``m`` - a monomial.
355
356
OUTPUT: An index for a SAT variable corresponding to ``m``.
357
358
EXAMPLE::
359
360
sage: B.<a,b,c> = BooleanPolynomialRing()
361
sage: from sage.sat.converters.polybori import CNFEncoder
362
sage: from sage.sat.solvers.dimacs import DIMACS
363
sage: fn = tmp_filename()
364
sage: solver = DIMACS(filename=fn)
365
sage: e = CNFEncoder(solver, B)
366
sage: e.clauses_dense(a*b + a + 1)
367
sage: e.phi
368
[None, a, b, c, a*b]
369
370
If monomial is called on a new monomial, a new variable is created::
371
372
sage: e.monomial(a*b*c)
373
5
374
sage: e.phi
375
[None, a, b, c, a*b, a*b*c]
376
377
If monomial is called on a monomial that was queried before,
378
the index of the old variable is returned and no new variable
379
is created::
380
381
sage: e.monomial(a*b)
382
4
383
sage: e.phi
384
[None, a, b, c, a*b, a*b*c]
385
386
.. note::
387
388
For correctness, this function is cached.
389
"""
390
if m.deg() == 1:
391
return m.index()+1
392
else:
393
# we need to encode the relationship between the monomial
394
# and its variables
395
variables = [self.monomial(v) for v in m.variables()]
396
monomial = self.var(m)
397
398
# (a | -w) & (b | -w) & (w | -a | -b) <=> w == a*b
399
for v in variables:
400
self.solver.add_clause( (v, -monomial) )
401
self.solver.add_clause( tuple([monomial] + [-v for v in variables]) )
402
403
return monomial
404
405
@cached_function
406
def permutations(length, equal_zero):
407
"""
408
Return permutations of length ``length`` which are equal to
409
zero if ``equal_zero`` and equal to one otherwise.
410
411
A variable is false if the integer in its position is smaller
412
than zero and true otherwise.
413
414
INPUT:
415
416
- ``length`` - the number of variables
417
- ``equal_zero`` - should the sum be equal to zero?
418
419
EXAMPLE::
420
421
422
sage: from sage.sat.converters.polybori import CNFEncoder
423
sage: from sage.sat.solvers.dimacs import DIMACS
424
sage: B.<a,b,c> = BooleanPolynomialRing()
425
sage: ce = CNFEncoder(DIMACS(), B)
426
sage: ce.permutations(3, True)
427
[[-1, -1, -1], [1, 1, -1], [1, -1, 1], [-1, 1, 1]]
428
429
sage: ce.permutations(3, False)
430
[[1, -1, -1], [-1, 1, -1], [-1, -1, 1], [1, 1, 1]]
431
"""
432
E = []
433
for num_negated in range(0, length+1) :
434
if (((num_negated % 2) ^ ((length+1) % 2)) == equal_zero) :
435
continue
436
start = []
437
for i in range(num_negated) :
438
start.append(1)
439
for i in range(length - num_negated) :
440
start.append(-1)
441
E.extend(Permutations(start))
442
return E
443
444
def split_xor(self, monomial_list, equal_zero):
445
"""
446
Split XOR chains into subchains.
447
448
INPUT:
449
450
- ``monomial_list`` - a list of monomials
451
- ``equal_zero`` - is the constant coefficient zero?
452
453
EXAMPLE::
454
455
sage: from sage.sat.converters.polybori import CNFEncoder
456
sage: from sage.sat.solvers.dimacs import DIMACS
457
sage: B.<a,b,c,d,e,f> = BooleanPolynomialRing()
458
sage: ce = CNFEncoder(DIMACS(), B, cutting_number=3)
459
sage: ce.split_xor([1,2,3,4,5,6], False)
460
[[[1, 7], False], [[7, 2, 8], True], [[8, 3, 9], True], [[9, 4, 10], True], [[10, 5, 11], True], [[11, 6], True]]
461
462
sage: ce = CNFEncoder(DIMACS(), B, cutting_number=4)
463
sage: ce.split_xor([1,2,3,4,5,6], False)
464
[[[1, 2, 7], False], [[7, 3, 4, 8], True], [[8, 5, 6], True]]
465
466
sage: ce = CNFEncoder(DIMACS(), B, cutting_number=5)
467
sage: ce.split_xor([1,2,3,4,5,6], False)
468
[[[1, 2, 3, 7], False], [[7, 4, 5, 6], True]]
469
"""
470
c = self.cutting_number
471
472
nm = len(monomial_list)
473
step = ceil((c-2)/ZZ(nm) * nm)
474
M = []
475
476
new_variables = []
477
for j in range(0, nm, step):
478
m = new_variables + monomial_list[j:j+step]
479
if (j + step) < nm:
480
new_variables = [self.var(None)]
481
m += new_variables
482
M.append([m, equal_zero])
483
equal_zero = True
484
return M
485
486
###################################################
487
# Highlevel Functions
488
###################################################
489
490
def clauses(self, f):
491
"""
492
Convert ``f`` using the sparse strategy if ``f.nvariables()`` is
493
at most ``max_vars_sparse`` and the dense strategy otherwise.
494
495
INPUT:
496
497
- ``f`` - a :class:`sage.rings.polynomial.pbori.BooleanPolynomial`
498
499
EXAMPLE::
500
501
sage: B.<a,b,c> = BooleanPolynomialRing()
502
sage: from sage.sat.converters.polybori import CNFEncoder
503
sage: from sage.sat.solvers.dimacs import DIMACS
504
sage: fn = tmp_filename()
505
sage: solver = DIMACS(filename=fn)
506
sage: e = CNFEncoder(solver, B, max_vars_sparse=2)
507
sage: e.clauses(a*b + a + 1)
508
sage: _ = solver.write()
509
sage: print open(fn).read()
510
p cnf 3 2
511
1 0
512
-2 0
513
sage: e.phi
514
[None, a, b, c]
515
516
sage: B.<a,b,c> = BooleanPolynomialRing()
517
sage: from sage.sat.converters.polybori import CNFEncoder
518
sage: from sage.sat.solvers.dimacs import DIMACS
519
sage: fn = tmp_filename()
520
sage: solver = DIMACS(filename=fn)
521
sage: e = CNFEncoder(solver, B, max_vars_sparse=2)
522
sage: e.clauses(a*b + a + c)
523
sage: _ = solver.write()
524
sage: print open(fn).read()
525
p cnf 4 7
526
1 -4 0
527
2 -4 0
528
4 -1 -2 0
529
-4 -1 -3 0
530
4 1 -3 0
531
4 -1 3 0
532
-4 1 3 0
533
534
sage: e.phi
535
[None, a, b, c, a*b]
536
"""
537
if f.nvariables() <= self.max_vars_sparse:
538
self.clauses_sparse(f)
539
else:
540
self.clauses_dense(f)
541
542
def __call__(self, F):
543
"""
544
Encode the boolean polynomials in ``F`` .
545
546
INPUT:
547
548
- ``F`` - an iterable of :class:`sage.rings.polynomial.pbori.BooleanPolynomial`
549
550
OUTPUT: An inverse map int -> variable
551
552
553
EXAMPLE::
554
555
sage: B.<a,b,c> = BooleanPolynomialRing()
556
sage: from sage.sat.converters.polybori import CNFEncoder
557
sage: from sage.sat.solvers.dimacs import DIMACS
558
sage: fn = tmp_filename()
559
sage: solver = DIMACS(filename=fn)
560
sage: e = CNFEncoder(solver, B, max_vars_sparse=2)
561
sage: e([a*b + a + 1, a*b+ a + c])
562
[None, a, b, c, a*b]
563
sage: _ = solver.write()
564
sage: print open(fn).read()
565
p cnf 4 9
566
1 0
567
-2 0
568
1 -4 0
569
2 -4 0
570
4 -1 -2 0
571
-4 -1 -3 0
572
4 1 -3 0
573
4 -1 3 0
574
-4 1 3 0
575
576
sage: e.phi
577
[None, a, b, c, a*b]
578
"""
579
res = []
580
for f in F:
581
self.clauses(f)
582
return self.phi
583
584
####################################################
585
# Highlevel Functions
586
###################################################
587
588
def to_polynomial(self, c):
589
"""
590
Convert clause to :class:`sage.rings.polynomial.pbori.BooleanPolynomial`
591
592
INPUT:
593
594
- ``c`` - a clause
595
596
EXAMPLE::
597
598
sage: B.<a,b,c> = BooleanPolynomialRing()
599
sage: from sage.sat.converters.polybori import CNFEncoder
600
sage: from sage.sat.solvers.dimacs import DIMACS
601
sage: fn = tmp_filename()
602
sage: solver = DIMACS(filename=fn)
603
sage: e = CNFEncoder(solver, B, max_vars_sparse=2)
604
sage: _ = e([a*b + a + 1, a*b+ a + c])
605
sage: e.to_polynomial( (1,-2,3) )
606
a*b*c + a*b + b*c + b
607
"""
608
def product(l):
609
# order of these multiplications for performance
610
res = l[0]
611
for p in l[1:]:
612
res = res*p
613
return res
614
615
phi = self.phi
616
product = self.ring(1)
617
for v in c:
618
if phi[abs(v)] is None:
619
raise ValueError("Clause containst an XOR glueing variable.")
620
product *= phi[abs(v)] + int(v>0)
621
return product
622
623