python-sat 1.8.dev25__cp310-cp310-musllinux_1_2_x86_64.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.
Files changed (50) hide show
  1. pycard.cpython-310-x86_64-linux-gnu.so +0 -0
  2. pysat/__init__.py +24 -0
  3. pysat/_fileio.py +209 -0
  4. pysat/_utils.py +58 -0
  5. pysat/allies/__init__.py +0 -0
  6. pysat/allies/approxmc.py +385 -0
  7. pysat/allies/unigen.py +435 -0
  8. pysat/card.py +802 -0
  9. pysat/engines.py +1302 -0
  10. pysat/examples/__init__.py +0 -0
  11. pysat/examples/bbscan.py +663 -0
  12. pysat/examples/bica.py +691 -0
  13. pysat/examples/fm.py +527 -0
  14. pysat/examples/genhard.py +516 -0
  15. pysat/examples/hitman.py +653 -0
  16. pysat/examples/lbx.py +638 -0
  17. pysat/examples/lsu.py +496 -0
  18. pysat/examples/mcsls.py +610 -0
  19. pysat/examples/models.py +189 -0
  20. pysat/examples/musx.py +344 -0
  21. pysat/examples/optux.py +710 -0
  22. pysat/examples/primer.py +620 -0
  23. pysat/examples/rc2.py +1858 -0
  24. pysat/examples/usage.py +63 -0
  25. pysat/formula.py +5619 -0
  26. pysat/pb.py +463 -0
  27. pysat/process.py +363 -0
  28. pysat/solvers.py +7591 -0
  29. pysolvers.cpython-310-x86_64-linux-gnu.so +0 -0
  30. python_sat-1.8.dev25.data/scripts/approxmc.py +385 -0
  31. python_sat-1.8.dev25.data/scripts/bbscan.py +663 -0
  32. python_sat-1.8.dev25.data/scripts/bica.py +691 -0
  33. python_sat-1.8.dev25.data/scripts/fm.py +527 -0
  34. python_sat-1.8.dev25.data/scripts/genhard.py +516 -0
  35. python_sat-1.8.dev25.data/scripts/lbx.py +638 -0
  36. python_sat-1.8.dev25.data/scripts/lsu.py +496 -0
  37. python_sat-1.8.dev25.data/scripts/mcsls.py +610 -0
  38. python_sat-1.8.dev25.data/scripts/models.py +189 -0
  39. python_sat-1.8.dev25.data/scripts/musx.py +344 -0
  40. python_sat-1.8.dev25.data/scripts/optux.py +710 -0
  41. python_sat-1.8.dev25.data/scripts/primer.py +620 -0
  42. python_sat-1.8.dev25.data/scripts/rc2.py +1858 -0
  43. python_sat-1.8.dev25.data/scripts/unigen.py +435 -0
  44. python_sat-1.8.dev25.dist-info/METADATA +45 -0
  45. python_sat-1.8.dev25.dist-info/RECORD +50 -0
  46. python_sat-1.8.dev25.dist-info/WHEEL +5 -0
  47. python_sat-1.8.dev25.dist-info/licenses/LICENSE.txt +21 -0
  48. python_sat-1.8.dev25.dist-info/top_level.txt +3 -0
  49. python_sat.libs/libgcc_s-0cd532bd.so.1 +0 -0
  50. python_sat.libs/libstdc++-5d72f927.so.6.0.33 +0 -0
