python-sat 1.9.dev6__cp313-cp313-pyemscripten_2025_0_wasm32.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.

Potentially problematic release.


This version of python-sat might be problematic. Click here for more details.

Files changed (50) hide show
  1. pycard.cpython-313-wasm32-emscripten.so +0 -0
  2. pyformula.cpython-313-wasm32-emscripten.so +0 -0
  3. pysat/__init__.py +24 -0
  4. pysat/_fileio.py +224 -0
  5. pysat/_utils.py +58 -0
  6. pysat/allies/__init__.py +0 -0
  7. pysat/allies/approxmc.py +385 -0
  8. pysat/allies/unigen.py +435 -0
  9. pysat/card.py +802 -0
  10. pysat/engines.py +1328 -0
  11. pysat/examples/__init__.py +0 -0
  12. pysat/examples/bbscan.py +663 -0
  13. pysat/examples/bica.py +691 -0
  14. pysat/examples/fm.py +527 -0
  15. pysat/examples/genhard.py +516 -0
  16. pysat/examples/hitman.py +653 -0
  17. pysat/examples/lbx.py +638 -0
  18. pysat/examples/lsu.py +496 -0
  19. pysat/examples/mcsls.py +610 -0
  20. pysat/examples/models.py +189 -0
  21. pysat/examples/musx.py +344 -0
  22. pysat/examples/optux.py +710 -0
  23. pysat/examples/primer.py +620 -0
  24. pysat/examples/rc2.py +2033 -0
  25. pysat/examples/usage.py +63 -0
  26. pysat/formula.py +5777 -0
  27. pysat/integer.py +2606 -0
  28. pysat/pb.py +463 -0
  29. pysat/process.py +363 -0
  30. pysat/solvers.py +9082 -0
  31. pysolvers.cpython-313-wasm32-emscripten.so +0 -0
  32. python_sat-1.9.dev6.data/scripts/approxmc.py +385 -0
  33. python_sat-1.9.dev6.data/scripts/bbscan.py +663 -0
  34. python_sat-1.9.dev6.data/scripts/bica.py +691 -0
  35. python_sat-1.9.dev6.data/scripts/fm.py +527 -0
  36. python_sat-1.9.dev6.data/scripts/genhard.py +516 -0
  37. python_sat-1.9.dev6.data/scripts/lbx.py +638 -0
  38. python_sat-1.9.dev6.data/scripts/lsu.py +496 -0
  39. python_sat-1.9.dev6.data/scripts/mcsls.py +610 -0
  40. python_sat-1.9.dev6.data/scripts/models.py +189 -0
  41. python_sat-1.9.dev6.data/scripts/musx.py +344 -0
  42. python_sat-1.9.dev6.data/scripts/optux.py +710 -0
  43. python_sat-1.9.dev6.data/scripts/primer.py +620 -0
  44. python_sat-1.9.dev6.data/scripts/rc2.py +2033 -0
  45. python_sat-1.9.dev6.data/scripts/unigen.py +435 -0
  46. python_sat-1.9.dev6.dist-info/METADATA +45 -0
  47. python_sat-1.9.dev6.dist-info/RECORD +50 -0
  48. python_sat-1.9.dev6.dist-info/WHEEL +5 -0
  49. python_sat-1.9.dev6.dist-info/licenses/LICENSE.txt +21 -0
  50. python_sat-1.9.dev6.dist-info/top_level.txt +4 -0