pysat/engines.py ADDED
@@ -0,0 +1,1302 @@
1
+ #!/usr/bin/env python
2
+ #-*- coding:utf-8 -*-
3
+ ##
4
+ ## engines.py
5
+ ##
6
+ ## Created on: Sep 20, 2023
7
+ ## Author: Alexey Ignatiev, Zi Li Tan
8
+ ## E-mail: alexey.ignatiev@monash.edu, ztan0050@student.monash.edu
9
+ ##
10
+
11
+ """
12
+ ===============
13
+ List of classes
14
+ ===============
15
+
16
+ .. autosummary::
17
+ :nosignatures:
18
+
19
+ Propagator
20
+ BooleanEngine
21
+ LinearConstraint
22
+ ParityConstraint
23
+
24
+ ==================
25
+ Module description
26
+ ==================
27
+
28
+ This module provides a user with the possibility to define their own
29
+ propagation engines, i.e. constraint propagators, attachable to a SAT
30
+ solver. The implementation of this functionality builds on the use of the
31
+ IPASIR-UP interface [1]_. This may come in handy when it is beneficial to
32
+ reason over non-clausal constraints, for example, in the settings of
33
+ satisfiability modulo theories (SMT), constraint programming (CP) and lazy
34
+ clause generation (LCG).
35
+
36
+ .. [1] Katalin Fazekas, Aina Niemetz, Mathias Preiner, Markus Kirchweger,
37
+ Stefan Szeider, Armin Biere. *IPASIR-UP: User Propagators for CDCL*.
38
+ SAT. 2023. pp. 8:1-8:13
39
+
40
+ .. note::
41
+
42
+ Currently, the only SAT solver available in PySAT supporting the
43
+ interface is CaDiCaL 1.9.5.
44
+
45
+ The interface allows a user to attach a single reasoning engine to the
46
+ solver. This means that if one needs to support multiple kinds of
47
+ constraints simultaneously, the implementation of the engine may need to
48
+ be sophisticated enough to make it work.
49
+
50
+ It is imperative that any propagator a user defines must inherit the
51
+ interface of the abstract class :class:`Propagator` and defines all the
52
+ required methods for the correct operation of the engine.
53
+
54
+ An example propagator is shown in the class :class:`BooleanEngine`. It
55
+ currently supports two kinds of example constraints: linear (cardinality
56
+ and pseudo-Boolean) constraints and parity (exclusive OR, XOR)
57
+ constraints. The engine can run in the *adaptive mode*, i.e. it can enable
58
+ and disable itself on the fly.
59
+
60
+ Once an engine is implemented, it should be attached to a solver object by
61
+ calling :meth:`connect_propagator` of :class:`Cadical195`. The propagator
62
+ will then need to inform the solver what variable it requires to observe.
63
+
64
+ .. code-block:: python
65
+
66
+ solver = Solver(name='cadical195', bootstrap_with=some_formula)
67
+
68
+ engine = MyPowerfulEngine(...)
69
+ solver.connect_propagator(engine)
70
+
71
+ # attached propagator wants to observe these variables
72
+ for var in range(some_variables):
73
+ solver.observe(var)
74
+
75
+ ...
76
+
77
+ .. note::
78
+
79
+ A user is encouraged to examine the source code of
80
+ :class:`BooleanEngine` in order to see how an external reasoning
81
+ engine can be implemented and attached to CaDiCaL 1.9.5. Also consult
82
+ the implementation of the corresponding methods of
83
+ :class:`.Cadical195`.
84
+
85
+ ==============
86
+ Module details
87
+ ==============
88
+ """
89
+
90
+ #
91
+ #==============================================================================
92
+ from collections import Counter, defaultdict
93
+ import functools
94
+ import itertools
95
+ from typing import List
96
+
97
+
98
+ #
99
+ #==============================================================================
100
+ class LinearConstraint:
101
+ """
102
+ A possible implementation of linear constraints over Boolean
103
+ variables, including cardinality and pseudo-Boolean constraints. Each
104
+ such constraint is meant to be in the less-than form, i.e. a user
105
+ should transform the literals, weights and the right-hand side of the
106
+ constraint into this form before creating an object of
107
+ :class:`LinearConstraint`. The class is designed to work with
108
+ :class:`BooleanEngine`.
109
+
110
+ The implementation of linear constraint propagation builds on the use
111
+ of counters. Basically, each time a literal is assigned to a positive
112
+ value, it is assumed to contribute to the total weight on the
113
+ left-hand side of the constraint, which is calculated and compared to
114
+ the right-hand side.
115
+
116
+ The constructor receives three arguments: ``lits``, ``weights``, and
117
+ ``bound``. Argument ``lits`` represents a list of literals on the
118
+ left-hand side of the constraint while argument ``weights`` contains
119
+ either a list of their weights or a dictionary mapping literals to
120
+ weights. Finally, argument ``bound`` is the right-hand side of the
121
+ constraint.
122
+
123
+ Note that if no weights are provided, each occurrence of a literal is
124
+ assumed to have weight 1.
125
+
126
+ .. note::
127
+
128
+ All weights are supposed to be non-negative values.
129
+
130
+ :param lits: list of literals (left-hand side)
131
+ :param weights: weights of the literals
132
+ :param bound: right-hand side of the constraint
133
+
134
+ :type lits:
135
+ :type weights: list or dict
136
+ :type bound: int or float
137
+ """
138
+
139
+ def __init__(self, lits=[], weights={}, bound=1):
140
+ """
141
+ Constraint initialiser.
142
+ """
143
+
144
+ if not weights or type(weights) == dict:
145
+ # the same literal may appear multiple times in the list
146
+ # so we need to calculate it's actual weight
147
+ cntr = Counter(lits)
148
+
149
+ # left-hand side in terms of literals and their weights
150
+ self.lits = sorted(cntr.keys())
151
+ self.wght = {l: cntr[l] * (weights[l] if weights else 1) for l in self.lits}
152
+ else:
153
+ assert len(lits) == len(weights), 'The number of weights differs from the number of literals'
154
+
155
+ # first, computing the weights (there may be literal repetition)
156
+ self.wght = defaultdict(lambda: 0)
157
+ for l, w in zip(lits, weights):
158
+ self.wght[l] += w
159
+
160
+ # second, extracting the list of unique literals
161
+ self.lits = sorted(self.wght.keys())
162
+
163
+ # right-hand side and value counter
164
+ self.rbnd = bound
165
+ self.vcnt = 0
166
+
167
+ # auxiliary data used during propagation
168
+ # None is needed here for propagation without an assignment
169
+ self.lset = set(self.lits + [None])
170
+ self.lval = None
171
+
172
+ # a model that falsifies the constraint (if any)
173
+ self.fmod = None
174
+
175
+ if min(self.wght.values()) == max(self.wght.values()):
176
+ if self.wght[self.lits[0]] != 1:
177
+ self.rbnd //= self.wght[self.lits[0]]
178
+ self.wght = {l: 1 for l in lits}
179
+
180
+ # unweighted / cardinality propagation
181
+ self.propagate = self.propagate_unweighted
182
+
183
+ # constraint propagates all literals at once,
184
+ # i.e. they all share the same reason
185
+ self.expl = []
186
+ self.justify = self.justify_unweighted
187
+ self.abandon = self.abandon_unweighted
188
+ else:
189
+ # weighted / pseudo-Boolean propagation
190
+ self.propagate = self.propagate_weighted
191
+
192
+ # constraint may propagate different literals
193
+ # at different points in time, reasons may differ
194
+ self.expl = defaultdict(lambda: [])
195
+ self.justify = self.justify_weighted
196
+ self.abandon = self.abandon_weighted
197
+
198
+ # for weighted case only: a flag for indicating whether we can propagate
199
+ self.done = False
200
+
201
+ # adding literal 'None' with 0-weight
202
+ self.wght[None] = 0
203
+
204
+ def register_watched(self, to_watch):
205
+ """
206
+ Add self to the centralised watched literals lists in
207
+ :class:`BooleanEngine`.
208
+ """
209
+
210
+ for lit in self.lits:
211
+ to_watch[lit].append(self)
212
+
213
+ def attach_values(self, values):
214
+ """
215
+ Give the constraint access to centralised values exposed from
216
+ :class:`BooleanEngine`.
217
+ """
218
+
219
+ self.lval = values
220
+
221
+ def propagate_unweighted(self, lit=None):
222
+ """
223
+ Get all the consequences of a given literal in the unweighted
224
+ case. The implementation *counts* how many literals on the
225
+ left-hand side are assigned to true.
226
+ """
227
+
228
+ if not self.expl:
229
+ self.vcnt += self.wght[lit]
230
+
231
+ if self.vcnt == self.rbnd: # this is when we should propagate
232
+ iter1, iter2 = itertools.tee(self.lits)
233
+ self.expl = list(-l for l in iter1 if self.lval[abs(l)] == l)
234
+ to_entail = list(-l for l in iter2 if self.lval[abs(l)] is None)
235
+
236
+ return to_entail
237
+
238
+ return []
239
+
240
+ def justify_unweighted(self, dummy_lit):
241
+ """
242
+ Provide a reason for a literal propagated by this constraint. In
243
+ the unweighted case, all the literals propagated by this
244
+ constraint share the same reason.
245
+ """
246
+
247
+ return self.expl
248
+
249
+ def abandon_unweighted(self, dummy_lit):
250
+ """
251
+ Clear the reason of a given literal.
252
+ """
253
+
254
+ self.expl.clear()
255
+
256
+ def propagate_weighted(self, lit=None):
257
+ """
258
+ Get all the consequences of a given literal in the weighted case.
259
+ The implementation counts the weights of all the literals assigned
260
+ to true and propagates all the other literals (yet unassigned)
261
+ such that adding their weights to the total sum would exceed the
262
+ right-hand side of the constraint.
263
+ """
264
+
265
+ if not self.done:
266
+ self.vcnt += self.wght[lit]
267
+
268
+ iter1, iter2, iter3 = itertools.tee(self.lits, 3)
269
+ expl = list(-l for l in iter1 if self.lval[abs(l)] == l)
270
+ to_entail = list(-l for l in iter2 if self.lval[abs(l)] is None and self.vcnt + self.wght[l] > self.rbnd and not self.expl[-l])
271
+
272
+ # counting the number of unassigned literals to check
273
+ # whether there is something left to do in the future
274
+ nunknown = functools.reduce(lambda x, y: x + 1 if self.lval[abs(y)] is not None else x, iter3, 0)
275
+
276
+ # are we done with constraint for now?
277
+ if len(to_entail) == nunknown:
278
+ self.done = True
279
+
280
+ # setting the reason for each of the propagated literals
281
+ for l in to_entail:
282
+ self.expl[l] = expl
283
+
284
+ return to_entail
285
+
286
+ return []
287
+
288
+ def justify_weighted(self, lit):
289
+ """
290
+ Provide a reason for a literal propagated by this constraint. In
291
+ the case of weighted constraints, a literal may have a reason
292
+ different from the other literals propagated by the same
293
+ constraint.
294
+ """
295
+
296
+ return self.expl[lit]
297
+
298
+ def abandon_weighted(self, lit):
299
+ """
300
+ Clear the reason of a given literal.
301
+ """
302
+
303
+ self.expl[lit].clear()
304
+
305
+ def unassign(self, lit):
306
+ """
307
+ Unassign a given literal, which is done by decrementing the
308
+ literal's contribution to the total sum of the weights of assigned
309
+ literals.
310
+ """
311
+
312
+ self.vcnt -= self.wght[lit]
313
+ self.done = False
314
+
315
+ def falsified_by(self, model):
316
+ """
317
+ Check if the constraint is violated by a given assignment. Upon
318
+ receiving such an input assignment, the method counts the sum of
319
+ the weights of all satisfied literals and checks if it exceeds the
320
+ right-hand side.
321
+ """
322
+
323
+ value = functools.reduce(lambda x, y: x + self.wght[y] if y in self.lset else x, model, 0)
324
+
325
+ if value > self.rbnd:
326
+ self.fmod = model
327
+ return True
328
+
329
+ return False
330
+
331
+ def explain_failure(self):
332
+ """
333
+ Provide a reason clause for why the previous model falsified
334
+ the constraint. This will clause will be added to the solver.
335
+ """
336
+
337
+ return [-l for l in self.fmod if l in self.lset]
338
+
339
+
340
+ #
341
+ #==============================================================================
342
+ class ParityConstraint:
343
+ """
344
+ A possible implementation of parity constraints. These are constraints
345
+ of the form :math:`l_1 \\oplus l_2 \\oplus \\ldots \\oplus l_n = b`
346
+ where each :math:`l_i` is a Boolean literal while :math:`b` is a
347
+ Boolean constant. The class is designed to exemplify the work with
348
+ :class:`BooleanEngine`.
349
+
350
+ The implementation is pretty naive. It propagates a last unassigned
351
+ literal if all but one literals got their values. The value the
352
+ propagated literals is assigned to depends on the other values in the
353
+ constraint as well as on the right-hand side value.
354
+
355
+ The constructor receives two arguments: ``lits`` and ``value``.
356
+ Argument ``lits`` represents a list of literals on the left-hand side
357
+ of the constraint while argument ``value`` represents the right-hand
358
+ side. By default, ``value`` equals ``1``.
359
+
360
+ :param lits: list of literals (left-hand side)
361
+ :param value: right-hand side of the constraint
362
+
363
+ :type lits:
364
+ :type value: bool
365
+ """
366
+
367
+ def __init__(self, lits=[], value=1):
368
+ """
369
+ Constraint initialiser.
370
+ """
371
+
372
+ # checking whether the right-hand side is correct
373
+ assert value in (0, 1), 'incorrect right-hand side value'
374
+
375
+ # left-hand side of the parity constraint
376
+ # trivially removing all the negative literals
377
+ self.lits = []
378
+ for lit in lits:
379
+ if lit < 0:
380
+ lit = -lit
381
+ value ^= value
382
+ self.lits.append(lit)
383
+
384
+ # right-hand side, current value, and variable counter
385
+ self.rval = value
386
+ self.curr = 0
387
+ self.vcnt = 0
388
+
389
+ # auxiliary data used during propagation
390
+ # None is needed here for propagation without an assignment
391
+ self.lset = set(self.lits + [None])
392
+ self.lval = None
393
+
394
+ # reason for the propagated literals
395
+ self.expl = []
396
+
397
+ # a model that falsifies the constraint (if any)
398
+ self.fmod = None
399
+
400
+ # a copy of watched literals, watched length, and
401
+ # a mapping to the index of an opposite literal
402
+ self.wlst = None
403
+ self.wopp = defaultdict(lambda: None)
404
+
405
+ # if it is unit clause then propagate its sole literal at level 0
406
+ self.zlev = self.lits[0] * (2 * self.rval - 1) if len(self.lits) == 1 else None
407
+
408
+ def register_watched(self, to_watch):
409
+ """
410
+ Add self to the centralised watched literals lists in
411
+ :class:`BooleanEngine`.
412
+ """
413
+
414
+ if len(self.lits) > 1:
415
+ # copying the watched lists
416
+ if self.wlst is None:
417
+ self.wlst = to_watch
418
+
419
+ # we need to trigger this constraint
420
+ # no matter what value a variable gets
421
+ for lit in self.lits[:2]:
422
+ self.wopp[+lit] = len(self.wlst[-lit])
423
+ self.wopp[-lit] = len(self.wlst[+lit])
424
+
425
+ self.wlst[+lit].append(self)
426
+ self.wlst[-lit].append(self)
427
+
428
+ def replace_watched(self, old, new):
429
+ """
430
+ Register this constraint as to be watched for a new literal.
431
+ """
432
+
433
+ # clean up the old literal
434
+ p, n = self.wopp[-old], self.wopp[+old]
435
+ self.wlst[+old][p] = n
436
+ self.wopp[+old] = None # technically, we don't have to remove these
437
+ self.wopp[-old] = None
438
+
439
+ # register the new literal
440
+ self.wopp[+new] = len(self.wlst[-new])
441
+ self.wopp[-new] = len(self.wlst[+new])
442
+ self.wlst[+new].append(self)
443
+ self.wlst[-new].append(self)
444
+
445
+ def attach_values(self, values):
446
+ """
447
+ Give the constraint access to centralised values exposed from
448
+ :class:`BooleanEngine`.
449
+ """
450
+
451
+ self.lval = values
452
+
453
+ def propagate(self, lit=None):
454
+ """
455
+ Get all the consequences of a given literal. Propagation here
456
+ should be similar to that of normal clauses (2-watched scheme).
457
+ """
458
+
459
+ if not self.expl:
460
+ if lit:
461
+ if self.lits[0] in (lit, -lit): # this is the first literal
462
+ # so make it the second one
463
+ self.lits[0], self.lits[1] = self.lits[1], self.lits[0]
464
+
465
+ # find the new watch
466
+ value = self.lval[abs(self.lits[1])] in self.lset
467
+ for i in range(2, len(self.lits)):
468
+ v = abs(self.lits[i])
469
+
470
+ if self.lval[v] is None: # v is going to be a new watch
471
+ self.replace_watched(lit, self.lits[i])
472
+ self.lits[1], self.lits[i] = self.lits[i], self.lits[1]
473
+ break
474
+
475
+ value ^= self.lval[v] in self.lset
476
+ else: # failed to find a new watch!
477
+ self.expl = [-self.lval[abs(self.lits[i])] for i in range(1, len(self.lits))]
478
+ to_entail = self.lits[0]
479
+ if value == self.rval:
480
+ to_entail *= -1
481
+ return [to_entail]
482
+ elif self.zlev is not None:
483
+ self.expl, to_entail, self.zlev = [], self.zlev, None
484
+ return [to_entail]
485
+
486
+ return []
487
+
488
+ def justify(self, dummy_lit):
489
+ """
490
+ Provide a reason for the literal propagated from here.
491
+ """
492
+
493
+ return self.expl
494
+
495
+ def abandon(self, dummy_lit):
496
+ """
497
+ Clear the reason of a given literal.
498
+ """
499
+
500
+ self.expl.clear()
501
+
502
+ def unassign(self, lit):
503
+ """
504
+ Unassign a given literal. A dummy method, which does nothing.
505
+ """
506
+
507
+ pass
508
+
509
+ def falsified_by(self, model):
510
+ """
511
+ Check if the constraint is violated by a given assignment, which
512
+ is done by xor'ing up all the values on the left-hand side (given
513
+ a model) and comparing with the right-hand side value.
514
+ """
515
+
516
+ value = functools.reduce(lambda x, y: x ^ 1 if y in self.lset else x, model, 0)
517
+
518
+ if value != self.rval:
519
+ self.fmod = model
520
+ return True
521
+
522
+ return False
523
+
524
+ def explain_failure(self):
525
+ """
526
+ Provide a reason clause for why the previous model falsified
527
+ the constraint. This will clause will be added to the solver.
528
+ """
529
+
530
+ return [-l for l in self.fmod if l in self.lset or -l in self.lset]
531
+
532
+
533
+ #
534
+ #==============================================================================
535
+ class Propagator(object):
536
+ """
537
+ An abstract class for creating external user-defined propagators /
538
+ reasoning engines to be used with solver :class:`Cadical195` through
539
+ the IPASIR-UP interface. All user-defined propagators should inherit
540
+ the interface of this abstract class, i.e. all the below methods need
541
+ to be properly defined. The interface is as follows:
542
+
543
+ .. code-block:: python
544
+
545
+ class Propagator(object):
546
+ def on_assignment(self, lit: int, fixed: bool = False) -> None:
547
+ pass # receive a new literal assigned by the solver
548
+
549
+ def on_new_level(self) -> None:
550
+ pass # get notified about a new decision level
551
+
552
+ def on_backtrack(self, to: int) -> None:
553
+ pass # process backtracking to a given level
554
+
555
+ def check_model(self, model: List[int]) -> bool:
556
+ pass # check if a given assignment is indeed a model
557
+
558
+ def decide(self) -> int:
559
+ return 0 # make a decision and (if any) inform the solver
560
+
561
+ def propagate(self) -> List[int]:
562
+ return [] # propagate and return inferred literals (if any)
563
+
564
+ def provide_reason(self, lit: int) -> List[int]:
565
+ pass # explain why a given literal was propagated
566
+
567
+ def add_clause(self) -> List[int]:
568
+ return [] # add an(y) external clause to the solver
569
+ """
570
+
571
+ def __init__(self):
572
+ """
573
+ Initialiser / constructor. Declare/define all the variables
574
+ required for the propagator's functionality here.
575
+ """
576
+
577
+ # internal flag to declare the propagator "lazy"
578
+ # (a lazy propagator only checks complete assignments;
579
+ # currently, checked only when it gets connected)
580
+ self.is_lazy = False
581
+
582
+ def __delete__(self):
583
+ """
584
+ Destructor.
585
+ """
586
+
587
+ pass
588
+
589
+ def on_assignment(self, lit: int, fixed: bool = False) -> None:
590
+ """
591
+ The method is called to notify the propagator about an assignment
592
+ made for one of the observed variables. An assignment is set to be
593
+ "fixed" if it is permanent, i.e. the propagator is not allowed to
594
+ undo it.
595
+
596
+ :param lit: assigned literal
597
+ :param fixed: a flag to mark the assignment as "fixed"
598
+
599
+ :type lit: int
600
+ :type fixed: bool
601
+ """
602
+
603
+ pass
604
+
605
+ def on_new_level(self) -> None:
606
+ """
607
+ The method called to notify the propagator about a new decision
608
+ level created by the solver.
609
+ """
610
+
611
+ pass
612
+
613
+ def on_backtrack(self, to: int) -> None:
614
+ """
615
+ The method for notifying the propagator about backtracking to a
616
+ given decision level. Accepts a single argument ``to`` signifying
617
+ the backtrack level.
618
+
619
+ :param to: backtrack level
620
+ :type to: int
621
+ """
622
+
623
+ pass
624
+
625
+ def check_model(self, model: List[int]) -> bool:
626
+ """
627
+ The method is used for checking if a given (complete) truth
628
+ assignment satisfies the constraint managed by the propagator.
629
+ Receives a single argument storing the truth assignment found by
630
+ the solver.
631
+
632
+ .. note::
633
+
634
+ If this method returns ``False``, the propagator must be ready
635
+ to provide an external clause in the following callback.
636
+
637
+ :param model: a list of integers representing the current model
638
+ :type model: iterable(int)
639
+
640
+ :rtype: bool
641
+ """
642
+
643
+ pass
644
+
645
+ def decide(self) -> int:
646
+ """
647
+ This method allows the propagator to influence the decision
648
+ process. Namely, it is used when the solver asks the propagator
649
+ for the next decision literal (if any). If the method returns
650
+ ``0``, the solver will make its own choice.
651
+
652
+ :rtype: int
653
+ """
654
+
655
+ return 0
656
+
657
+ def propagate(self) -> List[int]:
658
+ """
659
+ The method should invoke propagation under the current assignment.
660
+ It can return either a list of literals propagated or an empty
661
+ list ``[]``, informing the solver that no propagation is made
662
+ under the current assignment.
663
+
664
+ :rtype: int
665
+ """
666
+
667
+ return []
668
+
669
+ def provide_reason(self, lit: int) -> List[int]:
670
+ """
671
+ The method is called by the solver when asking the propagator for
672
+ the reason / antecedent clause for a literal the propagator
673
+ previously inferred. This clause will be used in the following
674
+ conflict analysis.
675
+
676
+ .. note::
677
+
678
+ The clause must contain the propagated literal.
679
+
680
+ :param lit: literal to provide reason for
681
+ :type lit: int
682
+
683
+ :rtype: iterable(int)
684
+ """
685
+
686
+ pass
687
+
688
+ def add_clause(self) -> List[int]:
689
+ """
690
+ The method is called by the solver to add an external clause if
691
+ there is any. The clause can be arbitrary but if it is
692
+ root-satisfied or tautological, the solver will ignore it without
693
+ learning it.
694
+
695
+ Root-falsified literals are eagerly removed from the clause.
696
+ Falsified clauses trigger conflict analysis, propagating clauses
697
+ trigger propagation. Unit clauses always (unless root-satisfied,
698
+ see above) trigger backtracking to level 0.
699
+
700
+ An empty clause (or root falsified clause, see above) makes the
701
+ formula unsatisfiable and stops the search immediately.
702
+
703
+ :rtype: iterable(int)
704
+ """
705
+
706
+ pass
707
+
708
+
709
+ #
710
+ #==============================================================================
711
+ class BooleanEngine(Propagator):
712
+ """
713
+ A simple *example* Boolean constraint propagator inheriting from the
714
+ class :class:`Propagator`. The idea is to exemplify the use of
715
+ external reasoning engines. The engine should be general enough to
716
+ support various constraints over Boolean variables.
717
+
718
+ .. note::
719
+
720
+ Note that this is not meant to be a model implementation of an
721
+ external engine. One can devise a more efficient implementation
722
+ with the same functionality.
723
+
724
+ The initialiser of of the class object may be provided with a list of
725
+ constraints, each being a tuple ('type', constraint), as a value for
726
+ parameter ``bootstrap_with``.
727
+
728
+ Currently, there are two types of constraints supported (to be
729
+ specified) in the constraints passed in: ``'linear'`` and ``'parity'``
730
+ (exclusive OR). The former will be handled as objects of class
731
+ :class:`LinearConstraint` while the latter will be transformed into
732
+ objects of :class:`ParityConstraint`.
733
+
734
+ Here, each type of constraint is meant to have a list of literals
735
+ stored in variable ``.lits``. This is required to set up watched lists
736
+ properly.
737
+
738
+ The second keyword argument ``adaptive`` (set to ``True`` by default)
739
+ denotes the fact that the engine should check its own efficiency and
740
+ disable or enable itself on the fly. This functionality is meant to
741
+ exemplify how adaptive external engines can be created. A user is
742
+ referred to the source code of the implementation for the details.
743
+ """
744
+
745
+ def __init__(self, bootstrap_with=[], adaptive=True):
746
+ """
747
+ Initialiser.
748
+ """
749
+
750
+ self.vars = [] # all known variables
751
+ self.vset = set() # a set of all variables
752
+ self.cons = [] # all known constraints
753
+ self.lins = [] # linear constraints
754
+ self.xors = [] # parity constraints
755
+ self.wlst = defaultdict(lambda: []) # constraints to watch
756
+
757
+ # this propagator may disable itself depending on the circumstances
758
+ self.is_adaptive = adaptive
759
+
760
+ # setting up individual constraint handlers
761
+ if bootstrap_with:
762
+ for cs in bootstrap_with:
763
+ cs = self._add_constraint(cs)
764
+
765
+ # updating the set of known variables
766
+ self.vset.update(set(abs(l) for l in cs.lits))
767
+
768
+ # setting all known variables
769
+ self.vars = sorted(self.vset)
770
+
771
+ # run the preprocessing techniques (if any)
772
+ self.preprocess()
773
+
774
+ # attaching new constraints after processing to the watched lists
775
+ for cs in self.cons:
776
+ cs.register_watched(self.wlst)
777
+
778
+ # value and trail handling
779
+ self.value = {v: None for v in self.vars}
780
+ self.fixed = {v: False for v in self.vars}
781
+ self.trail = []
782
+ self.trlim = []
783
+ self.props = defaultdict(lambda: [])
784
+ self.qhead = None
785
+
786
+ # decision level
787
+ self.decision_level = 0
788
+ self.level = {v: None for v in self.vars}
789
+
790
+ # reasons for propagated literals
791
+ self.origin = defaultdict(lambda: None)
792
+
793
+ # this variable points to the constraint previously violated by a
794
+ # given model offered by the solver
795
+ self.falsified = None
796
+
797
+ # giving constraints access to the values
798
+ for cs in self.cons:
799
+ cs.attach_values(self.value)
800
+
801
+ # currently, no solver is known to this propagator
802
+ # this will be update upon calling self.setup_observe()
803
+ self.solver = None
804
+
805
+ # the following constants are used in adaptive mode:
806
+ # the number of calls to propagate() between model checks and
807
+ # the number of propagated literals found between those checks
808
+ # followed by decaying propagation ratio-related constants
809
+ self.nprops, self.ncalls = 0, 0
810
+ self.pratio, self.pdecay, self.pbound = 0, 2, 0.2
811
+
812
+ # decaying ratio of falsifying assignments
813
+ # over all assignments offered by the solver
814
+ self.mratio, self.mdecay, self.mbound = 0, 2, 2
815
+
816
+ def adaptive_constants(self, pdecay, pbound, mdecay, mbound):
817
+ """
818
+ Set magic numeric constants used in adaptive mode.
819
+ """
820
+
821
+ self.pdecay = pdecay
822
+ self.pbound = pbound
823
+
824
+ self.mdecay = mdecay
825
+ self.mbound = mbound
826
+
827
+ def enable(self):
828
+ """
829
+ Notify the solver that the propagator is willing to become active
830
+ from now on.
831
+ """
832
+
833
+ assert self.solver, 'Solver is not connected!'
834
+ self.solver.enable_propagator()
835
+
836
+ def disable(self):
837
+ """
838
+ Notify the solver that the propagator should become inactive as it
839
+ does not contribute much to the inference process. From now on, it
840
+ will only be called to check complete models obtained by the
841
+ solver (see :meth:`check_model`).
842
+ """
843
+
844
+ assert self.solver, 'Solver is not connected!'
845
+ self.solver.disable_propagator()
846
+
847
+ def is_active(self):
848
+ """
849
+ Return engine's status. It is deemed active if the method returns
850
+ ``True`` and passive otherwise.
851
+ """
852
+
853
+ assert self.solver, 'Solver is not connected!'
854
+ return self.solver.propagator_active()
855
+
856
+ def add_constraint(self, constraint):
857
+ """
858
+ Add a new constraint to the engine and integrate it to the
859
+ internal structures, i.e. watched lists. Also, return the newly
860
+ added constraint to the callee.
861
+ """
862
+
863
+ cs = self._add_constraint(constraint)
864
+
865
+ # adding it to the watched lists
866
+ cs.register_watched(self.wlst)
867
+
868
+ # updating the other structures of the propagator
869
+ for lit in cs.lits:
870
+ var = abs(lit)
871
+ if var not in self.vset:
872
+ # this variable is currently unknown
873
+ self.vset.add(var)
874
+ self.value[var] = None
875
+ self.level[var] = None
876
+ self.fixed[var] = False
877
+
878
+ # letting the solver know we observe this variable
879
+ self.solver.observe(var)
880
+
881
+ # passing values to the constraint
882
+ cs.attach_values(self.value)
883
+
884
+ def _add_constraint(self, cs):
885
+ """
886
+ Create a new constraint, add to the list and return for further
887
+ processing.
888
+ """
889
+
890
+ if cs[0] == 'linear':
891
+ cs = LinearConstraint(lits=cs[1][0],
892
+ weights={} if len(cs[1]) == 2 else cs[1][2],
893
+ bound=cs[1][1])
894
+ self.lins.append(cs)
895
+ elif cs[0] == 'parity':
896
+ cs = ParityConstraint(lits=cs[1][0], value=cs[1][1])
897
+ self.xors.append(cs)
898
+ else:
899
+ assert 0, 'Unknown type of constraint'
900
+
901
+ # adding the newly created constraint
902
+ # to the list of known constraints
903
+ self.cons.append(cs)
904
+
905
+ return cs
906
+
907
+ def setup_observe(self, solver):
908
+ """
909
+ Inform the solver about all the variables the engine is interested
910
+ in. The solver will mark them as observed by the propagator.
911
+ """
912
+
913
+ # saving the solver for the future
914
+ self.solver = solver
915
+
916
+ for var in self.vars:
917
+ self.solver.observe(var)
918
+
919
+ def preprocess(self):
920
+ """
921
+ Run some (naive) preprocessing techniques if available for the
922
+ types of constraints under considerations. Each type of
923
+ constraints is handled separately of the rest of constraints.
924
+ """
925
+
926
+ if self.lins:
927
+ self.process_linear()
928
+
929
+ if self.xors:
930
+ self.process_parity()
931
+
932
+ def process_linear(self):
933
+ """
934
+ Process linear constraints. Here we apply simple pairwise
935
+ summation of constraints. As the number of result constraints is
936
+ quadratic, we stop the process as soon as we get 100 new
937
+ constraints. Also, if a result of the sum is longer than each of
938
+ the summands, the result constraint is ignored.
939
+
940
+ This is trivial procedure is made to illustrate how constraint
941
+ processing can be done. It can be made dependent on user-specified
942
+ parameters, e.g. the number of rounds or a numeric value
943
+ indicating when a pair of constraints should be added and when
944
+ they should not be added. For consideration in the future.
945
+ """
946
+
947
+ newc = []
948
+
949
+ for cs1, cs2 in itertools.combinations(self.lins, 2):
950
+ seen, wght, remd = set(), defaultdict(lambda: 0), 0
951
+
952
+ # traversing the first constraint and handling clashes
953
+ for l in cs1.lits:
954
+ if l in cs2.lset:
955
+ # the literal appears in both constraints
956
+ wght[l] = cs1.wght[l] + cs2.wght[l]
957
+ elif -l in cs2.lset:
958
+ # the literal appears in opposite phases
959
+ maxp = max([(l, cs1.wght[l]), (-l, cs2.wght[-l])], key=lambda pair: pair[1])
960
+ minp = min([(l, cs1.wght[l]), (-l, cs2.wght[-l])], key=lambda pair: pair[1])
961
+
962
+ if maxp[1] != minp[1]: # the weights aren't equal
963
+ wght[maxp[0]] = maxp[1] - minp[1]
964
+
965
+ # saving the remainder
966
+ remd += minp[1]
967
+ else:
968
+ # no clash
969
+ wght[l] = cs1.wght[l]
970
+
971
+ seen.add(abs(l))
972
+
973
+ # traversing the remainder of 2nd constraints; no clashes
974
+ for l in cs2.lits:
975
+ if abs(l) in seen:
976
+ continue
977
+ wght[l] = cs2.wght[l]
978
+
979
+ # right-hand side of the result constraint
980
+ rbnd = cs1.rbnd + cs2.rbnd - remd
981
+
982
+ if wght and rbnd >= 0:
983
+ # left-hand size of the result constraint
984
+ lits = sorted(wght.keys())
985
+
986
+ if len(lits) > len(cs1.lits) and len(lits) > len(cs2.lits):
987
+ # the result constraint is not valuable -> ignore it
988
+ continue
989
+
990
+ # all good, recording the constraint
991
+ newc.append(['linear', [lits, rbnd, wght]])
992
+
993
+ if len(newc) == 100:
994
+ break
995
+
996
+ elif rbnd < 0: # negative bound signifies unsatisfiability
997
+ # stopping immediately
998
+ newc = [('linear', [[cs1.lits[0]], 0]), ('linear', [[-cs1.lits[0]], 0])]
999
+ break
1000
+
1001
+ if newc: # there are some new constraints to add
1002
+ for cs in newc:
1003
+ # adding constraint to the engine
1004
+ cs = self._add_constraint(cs)
1005
+
1006
+ # updating the set of known variables
1007
+ self.vset.update(set(abs(l) for l in cs.lits))
1008
+
1009
+ # updating all known variables
1010
+ self.vars = sorted(self.vset)
1011
+
1012
+ def process_parity(self):
1013
+ """
1014
+ Process parity/XOR constraints. Basically, this runs Gaussian
1015
+ elimination and see if anything can be derived from it.
1016
+ """
1017
+
1018
+ status = True
1019
+
1020
+ # removing None from all sets
1021
+ for cs in self.xors:
1022
+ cs.lset.remove(None)
1023
+
1024
+ # forward simplification
1025
+ for i, csi in enumerate(self.xors):
1026
+ v = csi.lits[0] # pivot variable
1027
+
1028
+ # going over all the other constraints and eliminating the pivot
1029
+ for j in range(i + 1, len(self.xors)):
1030
+ csj = self.xors[j]
1031
+
1032
+ if v not in csj.lset: # do nothing if pivot var is not present
1033
+ continue
1034
+
1035
+ # summing i'th and j'th constraints up
1036
+ csj.lset ^= csi.lset
1037
+ csj.rval ^= csi.rval
1038
+
1039
+ if not csj.lset and csj.rval:
1040
+ # inconsistency detected
1041
+ status = False
1042
+
1043
+ # adding two inconsistent level-0 literals: 1 and -1
1044
+ csi.lits, csj.lits = [1], [-1]
1045
+ csi.zlev, csj.zlev = 1, -1
1046
+
1047
+ break
1048
+
1049
+ # final result
1050
+ csj.lits = sorted(csj.lset)
1051
+
1052
+ if status:
1053
+ # back substitution if no inconsistency was detected
1054
+ vals = {}
1055
+ for i in range(len(self.xors) - 1, -1, -1):
1056
+ cs = self.xors[i]
1057
+
1058
+ lits = []
1059
+ for l in cs.lits:
1060
+ if l in vals:
1061
+ cs.rval ^= vals[l] # substitution
1062
+ else:
1063
+ lits.append(l)
1064
+ cs.lits = lits
1065
+
1066
+ if len(cs.lits) == 1: # checking if substitution propagates
1067
+ vals[cs.lits[0]] = cs.rval
1068
+ cs.zlev = cs.lits[0] * (2 * cs.rval - 1)
1069
+
1070
+ # updating the sets of literals and putting None back
1071
+ for cs in self.xors:
1072
+ cs.lset = set(cs.lits + [None])
1073
+
1074
+ def on_assignment(self, lit, fixed):
1075
+ """
1076
+ Update the propagator's state given a new assignment.
1077
+ """
1078
+
1079
+ self.level[abs(lit)] = self.decision_level
1080
+
1081
+ if self.qhead is None:
1082
+ # this is the first assignment we get after backtracking
1083
+ # marking that next propagation should start from here
1084
+ self.qhead = len(self.trail)
1085
+
1086
+ self.trail.append(lit)
1087
+
1088
+ if fixed:
1089
+ self.fixed[abs(lit)] = True
1090
+
1091
+ def on_new_level(self):
1092
+ """
1093
+ Keep track of decision level updates.
1094
+ """
1095
+
1096
+ self.decision_level += 1
1097
+ self.trlim.append(len(self.trail))
1098
+
1099
+ def on_backtrack(self, to):
1100
+ """
1101
+ Cancel all the decisions up to a certain level.
1102
+ """
1103
+
1104
+ # undoing the assignments
1105
+ while len(self.trlim) > to:
1106
+ while len(self.trail) > self.trlim[-1]:
1107
+ lit = self.trail.pop()
1108
+ var = abs(lit)
1109
+
1110
+ if self.value[var] is not None and not self.fixed[var]:
1111
+ # informing all the constraints
1112
+ for cs in self.wlst[lit]:
1113
+ cs.unassign(lit)
1114
+
1115
+ self.value[var] = None
1116
+
1117
+ self.level[var] = None
1118
+
1119
+ # cleaning all the consequences
1120
+ for l in self.props[lit]:
1121
+ self.origin[l].abandon(l)
1122
+ self.origin[l] = None
1123
+ self.props[lit] = []
1124
+
1125
+ self.trlim.pop()
1126
+
1127
+ # updating decision level
1128
+ self.decision_level = to
1129
+
1130
+ # updating queue head
1131
+ self.qhead = None
1132
+
1133
+ def check_model(self, model):
1134
+ """
1135
+ Check if a given model satisfies all the constraints.
1136
+ """
1137
+
1138
+ st = True # no falsified constraints by default
1139
+
1140
+ # checking all constraints one by one
1141
+ for cs in self.cons:
1142
+ if cs.falsified_by(model):
1143
+ self.falsified = cs
1144
+ st = False
1145
+ break
1146
+
1147
+ if self.is_adaptive:
1148
+ self.adaptive_update(st)
1149
+
1150
+ return st
1151
+
1152
+ def adaptive_update(self, satisfied):
1153
+ """
1154
+ Update adaptive mode: either enable or disable the engine. This
1155
+ depends on the statistics accumulated in the current run and
1156
+ whether or not the previous assignment found by the solver
1157
+ satisfied the constraints.
1158
+ """
1159
+
1160
+ # decaying increment
1161
+ self.pratio = self.pratio / self.pdecay + (self.nprops / self.ncalls if self.ncalls else 0)
1162
+
1163
+ # taking into account whether the current model falsified something
1164
+ self.mratio = self.mratio / self.mdecay + (0 if satisfied else 1)
1165
+
1166
+ if satisfied and self.is_active():
1167
+ # we are currently active and it does not pay off
1168
+ # in terms of propagation; should we slack off a bit?
1169
+ if self.pratio < self.pbound:
1170
+ self.disable()
1171
+ elif not satisfied and not self.is_active():
1172
+ # we are currently passive and there are many conflicts
1173
+ # let's put more effort into propagation to avoid them?
1174
+ if self.mratio > self.mbound:
1175
+ self.enable()
1176
+
1177
+ # resetting the moving average stats
1178
+ self.nprops = self.ncalls = 0
1179
+
1180
+ def propagate(self):
1181
+ """
1182
+ Run the propagator given the current assignment.
1183
+ """
1184
+
1185
+ result = []
1186
+
1187
+ if self.qhead is not None:
1188
+ while self.qhead < len(self.trail):
1189
+ lit = self.trail[self.qhead]
1190
+
1191
+ # if we have just propagated an opposite value,
1192
+ # we stop and expect conflict analysis to be performed
1193
+ if self.origin[-lit] is not None:
1194
+ break
1195
+
1196
+ self.value[abs(lit)] = lit # actually setting the value
1197
+
1198
+ watched_holes = []
1199
+ for csid, cs in enumerate(self.wlst[lit]):
1200
+ # checking the constraint
1201
+ propagated = cs.propagate(lit)
1202
+
1203
+ # extracting the reason
1204
+ for l in propagated:
1205
+ if self.origin[l] is None:
1206
+ # this literal is currently unknown
1207
+ self.origin[l] = cs
1208
+
1209
+ # augment the list of entailed literals to return
1210
+ result.append(l)
1211
+
1212
+ # recording all the dependencies
1213
+ self.props[lit].append(l)
1214
+
1215
+ # checking whether or not this cs is still watched
1216
+ if self.wlst[lit][csid] is None or type(self.wlst[lit][csid]) == int:
1217
+ # instead of cs, we have None or an integer, i.e. we
1218
+ # removed cs from the watched list for this literal
1219
+ watched_holes.append((csid, self.wlst[lit][csid]))
1220
+
1221
+ # cleaning up watches, should be more-or-less cheap,
1222
+ # doing so for +lit but also for -lit if it is affected
1223
+ if watched_holes:
1224
+ self.cleanup_watched(lit, watched_holes)
1225
+
1226
+ self.qhead += 1
1227
+
1228
+ if self.is_adaptive:
1229
+ # collecting the stats
1230
+ self.nprops += len(result)
1231
+ self.ncalls += 1
1232
+ else:
1233
+ # propagating without an assignment
1234
+ for cs in self.cons:
1235
+ propagated = cs.propagate()
1236
+
1237
+ # extracting the (empty) reason
1238
+ for l in propagated:
1239
+ if self.origin[l] is None:
1240
+ # this literal is currently unknown
1241
+ self.origin[l] = cs
1242
+
1243
+ # augment the list of entailed literals to return
1244
+ result.append(l)
1245
+
1246
+ return result
1247
+
1248
+ def cleanup_watched(self, lit, garbage):
1249
+ """
1250
+ Garbage collect holes in the watched list for +lit
1251
+ (and potentially for -lit).
1252
+ """
1253
+
1254
+ otherside = []
1255
+
1256
+ # going backwards, to make it work
1257
+ for i in range(len(garbage) - 1, -1, -1):
1258
+ pid, nid = garbage[i]
1259
+
1260
+ self.wlst[lit][pid] = self.wlst[lit][-1]
1261
+ self.wlst[lit].pop()
1262
+
1263
+ if nid is not None:
1264
+ # this one has a negative counterpart
1265
+ if pid < len(self.wlst[lit]):
1266
+ # and it's affected
1267
+ self.wlst[lit][pid].wopp[-lit] = pid
1268
+
1269
+ otherside.append(nid)
1270
+
1271
+ # taking care of the negative counterparts (if those are affected)
1272
+ for nid in sorted(otherside, reverse=True):
1273
+ self.wlst[-lit][nid] = self.wlst[-lit][-1]
1274
+ self.wlst[-lit].pop()
1275
+
1276
+ if nid < len(self.wlst[-lit]):
1277
+ self.wlst[-lit][nid].wopp[lit] = nid
1278
+
1279
+ def provide_reason(self, lit):
1280
+ """
1281
+ Return the reason clause for a given literal.
1282
+ """
1283
+
1284
+ return [lit] + self.origin[lit].justify(lit)
1285
+
1286
+ def add_clause(self):
1287
+ """
1288
+ Extract a new clause to add to the solver if one exists; return an
1289
+ empty clause ``[]`` otherwise.
1290
+ """
1291
+
1292
+ if self.falsified is None:
1293
+ return []
1294
+
1295
+ to_add = self.falsified.explain_failure()
1296
+
1297
+ # cleaning the previous failure (currently being added)
1298
+ # technically, we should clean self.falsified.fmod too
1299
+ # but it should be safe enough not to do so
1300
+ self.falsified = None
1301
+
1302
+ return to_add