pysat/engines.py ADDED
@@ -0,0 +1,1328 @@
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 has_clause(self) -> bool:
568
+ return False # optional: report whether an external clause exists
569
+
570
+ def add_clause(self) -> List[int]:
571
+ return [] # if has_clause() is used, may also be the empty clause
572
+ """
573
+
574
+ def __init__(self):
575
+ """
576
+ Initialiser / constructor. Declare/define all the variables
577
+ required for the propagator's functionality here.
578
+ """
579
+
580
+ # internal flag to declare the propagator "lazy"
581
+ # (a lazy propagator only checks complete assignments;
582
+ # currently, checked only when it gets connected)
583
+ self.is_lazy = False
584
+
585
+ def __delete__(self):
586
+ """
587
+ Destructor.
588
+ """
589
+
590
+ pass
591
+
592
+ def on_assignment(self, lit: int, fixed: bool = False) -> None:
593
+ """
594
+ The method is called to notify the propagator about an assignment
595
+ made for one of the observed variables. An assignment is set to be
596
+ "fixed" if it is permanent, i.e. the propagator is not allowed to
597
+ undo it.
598
+
599
+ :param lit: assigned literal
600
+ :param fixed: a flag to mark the assignment as "fixed"
601
+
602
+ :type lit: int
603
+ :type fixed: bool
604
+ """
605
+
606
+ pass
607
+
608
+ def on_new_level(self) -> None:
609
+ """
610
+ The method called to notify the propagator about a new decision
611
+ level created by the solver.
612
+ """
613
+
614
+ pass
615
+
616
+ def on_backtrack(self, to: int) -> None:
617
+ """
618
+ The method for notifying the propagator about backtracking to a
619
+ given decision level. Accepts a single argument ``to`` signifying
620
+ the backtrack level.
621
+
622
+ :param to: backtrack level
623
+ :type to: int
624
+ """
625
+
626
+ pass
627
+
628
+ def check_model(self, model: List[int]) -> bool:
629
+ """
630
+ The method is used for checking if a given (complete) truth
631
+ assignment satisfies the constraint managed by the propagator.
632
+ Receives a single argument storing the truth assignment found by
633
+ the solver.
634
+
635
+ .. note::
636
+
637
+ If this method returns ``False``, the propagator must be ready
638
+ to provide an external clause in the following callback.
639
+
640
+ :param model: a list of integers representing the current model
641
+ :type model: iterable(int)
642
+
643
+ :rtype: bool
644
+ """
645
+
646
+ pass
647
+
648
+ def decide(self) -> int:
649
+ """
650
+ This method allows the propagator to influence the decision
651
+ process. Namely, it is used when the solver asks the propagator
652
+ for the next decision literal (if any). If the method returns
653
+ ``0``, the solver will make its own choice.
654
+
655
+ :rtype: int
656
+ """
657
+
658
+ return 0
659
+
660
+ def propagate(self) -> List[int]:
661
+ """
662
+ The method should invoke propagation under the current assignment.
663
+ It can return either a list of literals propagated or an empty
664
+ list ``[]``, informing the solver that no propagation is made
665
+ under the current assignment.
666
+
667
+ :rtype: int
668
+ """
669
+
670
+ return []
671
+
672
+ def provide_reason(self, lit: int) -> List[int]:
673
+ """
674
+ The method is called by the solver when asking the propagator for
675
+ the reason / antecedent clause for a literal the propagator
676
+ previously inferred. This clause will be used in the following
677
+ conflict analysis.
678
+
679
+ .. note::
680
+
681
+ The clause must contain the propagated literal.
682
+
683
+ :param lit: literal to provide reason for
684
+ :type lit: int
685
+
686
+ :rtype: iterable(int)
687
+ """
688
+
689
+ pass
690
+
691
+ def add_clause(self) -> List[int]:
692
+ """
693
+ The method is called by the solver to obtain an external clause.
694
+ The clause can be arbitrary but if it is root-satisfied or
695
+ tautological, the solver will ignore it without learning it.
696
+
697
+ Root-falsified literals are eagerly removed from the clause.
698
+ Falsified clauses trigger conflict analysis, propagating clauses
699
+ trigger propagation. Unit clauses always (unless root-satisfied,
700
+ see above) trigger backtracking to level 0.
701
+
702
+ An empty clause (or root falsified clause, see above) makes the
703
+ formula unsatisfiable and stops the search immediately.
704
+
705
+ .. note::
706
+
707
+ The exact meaning of returning ``[]`` depends on whether the
708
+ propagator defines method ``has_clause()``.
709
+
710
+ - If ``has_clause()`` is defined, the solver first queries it.
711
+ Whenever it returns ``True``, the follow-up call of
712
+ :meth:`add_clause` is expected to return the clause itself,
713
+ and ``[]`` denotes the actual empty clause.
714
+ - Otherwise, if ``has_clause()`` is undefined, returning
715
+ ``[]`` from :meth:`add_clause` is interpreted as saying that
716
+ there is no clause to add.
717
+
718
+ :rtype: iterable(int)
719
+ """
720
+
721
+ pass
722
+
723
+
724
+ #
725
+ #==============================================================================
726
+ class BooleanEngine(Propagator):
727
+ """
728
+ A simple *example* Boolean constraint propagator inheriting from the
729
+ class :class:`Propagator`. The idea is to exemplify the use of
730
+ external reasoning engines. The engine should be general enough to
731
+ support various constraints over Boolean variables.
732
+
733
+ .. note::
734
+
735
+ Note that this is not meant to be a model implementation of an
736
+ external engine. One can devise a more efficient implementation
737
+ with the same functionality.
738
+
739
+ The initialiser of of the class object may be provided with a list of
740
+ constraints, each being a tuple ('type', constraint), as a value for
741
+ parameter ``bootstrap_with``.
742
+
743
+ Currently, there are two types of constraints supported (to be
744
+ specified) in the constraints passed in: ``'linear'`` and ``'parity'``
745
+ (exclusive OR). The former will be handled as objects of class
746
+ :class:`LinearConstraint` while the latter will be transformed into
747
+ objects of :class:`ParityConstraint`.
748
+
749
+ Here, each type of constraint is meant to have a list of literals
750
+ stored in variable ``.lits``. This is required to set up watched lists
751
+ properly.
752
+
753
+ The second keyword argument ``adaptive`` (set to ``True`` by default)
754
+ denotes the fact that the engine should check its own efficiency and
755
+ disable or enable itself on the fly. This functionality is meant to
756
+ exemplify how adaptive external engines can be created. A user is
757
+ referred to the source code of the implementation for the details.
758
+ """
759
+
760
+ def __init__(self, bootstrap_with=[], adaptive=True):
761
+ """
762
+ Initialiser.
763
+ """
764
+
765
+ self.vars = [] # all known variables
766
+ self.vset = set() # a set of all variables
767
+ self.cons = [] # all known constraints
768
+ self.lins = [] # linear constraints
769
+ self.xors = [] # parity constraints
770
+ self.wlst = defaultdict(lambda: []) # constraints to watch
771
+
772
+ # this propagator may disable itself depending on the circumstances
773
+ self.is_adaptive = adaptive
774
+
775
+ # setting up individual constraint handlers
776
+ if bootstrap_with:
777
+ for cs in bootstrap_with:
778
+ cs = self._add_constraint(cs)
779
+
780
+ # updating the set of known variables
781
+ self.vset.update(set(abs(l) for l in cs.lits))
782
+
783
+ # setting all known variables
784
+ self.vars = sorted(self.vset)
785
+
786
+ # run the preprocessing techniques (if any)
787
+ self.preprocess()
788
+
789
+ # attaching new constraints after processing to the watched lists
790
+ for cs in self.cons:
791
+ cs.register_watched(self.wlst)
792
+
793
+ # value and trail handling
794
+ self.value = {v: None for v in self.vars}
795
+ self.fixed = {v: False for v in self.vars}
796
+ self.trail = []
797
+ self.trlim = []
798
+ self.props = defaultdict(lambda: [])
799
+ self.qhead = None
800
+
801
+ # decision level
802
+ self.decision_level = 0
803
+ self.level = {v: None for v in self.vars}
804
+
805
+ # reasons for propagated literals
806
+ self.origin = defaultdict(lambda: None)
807
+
808
+ # this variable points to the constraint previously violated by a
809
+ # given model offered by the solver
810
+ self.falsified = None
811
+
812
+ # giving constraints access to the values
813
+ for cs in self.cons:
814
+ cs.attach_values(self.value)
815
+
816
+ # currently, no solver is known to this propagator
817
+ # this will be update upon calling self.setup_observe()
818
+ self.solver = None
819
+
820
+ # the following constants are used in adaptive mode:
821
+ # the number of calls to propagate() between model checks and
822
+ # the number of propagated literals found between those checks
823
+ # followed by decaying propagation ratio-related constants
824
+ self.nprops, self.ncalls = 0, 0
825
+ self.pratio, self.pdecay, self.pbound = 0, 2, 0.2
826
+
827
+ # decaying ratio of falsifying assignments
828
+ # over all assignments offered by the solver
829
+ self.mratio, self.mdecay, self.mbound = 0, 2, 2
830
+
831
+ def adaptive_constants(self, pdecay, pbound, mdecay, mbound):
832
+ """
833
+ Set magic numeric constants used in adaptive mode.
834
+ """
835
+
836
+ self.pdecay = pdecay
837
+ self.pbound = pbound
838
+
839
+ self.mdecay = mdecay
840
+ self.mbound = mbound
841
+
842
+ def enable(self):
843
+ """
844
+ Notify the solver that the propagator is willing to become active
845
+ from now on.
846
+ """
847
+
848
+ assert self.solver, 'Solver is not connected!'
849
+ self.solver.enable_propagator()
850
+
851
+ def disable(self):
852
+ """
853
+ Notify the solver that the propagator should become inactive as it
854
+ does not contribute much to the inference process. From now on, it
855
+ will only be called to check complete models obtained by the
856
+ solver (see :meth:`check_model`).
857
+ """
858
+
859
+ assert self.solver, 'Solver is not connected!'
860
+ self.solver.disable_propagator()
861
+
862
+ def is_active(self):
863
+ """
864
+ Return engine's status. It is deemed active if the method returns
865
+ ``True`` and passive otherwise.
866
+ """
867
+
868
+ assert self.solver, 'Solver is not connected!'
869
+ return self.solver.propagator_active()
870
+
871
+ def add_constraint(self, constraint):
872
+ """
873
+ Add a new constraint to the engine and integrate it to the
874
+ internal structures, i.e. watched lists. Also, return the newly
875
+ added constraint to the callee.
876
+ """
877
+
878
+ cs = self._add_constraint(constraint)
879
+
880
+ # adding it to the watched lists
881
+ cs.register_watched(self.wlst)
882
+
883
+ # updating the other structures of the propagator
884
+ for lit in cs.lits:
885
+ var = abs(lit)
886
+ if var not in self.vset:
887
+ # this variable is currently unknown
888
+ self.vset.add(var)
889
+ self.value[var] = None
890
+ self.level[var] = None
891
+ self.fixed[var] = False
892
+
893
+ # letting the solver know we observe this variable
894
+ self.solver.observe(var)
895
+
896
+ # passing values to the constraint
897
+ cs.attach_values(self.value)
898
+
899
+ def _add_constraint(self, cs):
900
+ """
901
+ Create a new constraint, add to the list and return for further
902
+ processing.
903
+ """
904
+
905
+ if cs[0] == 'linear':
906
+ cs = LinearConstraint(lits=cs[1][0],
907
+ weights={} if len(cs[1]) == 2 else cs[1][2],
908
+ bound=cs[1][1])
909
+ self.lins.append(cs)
910
+ elif cs[0] == 'parity':
911
+ cs = ParityConstraint(lits=cs[1][0], value=cs[1][1])
912
+ self.xors.append(cs)
913
+ else:
914
+ assert 0, 'Unknown type of constraint'
915
+
916
+ # adding the newly created constraint
917
+ # to the list of known constraints
918
+ self.cons.append(cs)
919
+
920
+ return cs
921
+
922
+ def setup_observe(self, solver):
923
+ """
924
+ Inform the solver about all the variables the engine is interested
925
+ in. The solver will mark them as observed by the propagator.
926
+ """
927
+
928
+ # saving the solver for the future
929
+ self.solver = solver
930
+
931
+ for var in self.vars:
932
+ self.solver.observe(var)
933
+
934
+ def preprocess(self):
935
+ """
936
+ Run some (naive) preprocessing techniques if available for the
937
+ types of constraints under considerations. Each type of
938
+ constraints is handled separately of the rest of constraints.
939
+ """
940
+
941
+ if self.lins:
942
+ self.process_linear()
943
+
944
+ if self.xors:
945
+ self.process_parity()
946
+
947
+ def process_linear(self):
948
+ """
949
+ Process linear constraints. Here we apply simple pairwise
950
+ summation of constraints. As the number of result constraints is
951
+ quadratic, we stop the process as soon as we get 100 new
952
+ constraints. Also, if a result of the sum is longer than each of
953
+ the summands, the result constraint is ignored.
954
+
955
+ This is trivial procedure is made to illustrate how constraint
956
+ processing can be done. It can be made dependent on user-specified
957
+ parameters, e.g. the number of rounds or a numeric value
958
+ indicating when a pair of constraints should be added and when
959
+ they should not be added. For consideration in the future.
960
+ """
961
+
962
+ newc = []
963
+
964
+ for cs1, cs2 in itertools.combinations(self.lins, 2):
965
+ seen, wght, remd = set(), defaultdict(lambda: 0), 0
966
+
967
+ # traversing the first constraint and handling clashes
968
+ for l in cs1.lits:
969
+ if l in cs2.lset:
970
+ # the literal appears in both constraints
971
+ wght[l] = cs1.wght[l] + cs2.wght[l]
972
+ elif -l in cs2.lset:
973
+ # the literal appears in opposite phases
974
+ maxp = max([(l, cs1.wght[l]), (-l, cs2.wght[-l])], key=lambda pair: pair[1])
975
+ minp = min([(l, cs1.wght[l]), (-l, cs2.wght[-l])], key=lambda pair: pair[1])
976
+
977
+ if maxp[1] != minp[1]: # the weights aren't equal
978
+ wght[maxp[0]] = maxp[1] - minp[1]
979
+
980
+ # saving the remainder
981
+ remd += minp[1]
982
+ else:
983
+ # no clash
984
+ wght[l] = cs1.wght[l]
985
+
986
+ seen.add(abs(l))
987
+
988
+ # traversing the remainder of 2nd constraints; no clashes
989
+ for l in cs2.lits:
990
+ if abs(l) in seen:
991
+ continue
992
+ wght[l] = cs2.wght[l]
993
+
994
+ # right-hand side of the result constraint
995
+ rbnd = cs1.rbnd + cs2.rbnd - remd
996
+
997
+ if wght and rbnd >= 0:
998
+ # left-hand size of the result constraint
999
+ lits = sorted(wght.keys())
1000
+
1001
+ if len(lits) > len(cs1.lits) and len(lits) > len(cs2.lits):
1002
+ # the result constraint is not valuable -> ignore it
1003
+ continue
1004
+
1005
+ # all good, recording the constraint
1006
+ newc.append(['linear', [lits, rbnd, wght]])
1007
+
1008
+ if len(newc) == 100:
1009
+ break
1010
+
1011
+ elif rbnd < 0: # negative bound signifies unsatisfiability
1012
+ # stopping immediately
1013
+ newc = [('linear', [[cs1.lits[0]], 0]), ('linear', [[-cs1.lits[0]], 0])]
1014
+ break
1015
+
1016
+ if newc: # there are some new constraints to add
1017
+ for cs in newc:
1018
+ # adding constraint to the engine
1019
+ cs = self._add_constraint(cs)
1020
+
1021
+ # updating the set of known variables
1022
+ self.vset.update(set(abs(l) for l in cs.lits))
1023
+
1024
+ # updating all known variables
1025
+ self.vars = sorted(self.vset)
1026
+
1027
+ def process_parity(self):
1028
+ """
1029
+ Process parity/XOR constraints. Basically, this runs Gaussian
1030
+ elimination and see if anything can be derived from it.
1031
+ """
1032
+
1033
+ status = True
1034
+
1035
+ # removing None from all sets
1036
+ for cs in self.xors:
1037
+ cs.lset.remove(None)
1038
+
1039
+ # forward simplification
1040
+ for i, csi in enumerate(self.xors):
1041
+ v = csi.lits[0] # pivot variable
1042
+
1043
+ # going over all the other constraints and eliminating the pivot
1044
+ for j in range(i + 1, len(self.xors)):
1045
+ csj = self.xors[j]
1046
+
1047
+ if v not in csj.lset: # do nothing if pivot var is not present
1048
+ continue
1049
+
1050
+ # summing i'th and j'th constraints up
1051
+ csj.lset ^= csi.lset
1052
+ csj.rval ^= csi.rval
1053
+
1054
+ if not csj.lset and csj.rval:
1055
+ # inconsistency detected
1056
+ status = False
1057
+
1058
+ # adding two inconsistent level-0 literals: 1 and -1
1059
+ csi.lits, csj.lits = [1], [-1]
1060
+ csi.zlev, csj.zlev = 1, -1
1061
+
1062
+ break
1063
+
1064
+ # final result
1065
+ csj.lits = sorted(csj.lset)
1066
+
1067
+ if status:
1068
+ # back substitution if no inconsistency was detected
1069
+ vals = {}
1070
+ for i in range(len(self.xors) - 1, -1, -1):
1071
+ cs = self.xors[i]
1072
+
1073
+ lits = []
1074
+ for l in cs.lits:
1075
+ if l in vals:
1076
+ cs.rval ^= vals[l] # substitution
1077
+ else:
1078
+ lits.append(l)
1079
+ cs.lits = lits
1080
+
1081
+ if len(cs.lits) == 1: # checking if substitution propagates
1082
+ vals[cs.lits[0]] = cs.rval
1083
+ cs.zlev = cs.lits[0] * (2 * cs.rval - 1)
1084
+
1085
+ # updating the sets of literals and putting None back
1086
+ for cs in self.xors:
1087
+ cs.lset = set(cs.lits + [None])
1088
+
1089
+ def on_assignment(self, lit, fixed):
1090
+ """
1091
+ Update the propagator's state given a new assignment.
1092
+ """
1093
+
1094
+ self.level[abs(lit)] = self.decision_level
1095
+
1096
+ if self.qhead is None:
1097
+ # this is the first assignment we get after backtracking
1098
+ # marking that next propagation should start from here
1099
+ self.qhead = len(self.trail)
1100
+
1101
+ self.trail.append(lit)
1102
+
1103
+ if fixed:
1104
+ self.fixed[abs(lit)] = True
1105
+
1106
+ def on_new_level(self):
1107
+ """
1108
+ Keep track of decision level updates.
1109
+ """
1110
+
1111
+ self.decision_level += 1
1112
+ self.trlim.append(len(self.trail))
1113
+
1114
+ def on_backtrack(self, to):
1115
+ """
1116
+ Cancel all the decisions up to a certain level.
1117
+ """
1118
+
1119
+ # undoing the assignments
1120
+ while len(self.trlim) > to:
1121
+ while len(self.trail) > self.trlim[-1]:
1122
+ lit = self.trail.pop()
1123
+ var = abs(lit)
1124
+
1125
+ if self.value[var] is not None and not self.fixed[var]:
1126
+ # informing all the constraints
1127
+ for cs in self.wlst[lit]:
1128
+ cs.unassign(lit)
1129
+
1130
+ self.value[var] = None
1131
+
1132
+ self.level[var] = None
1133
+
1134
+ # cleaning all the consequences
1135
+ for l in self.props[lit]:
1136
+ self.origin[l].abandon(l)
1137
+ self.origin[l] = None
1138
+ self.props[lit] = []
1139
+
1140
+ self.trlim.pop()
1141
+
1142
+ # updating decision level
1143
+ self.decision_level = to
1144
+
1145
+ # updating queue head
1146
+ self.qhead = None
1147
+
1148
+ def check_model(self, model):
1149
+ """
1150
+ Check if a given model satisfies all the constraints.
1151
+ """
1152
+
1153
+ st = True # no falsified constraints by default
1154
+
1155
+ # checking all constraints one by one
1156
+ for cs in self.cons:
1157
+ if cs.falsified_by(model):
1158
+ self.falsified = cs
1159
+ st = False
1160
+ break
1161
+
1162
+ if self.is_adaptive:
1163
+ self.adaptive_update(st)
1164
+
1165
+ return st
1166
+
1167
+ def adaptive_update(self, satisfied):
1168
+ """
1169
+ Update adaptive mode: either enable or disable the engine. This
1170
+ depends on the statistics accumulated in the current run and
1171
+ whether or not the previous assignment found by the solver
1172
+ satisfied the constraints.
1173
+ """
1174
+
1175
+ # decaying increment
1176
+ self.pratio = self.pratio / self.pdecay + (self.nprops / self.ncalls if self.ncalls else 0)
1177
+
1178
+ # taking into account whether the current model falsified something
1179
+ self.mratio = self.mratio / self.mdecay + (0 if satisfied else 1)
1180
+
1181
+ if satisfied and self.is_active():
1182
+ # we are currently active and it does not pay off
1183
+ # in terms of propagation; should we slack off a bit?
1184
+ if self.pratio < self.pbound:
1185
+ self.disable()
1186
+ elif not satisfied and not self.is_active():
1187
+ # we are currently passive and there are many conflicts
1188
+ # let's put more effort into propagation to avoid them?
1189
+ if self.mratio > self.mbound:
1190
+ self.enable()
1191
+
1192
+ # resetting the moving average stats
1193
+ self.nprops = self.ncalls = 0
1194
+
1195
+ def propagate(self):
1196
+ """
1197
+ Run the propagator given the current assignment.
1198
+ """
1199
+
1200
+ result = []
1201
+
1202
+ if self.qhead is not None:
1203
+ while self.qhead < len(self.trail):
1204
+ lit = self.trail[self.qhead]
1205
+
1206
+ # if we have just propagated an opposite value,
1207
+ # we stop and expect conflict analysis to be performed
1208
+ if self.origin[-lit] is not None:
1209
+ break
1210
+
1211
+ self.value[abs(lit)] = lit # actually setting the value
1212
+
1213
+ watched_holes = []
1214
+ for csid, cs in enumerate(self.wlst[lit]):
1215
+ # checking the constraint
1216
+ propagated = cs.propagate(lit)
1217
+
1218
+ # extracting the reason
1219
+ for l in propagated:
1220
+ if self.origin[l] is None:
1221
+ # this literal is currently unknown
1222
+ self.origin[l] = cs
1223
+
1224
+ # augment the list of entailed literals to return
1225
+ result.append(l)
1226
+
1227
+ # recording all the dependencies
1228
+ self.props[lit].append(l)
1229
+
1230
+ # checking whether or not this cs is still watched
1231
+ if self.wlst[lit][csid] is None or type(self.wlst[lit][csid]) == int:
1232
+ # instead of cs, we have None or an integer, i.e. we
1233
+ # removed cs from the watched list for this literal
1234
+ watched_holes.append((csid, self.wlst[lit][csid]))
1235
+
1236
+ # cleaning up watches, should be more-or-less cheap,
1237
+ # doing so for +lit but also for -lit if it is affected
1238
+ if watched_holes:
1239
+ self.cleanup_watched(lit, watched_holes)
1240
+
1241
+ self.qhead += 1
1242
+
1243
+ if self.is_adaptive:
1244
+ # collecting the stats
1245
+ self.nprops += len(result)
1246
+ self.ncalls += 1
1247
+ else:
1248
+ # propagating without an assignment
1249
+ for cs in self.cons:
1250
+ propagated = cs.propagate()
1251
+
1252
+ # extracting the (empty) reason
1253
+ for l in propagated:
1254
+ if self.origin[l] is None:
1255
+ # this literal is currently unknown
1256
+ self.origin[l] = cs
1257
+
1258
+ # augment the list of entailed literals to return
1259
+ result.append(l)
1260
+
1261
+ return result
1262
+
1263
+ def cleanup_watched(self, lit, garbage):
1264
+ """
1265
+ Garbage collect holes in the watched list for +lit
1266
+ (and potentially for -lit).
1267
+ """
1268
+
1269
+ otherside = []
1270
+
1271
+ # going backwards, to make it work
1272
+ for i in range(len(garbage) - 1, -1, -1):
1273
+ pid, nid = garbage[i]
1274
+
1275
+ self.wlst[lit][pid] = self.wlst[lit][-1]
1276
+ self.wlst[lit].pop()
1277
+
1278
+ if nid is not None:
1279
+ # this one has a negative counterpart
1280
+ if pid < len(self.wlst[lit]):
1281
+ # and it's affected
1282
+ self.wlst[lit][pid].wopp[-lit] = pid
1283
+
1284
+ otherside.append(nid)
1285
+
1286
+ # taking care of the negative counterparts (if those are affected)
1287
+ for nid in sorted(otherside, reverse=True):
1288
+ self.wlst[-lit][nid] = self.wlst[-lit][-1]
1289
+ self.wlst[-lit].pop()
1290
+
1291
+ if nid < len(self.wlst[-lit]):
1292
+ self.wlst[-lit][nid].wopp[lit] = nid
1293
+
1294
+ def provide_reason(self, lit):
1295
+ """
1296
+ Return the reason clause for a given literal.
1297
+ """
1298
+
1299
+ return [lit] + self.origin[lit].justify(lit)
1300
+
1301
+ def has_clause(self):
1302
+ """
1303
+ Check whether the engine currently has an external clause to add.
1304
+ When queried separately by the solver, this lets
1305
+ :meth:`add_clause` return ``[]`` as an actual empty clause.
1306
+ """
1307
+
1308
+ return self.falsified is not None
1309
+
1310
+ def add_clause(self):
1311
+ """
1312
+ Extract the external clause previously announced by
1313
+ :meth:`has_clause`.
1314
+ """
1315
+
1316
+ # # old behaviour checking if we have a clause to add;
1317
+ # # commenting this out because we have has_clause() now
1318
+ # if self.falsified is None:
1319
+ # return []
1320
+
1321
+ to_add = self.falsified.explain_failure()
1322
+
1323
+ # cleaning the previous failure (currently being added)
1324
+ # technically, we should clean self.falsified.fmod too
1325
+ # but it should be safe enough not to do so
1326
+ self.falsified = None
1327
+
1328
+ return to_add