mpbn 3.5__py3-none-any.whl → 4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mpbn/__init__.py CHANGED
@@ -31,10 +31,6 @@ from colomoto import minibn
31
31
 
32
32
  from boolean import boolean
33
33
  import clingo
34
-
35
- from pyeda.boolalg import bdd
36
- import pyeda.boolalg.expr
37
- from pyeda.boolalg.expr import expr
38
34
  sys.setrecursionlimit(max(100000, sys.getrecursionlimit()))
39
35
 
40
36
  __asplibdir__ = os.path.realpath(os.path.join(os.path.dirname(__file__), "asplib"))
@@ -46,23 +42,20 @@ if hasattr(clingo, "version") and clingo.version() >= (5,5,0):
46
42
  def aspf(basename):
47
43
  return os.path.join(__asplibdir__, basename)
48
44
 
49
- def clingo_subsets(limit=0):
50
- s = clingo.Control(clingo_options)
45
+ def _clingo_domrec(mod, limit=0, project=False, extra_opts=[]):
46
+ s = clingo.Control(clingo_options + extra_opts)
51
47
  s.configuration.solve.models = limit
52
- s.configuration.solve.project = 1
48
+ if project:
49
+ s.configuration.solve.project = 1
53
50
  s.configuration.solve.enum_mode = "domRec"
54
51
  s.configuration.solver[0].heuristic = "Domain"
55
- s.configuration.solver[0].dom_mod = "5,16"
52
+ s.configuration.solver[0].dom_mod = f"{mod},{16 if project else 0}"
56
53
  return s
57
54
 
58
- def clingo_supsets(limit=0):
59
- s = clingo.Control(clingo_options)
60
- s.configuration.solve.models = limit
61
- s.configuration.solve.project = 1
62
- s.configuration.solve.enum_mode = "domRec"
63
- s.configuration.solver[0].heuristic = "Domain"
64
- s.configuration.solver[0].dom_mod = "3,16"
65
- return s
55
+ def clingo_subsets(**opts):
56
+ return _clingo_domrec(5, **opts)
57
+ def clingo_supsets(**opts):
58
+ return _clingo_domrec(3, **opts)
66
59
 
67
60
  def clingo_exists():
68
61
  s = clingo.Control(clingo_options)
@@ -81,7 +74,7 @@ def s2v(s):
81
74
  def v2s(v):
82
75
  return 1 if v > 0 else 0
83
76
 
84
- def is_unate(ba, f):
77
+ def is_dnf_unate(ba, f):
85
78
  pos_lits = set()
86
79
  neg_lits = set()
87
80
  def is_lit(f):
@@ -119,34 +112,6 @@ def is_unate(ba, f):
119
112
  return test_monotonicity()
120
113
  return False
121
114
 
122
- def asp_of_bdd(bid, b):
123
- _rules = dict()
124
- def register(node, nid=None):
125
- if node is bdd.BDDNODEONE:
126
- if nid is not None:
127
- _rules[bid] = f"bdd({clingo.String(nid)},1)"
128
- return 1
129
- elif node is bdd.BDDNODEZERO:
130
- if nid is not None:
131
- _rules[bid] = f"bdd({clingo.String(nid)},-1)"
132
- return -1
133
- nid = clingo.String(f"{bid}_n{id(node)}" if nid is None else nid)
134
- if nid not in _rules:
135
- var = clingo.String(bdd._VARS[node.root].qualname)
136
- lo = register(node.lo)
137
- hi = register(node.hi)
138
- a = f"bdd({nid},{var},{lo},{hi})"
139
- _rules[nid] = a
140
- return nid
141
- register(b.node, bid)
142
- return _rules.values()
143
-
144
- def bddasp_of_boolfunc(f, i):
145
- e = expr(str(f).replace("!","~"))
146
- b = bdd.expr2bdd(e)
147
- atoms = asp_of_bdd(i, b)
148
- return "\n".join((f"{a}." for a in atoms))
149
-
150
115
  def circuitasp_of_boolfunc(f, i, ba):
151
116
  atoms = []
152
117
  fid = clingo.String(i)
@@ -179,28 +144,9 @@ def circuitasp_of_boolfunc(f, i, ba):
179
144
  atoms.append(f"circuit({fid},root,{root}).\n")
180
145
  return "\n".join(atoms)
181
146
 
182
-
183
- def expr2bpy(ex, ba):
184
- """
185
- converts a pyeda Boolean expression into a boolean.py one
186
- """
187
- if isinstance(ex, pyeda.boolalg.expr.Variable):
188
- return ba.Symbol(str(ex))
189
- elif isinstance(ex, pyeda.boolalg.expr._One):
190
- return ba.TRUE
191
- elif isinstance(ex, pyeda.boolalg.expr._Zero):
192
- return ba.FALSE
193
- elif isinstance(ex, pyeda.boolalg.expr.Complement):
194
- return ba.NOT(ba.Symbol(str(ex.__invert__())))
195
- elif isinstance(ex, pyeda.boolalg.expr.NotOp):
196
- return ba.NOT(expr2bpy(ex.x, ba))
197
- elif isinstance(ex, pyeda.boolalg.expr.OrOp):
198
- return ba.OR(*(expr2bpy(x, ba) for x in ex.xs))
199
- elif isinstance(ex, pyeda.boolalg.expr.AndOp):
200
- return ba.AND(*(expr2bpy(x, ba) for x in ex.xs))
201
- raise NotImplementedError(str(ex), type(ex))
202
-
203
147
  DEFAULT_ENCODING = "mixed-dnf-bdd"
148
+ DEFAULT_BOOLFUNCLIB = os.environ.get("MPBN_BOOLFUNCLIB", "aeon")
149
+ SUPPORTED_BOOLFUNCLIBS = ["aeon", "pyeda"]
204
150
 
205
151
  class MPBooleanNetwork(minibn.BooleanNetwork):
206
152
  """
@@ -221,7 +167,8 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
221
167
  def __init__(self, bn=minibn.BooleanNetwork(), auto_dnf=True,
222
168
  simplify=False,
223
169
  try_unate_hard=False,
224
- encoding=DEFAULT_ENCODING):
170
+ encoding=DEFAULT_ENCODING,
171
+ boolfunclib=DEFAULT_BOOLFUNCLIB):
225
172
  """
226
173
  Constructor for :py:class:`.MPBoooleanNetwork`.
227
174
 
@@ -230,6 +177,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
230
177
  :py:class:`colomoto.minibn.BooleanNetwork` constructor
231
178
  :param bool auto_dnf: if ``False``, turns off automatic DNF
232
179
  transformation of local functions
180
+ :param str boolfunlib: library to use for Boolean function manipulation
181
+ among ``"aeon"`` (default) or ``"pyeda"``. Default can be overriden with
182
+ ``MPBN_BOOLFUNCLIB`` environment variable.
233
183
 
234
184
  Examples:
235
185
 
@@ -239,11 +189,21 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
239
189
  >>> mbn = MPBooleanNetwork(bn)
240
190
  """
241
191
  assert encoding in self.supported_encodings
192
+ assert boolfunclib in SUPPORTED_BOOLFUNCLIBS
242
193
  self.auto_dnf = auto_dnf and encoding in self.dnf_encodings
243
194
  self.encoding = encoding
244
195
  self.try_unate_hard = try_unate_hard
245
196
  self._simplify = simplify
246
197
  self._is_unate = dict()
198
+
199
+ self._boolfunclib = boolfunclib
200
+ __boolfunclib_symbols = (
201
+ "make_dnf_boolfunc",
202
+ "bddasp_of_boolfunc",
203
+ )
204
+ self._bf_impl = __import__(f"mpbn.boolfunclib.{boolfunclib}_impl",
205
+ fromlist=__boolfunclib_symbols)
206
+
247
207
  super(MPBooleanNetwork, self).__init__(bn)
248
208
 
249
209
  def __setitem__(self, a, f):
@@ -256,18 +216,12 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
256
216
  f = self.ba.parse(f)
257
217
  f = self._autobool(f)
258
218
  if self.auto_dnf:
259
- e = expr(str(f).replace("!","~"))
260
- e = e.to_dnf()
261
- if self._simplify is not None:
262
- e = e.simplify()
263
- f = expr2bpy(e, self.ba)
264
- if self.try_unate_hard:
265
- f = minibn.simplify_dnf(self.ba, f)
266
- elif self._simplify:
267
- f = f.simplify()
219
+ f = self._bf_impl.make_dnf_boolfunc(self.ba, f,
220
+ simplify=self._simplify,
221
+ try_unate_hard=self.try_unate_hard)
268
222
  a = self._autokey(a)
269
223
  if self.encoding in self.dnf_encodings:
270
- self._is_unate[a] = is_unate(self.ba, f)
224
+ self._is_unate[a] = is_dnf_unate(self.ba, f)
271
225
  if self.encoding == "unate-dnf":
272
226
  assert self._is_unate[a], f"'{f}' seems not unate. Try simplify()?"
273
227
  return super().__setitem__(a, f)
@@ -314,24 +268,33 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
314
268
  elif f_encoding == "dnf":
315
269
  facts.extend(encode_dnf(f))
316
270
  elif f_encoding == "bdd":
317
- facts.append(bddasp_of_boolfunc(f, n))
271
+ facts.append(self._bf_impl.bddasp_of_boolfunc(self.ba, f, n))
318
272
  elif f_encoding == "mixed-dnf-bdd":
319
273
  facts.extend(encode_dnf(f))
320
274
  if self._is_unate[n]:
321
275
  facts.append(f"unate(\"{n}\").")
322
276
  else:
323
- facts.append(bddasp_of_boolfunc(f, n))
277
+ facts.append(self._bf_impl.bddasp_of_boolfunc(self.ba, f, n))
324
278
  elif f_encoding == "circuit":
325
279
  facts.append(circuitasp_of_boolfunc(f, n, self.ba))
326
280
  return "".join(facts)
327
281
 
328
- def load_eval(self, solver):
282
+ def _file_eval(self):
329
283
  if self.encoding == "circuit":
330
- solver.load(aspf("eval_circuit.asp"))
284
+ f = aspf("eval_circuit.asp")
331
285
  elif self.encoding == "mixed-dnf-bdd":
332
- solver.load(aspf("eval_mixed.asp"))
286
+ f = aspf("eval_mixed.asp")
333
287
  else:
334
- solver.load(aspf("mp_eval.asp"))
288
+ f = aspf("mp_eval.asp")
289
+ return f
290
+
291
+ def rules_eval(self):
292
+ f = self._file_eval()
293
+ with open(f, "r") as fp:
294
+ return fp.read()
295
+ def load_eval(self, solver):
296
+ f = self._file_eval()
297
+ solver.load(f)
335
298
 
336
299
  def assert_pc_encoding(self):
337
300
  assert self.encoding not in self.nonpc_encodings, "Unsupported encoding"
@@ -340,8 +303,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
340
303
  facts = ["timepoint({},{}).".format(e,t)]
341
304
  facts += [" mp_state({},{},\"{}\",{}).".format(e,t,n,s2v(s))
342
305
  for (n,s) in c.items()]
343
- facts += [" 1 {{mp_state({},{},\"{}\",(-1;1))}} 1.".format(e,t,n)
344
- for n in self if n not in c]
306
+ facts += [f"1 {{mp_state({e},{t},N,(-1;1))}} 1 :- node(N)."]
307
+ #facts += [" 1 {{mp_state({},{},\"{}\",(-1;1))}} 1 :- node(N).".format(e,t,n)
308
+ # for n in self if n not in c]
345
309
  return "".join(facts)
346
310
 
347
311
  def reachability(self, x, y):
@@ -371,6 +335,32 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
371
335
  res = s.solve()
372
336
  return res.satisfiable
373
337
 
338
+ def _ground_rules(self, ctl, rules):
339
+ rules = "\n".join(rules)
340
+ ctl.add("base", [], rules)
341
+ ctl.ground([("base",[])])
342
+
343
+ def _fixedpoints(self, reachable_from=None, constraints={}, limit=0):
344
+ e = "fp"
345
+ t2 = "fp"
346
+ rules = [self.asp_of_cfg(e, t2, constraints)]
347
+ rules.append(f"mp_reach({e},{t2},N,V) :- mp_state({e},{t2},N,V).")
348
+ rules.append(f":- mp_state({e},{t2},N,V), mp_eval({e},{t2},N,-V).")
349
+ rules.append(self.asp_of_bn())
350
+ if reachable_from:
351
+ self.assert_pc_encoding()
352
+ t1 = "0"
353
+ rules.append(open(aspf("mp_positivereach-np.asp")).read())
354
+ rules.append(self.asp_of_cfg(e,t1,reachable_from))
355
+ rules.append("is_reachable({},{},{}).".format(e,t1,t2))
356
+ rules.append(f"#show. #show fixpoint(N,V) : mp_state({e},{t2},N,V).")
357
+ rules.append(open(aspf("mp_eval.asp")).read())
358
+
359
+ project = reachable_from and set(self.keys()).difference(reachable_from)
360
+ s = clingo_enum(limit=limit, project=project)
361
+ self._ground_rules(s, rules)
362
+ return s
363
+
374
364
  def fixedpoints(self, reachable_from=None, constraints={}, limit=0):
375
365
  """
376
366
  Iterator over fixed points of the MPBN (i.e., of f)
@@ -383,23 +373,8 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
383
373
  the given constraints.
384
374
  :param int limit: maximum number of solutions, ``0`` for unlimited.
385
375
  """
386
- s = clingo_enum(limit=limit)
387
- s.add("base", [], self.asp_of_bn())
388
- e = "fp"
389
- t2 = "fp"
390
- self.load_eval(s)
391
- s.add("base", [], self.asp_of_cfg(e, t2, constraints))
392
- s.add("base", [], f"mp_reach({e},{t2},N,V) :- mp_state({e},{t2},N,V).")
393
- s.add("base", [], f":- mp_state({e},{t2},N,V), mp_eval({e},{t2},N,-V).")
394
- if reachable_from:
395
- self.assert_pc_encoding()
396
- t1 = "0"
397
- s.load(aspf("mp_positivereach-np.asp"))
398
- s.add("base", [], self.asp_of_cfg(e,t1,reachable_from))
399
- s.add("base", [], "is_reachable({},{},{}).".format(e,t1,t2))
400
- s.add("base", [], f"#show. #show fixpoint(N,V) : mp_state({e},{t2},N,V).")
401
-
402
- s.ground([("base",[])])
376
+ s = self._fixedpoints(reachable_from=reachable_from,
377
+ constraints=constraints, limit=limit)
403
378
  for sol in s.solve(yield_=True):
404
379
  x = {n: None for n in self}
405
380
  data = sol.symbols(shown=True)
@@ -413,36 +388,59 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
413
388
  x[n] = v
414
389
  yield x
415
390
 
391
+ def count_fixedpoints(self, reachable_from=None, constraints={}, limit=0):
392
+ """
393
+ Returns number of fixed points
416
394
 
417
- def _trapspaces(self, reachable_from=None, subcube={}, limit=0, star="*",
395
+ :param dict[str,int] reachable_from: restrict to the attractors
396
+ reachable from the given configuration. Whenever partial, restrict
397
+ attractors to the one reachable by at least one matching
398
+ configuration.
399
+ :param dict[str,int] constraints: consider only attractors matching with
400
+ the given constraints.
401
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
402
+ """
403
+ s = self._fixedpoints(reachable_from=reachable_from,
404
+ constraints=constraints, limit=limit)
405
+ return sum((1 for _ in s.solve(yield_=True)))
406
+
407
+
408
+ def _trapspaces(self, reachable_from=None, subcube={}, limit=0,
418
409
  mode="min", exclude_full=False):
419
410
  self.assert_pc_encoding()
420
- solver = clingo_subsets if mode == "min" else clingo_supsets
421
- s = solver(limit=limit)
422
- self.load_eval(s)
423
- s.load(aspf("mp_attractor.asp"))
424
- s.add("base", [], self.asp_of_bn())
411
+
412
+ rules = []
413
+ rules.append(self.asp_of_bn())
414
+ rules.append(self.rules_eval())
415
+ rules.append(open(aspf("mp_attractor.asp")).read())
416
+ rules.append("#show attractor/2.")
417
+
425
418
  e = "__a"
426
419
  t2 = "final"
427
420
  if exclude_full and not subcube:
428
- s.add("base", [], f"{{ mp_reach({e},{t2},N,(-1;1)): node(N) }} {len(self)*2-1}.")
421
+ rules.append(f"{{ mp_reach({e},{t2},N,(-1;1)): node(N) }} {len(self)*2-1}.")
429
422
  if reachable_from:
430
423
  t1 = "0"
431
- s.load(aspf("mp_positivereach-np.asp"))
432
- s.add("base", [], self.asp_of_cfg(e,t1,reachable_from))
433
- s.add("base", [], "is_reachable({},{},{}).".format(e,t1,t2))
434
- s.add("base", [], "mp_state({},{},N,V) :- attractor(N,V).".format(e,t2))
424
+ rules.append(open(aspf("mp_positivereach-np.asp")).read())
425
+ rules.append(self.asp_of_cfg(e,t1,reachable_from))
426
+ rules.append("is_reachable({},{},{}).".format(e,t1,t2))
427
+ rules.append("mp_state({},{},N,V) :- attractor(N,V).".format(e,t2))
435
428
 
436
429
  for n, b in subcube.items():
437
430
  if isinstance(b, str):
438
431
  b = int(b)
439
432
  if b not in [0,1]:
440
433
  continue
441
- s.add("base", [], ":- mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(1-b)))
434
+ rules.append(":- mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(1-b)))
442
435
 
443
- s.add("base", [], "#show attractor/2.")
436
+ project = reachable_from and set(self.keys()).difference(reachable_from)
437
+ solver = clingo_subsets if mode == "min" else clingo_supsets
438
+ s = solver(limit=limit, project=project)
439
+ self._ground_rules(s, rules)
440
+ return s
444
441
 
445
- s.ground([("base",[])])
442
+ def _yield_trapspaces(self, *args, star="*", **kwargs):
443
+ s = self._trapspaces(*args, **kwargs)
446
444
  for sol in s.solve(yield_=True):
447
445
  attractor = {n: None for n in self}
448
446
  data = sol.symbols(shown=True)
@@ -465,6 +463,10 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
465
463
  attractor[n] = v
466
464
  yield attractor
467
465
 
466
+ def _count_trapspaces(self, *args, **kwargs):
467
+ s = self._trapspaces(*args, **kwargs)
468
+ return sum((1 for _ in s.solve(yield_=True)))
469
+
468
470
  def attractors(self, reachable_from=None, constraints={}, limit=0, star='*'):
469
471
  """
470
472
  Iterator over attractors of the MPBN (minimal trap spaces of the BN).
@@ -481,17 +483,49 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
481
483
  :param str star: value to use for components which are free in the
482
484
  attractor
483
485
  """
484
- return self._trapspaces(reachable_from=reachable_from,
486
+ return self._yield_trapspaces(reachable_from=reachable_from,
485
487
  subcube=constraints, limit=limit, star=star,
486
488
  mode="min")
487
-
488
489
  minimal_trapspaces = attractors
489
490
 
490
491
  def maximal_trapspaces(self, limit=0, subcube={}, star="*",
491
492
  exclude_full=True):
492
- return self._trapspaces(subcube=subcube, limit=limit, star=star,
493
+ return self._yield_trapspaces(subcube=subcube, limit=limit, star=star,
493
494
  mode="max", exclude_full=exclude_full)
494
495
 
496
+ def count_attractors(self, reachable_from=None, constraints={}, limit=0):
497
+ """
498
+ Returns number of attractors of the MPBN (minimal trap spaces of the BN).
499
+
500
+ :param dict[str,int] reachable_from: restrict to the attractors
501
+ reachable from the given configuration. Whenever partial, restrict
502
+ attractors to the one reachable by at least one matching
503
+ configuration.
504
+ :param dict[str,int] constraints: consider only attractors matching with
505
+ the given constraints.
506
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
507
+ """
508
+ return self._count_trapspaces(reachable_from=reachable_from,
509
+ subcube=constraints, limit=limit,
510
+ mode="min")
511
+ count_minimal_trapspaces = count_attractors
512
+
513
+ def count_maximal_trapspaces(self, reachable_from=None, constraints={}, limit=0):
514
+ """
515
+ Returns number of attractors of the MPBN (minimal trap spaces of the BN).
516
+
517
+ :param dict[str,int] reachable_from: restrict to the attractors
518
+ reachable from the given configuration. Whenever partial, restrict
519
+ attractors to the one reachable by at least one matching
520
+ configuration.
521
+ :param dict[str,int] constraints: consider only attractors matching with
522
+ the given constraints.
523
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
524
+ """
525
+ return self._count_trapspaces(reachable_from=reachable_from,
526
+ subcube=constraints, limit=limit,
527
+ mode="max")
528
+
495
529
  def has_cyclic_attractor(self):
496
530
  for a in self.attractors():
497
531
  if "*" in a.values():
File without changes
@@ -0,0 +1,202 @@
1
+ import networkx as nx
2
+ import clingo
3
+
4
+ from boolean import boolean
5
+ from colomoto import minibn
6
+
7
+ from typing import Optional
8
+
9
+ from biodivine_aeon import Bdd, BddPointer
10
+ from biodivine_aeon import BddVariableSet, BddValuation
11
+
12
+ def is_unate_symbolic(f: Bdd) -> boolean:
13
+ """
14
+ Returns `True` if the given `biodivine_aeon.Bdd` represents a unate function
15
+ (i.e. all arguments are locally monotonic).
16
+
17
+ The way this is handled is that we test for positive/negative monotonicity by
18
+ symbolically expressing the inputs where decreasing the input increases the
19
+ output (i.e. a counterexample to positive monotonicity), or vice versa.
20
+ """
21
+ variables = f.__ctx__().variable_ids()
22
+ f_false = f.l_not()
23
+ f_true = f
24
+ for var in f.support_set():
25
+ var_is_true = f.__ctx__().mk_literal(var, True)
26
+ var_is_false = f.__ctx__().mk_literal(var, False)
27
+
28
+ f_1_to_0 = f_false.l_and(var_is_true).r_exists(var)
29
+ f_0_to_1 = f_true.l_and(var_is_false).r_exists(var)
30
+ is_positive = f_0_to_1.l_and(f_1_to_0).r_exists(variables).l_not().is_true()
31
+
32
+ f_0_to_0 = f_false.l_and(var_is_false).r_exists(var)
33
+ f_1_to_1 = f_true.l_and(var_is_true).r_exists(var)
34
+ is_negative = f_0_to_0.l_and(f_1_to_1).r_exists(variables).l_not().is_true()
35
+
36
+ # An input cannot be both positive and negative at the same time.
37
+ assert not (is_positive and is_negative)
38
+
39
+ if (not is_positive) and (not is_negative):
40
+ return False
41
+ return True
42
+
43
+
44
+ def ba_to_bdd(ba: boolean.BooleanAlgebra, f: boolean.Expression, ctx: BddVariableSet | None = None) -> Bdd:
45
+ """
46
+ Takes a `boolean.Expression` (with the associated `boolean.BooleanAlgebra`) and
47
+ converts it to a `biodivine_aeon.Bdd`.
48
+
49
+ Note that the `Bdd` has an associated `biodivine_aeon.BddVariableSet` context, which maps the
50
+ variable IDs to names. You can provide your own context, or one will be created for you
51
+ (to access the underlying context object, use `bdd.__ctx__()`).
52
+ """
53
+ ba_vars = f.symbols
54
+ variables = sorted([ str(var) for var in ba_vars ])
55
+ if ctx is None:
56
+ ctx = BddVariableSet(variables)
57
+ else:
58
+ # Check that all variables that exist in `f` also exist in `ctx`.
59
+ assert all((ctx.find_variable(var) is not None) for var in variables)
60
+ def ba_to_bdd_rec(f: boolean.Expression) -> Bdd:
61
+ if type(f) is ba.TRUE or isinstance(f, minibn._TRUE):
62
+ return ctx.mk_const(True)
63
+ if type(f) is ba.FALSE or isinstance(f, minibn._FALSE):
64
+ return ctx.mk_const(False)
65
+ if type(f) is ba.Symbol:
66
+ return ctx.mk_literal(str(f.obj), True)
67
+ if type(f) is ba.NOT:
68
+ assert len(f.args) == 1, "Cannot transform NOT with more than one argument."
69
+ return ba_to_bdd_rec(f.args[0]).l_not()
70
+ if type(f) is ba.AND:
71
+ result = ctx.mk_const(True)
72
+ for arg in f.args:
73
+ result = result.l_and(ba_to_bdd_rec(arg))
74
+ return result
75
+ if type(f) is ba.OR:
76
+ result = ctx.mk_const(False)
77
+ for arg in f.args:
78
+ result = result.l_or(ba_to_bdd_rec(arg))
79
+ return result
80
+ raise NotImplementedError(str(f), type(f))
81
+
82
+ return ba_to_bdd_rec(f)
83
+
84
+ def bdd_to_dnf(ba: boolean.BooleanAlgebra, f: Bdd) -> boolean.Expression:
85
+ """
86
+ Convert a `biodivine_aeon.Bdd` to a `boolean.Expression` in disjunctive normal form.
87
+ """
88
+ if f.is_true():
89
+ return ba.TRUE
90
+ if f.is_false():
91
+ return ba.FALSE
92
+ ctx = f.__ctx__()
93
+ # Technically, `optimize=True` should be set by default, but just in case.
94
+ dnf = f.to_dnf(optimize=True)
95
+ # Maps BDD variables to BooleanAlgebra Symbols.
96
+ var_to_symbol = { var: ba.Symbol(ctx.get_variable_name(var)) for var in ctx.variable_ids() }
97
+ ba_clauses = []
98
+ for clause in dnf:
99
+ literals = []
100
+ for (var, value) in clause.items():
101
+ if value:
102
+ literals.append(var_to_symbol[var])
103
+ else:
104
+ literals.append(ba.NOT(var_to_symbol[var]))
105
+ assert len(literals) > 0
106
+ if len(literals) == 1:
107
+ ba_clauses.append(literals[0])
108
+ else:
109
+ ba_clauses.append(ba.AND(*literals))
110
+ assert len(ba_clauses) > 0
111
+ if len(ba_clauses) == 1:
112
+ return ba_clauses[0]
113
+ else:
114
+ return ba.OR(*ba_clauses)
115
+
116
+ def make_dnf_boolfunc(ba, f, **unused_opts):
117
+ bdd = ba_to_bdd(ba, f)
118
+ return bdd_to_dnf(ba, bdd)
119
+
120
+ def asp_of_bdd(var_name, bdd: Bdd) -> list[str]:
121
+ """
122
+ Convert a `biodivine_aeon.Bdd` into a list of `clingo` atoms
123
+ representing the individual BDD nodes.
124
+ """
125
+ if bdd.is_false():
126
+ return [f"bdd({clingo.String(var_name)},-1)"]
127
+ if bdd.is_true():
128
+ return [f"bdd({clingo.String(var_name)},1)"]
129
+
130
+ _rules = {}
131
+ def _rec(node: BddPointer, node_name: Optional[str] = None) -> str:
132
+ if node.is_zero():
133
+ return "-1"
134
+ if node.is_one():
135
+ return "1"
136
+ if node_name is None:
137
+ node_name = f"{var_name}_n{int(node)}"
138
+ node_name_clingo = clingo.String(node_name)
139
+ if node_name_clingo in _rules:
140
+ # The node was already declared.
141
+ return node_name_clingo
142
+ node_var = bdd.node_variable(node)
143
+ assert node_var is not None # Only `None` if node is terminal.
144
+ (lo, hi) = bdd.node_links(node)
145
+ var = clingo.String(bdd.__ctx__().get_variable_name(node_var))
146
+ lo = _rec(lo)
147
+ hi = _rec(hi)
148
+ atom = f"bdd({node_name_clingo},{var},{lo},{hi})"
149
+ _rules[node_name_clingo] = atom
150
+ return node_name_clingo
151
+ _rec(bdd.root(), var_name)
152
+
153
+ return list(_rules.values())
154
+
155
+ def bddasp_of_boolfunc(ba, f, var_name):
156
+ f_bdd = ba_to_bdd(ba, f)
157
+ atoms = asp_of_bdd(var_name, f_bdd)
158
+ return "\n".join((f"{a}." for a in atoms))
159
+
160
+ def bn_of_asynchronous_transition_graph(adyn, names,
161
+ parse_node=(lambda n: tuple(map(int, n))),
162
+ bn_class=minibn.BooleanNetwork,
163
+ simplify=True):
164
+ """
165
+ Convert the transition graph of a (fully) asynchronous Boolean network to
166
+ a propositional logic representation.
167
+
168
+ The object `adyn` must be an instance of `networkx.DiGraph`.
169
+ The `parse_node` function must return a tuple of 0 and 1 from an `adyn`
170
+ node. By default, it is assumed that nodes are strings of binary values.
171
+ Returned object will be of `bn_class`, instantiated with a dictionnary
172
+ mapping component names to a string representation of their Boolean expression.
173
+ """
174
+ relabel = {label: parse_node(label) for label in adyn.nodes()}
175
+ adyn = nx.relabel_nodes(adyn, relabel)
176
+ n = len(next(iter(adyn.nodes)))
177
+ assert n == len(names), "list of component names and dimension of configuraitons seem different"
178
+ assert adyn.number_of_nodes() == 2**n, "unexpected number of nodes in the transition graph"
179
+
180
+ bdd_ctx = BddVariableSet(names)
181
+
182
+ f = []
183
+ for i in range(n):
184
+ pos = []
185
+ for x in adyn.nodes():
186
+ dx = list(x)
187
+ dx[i] = 1-x[i]
188
+ y = dx if tuple(dx) in adyn[x] else x
189
+ target = y[i]
190
+ if target:
191
+ pos.append(BddValuation(bdd_ctx, list(x)))
192
+ if len(pos) == 0:
193
+ f.append(bdd_ctx.mk_false())
194
+ else:
195
+ f.append(bdd_ctx.mk_dnf(pos))
196
+
197
+ bn = bn_class()
198
+ for (i, name) in enumerate(names):
199
+ bn[name] = bdd_to_dnf(bn.ba, f[i])
200
+ if simplify:
201
+ bn = bn.simplify()
202
+ return bn
@@ -0,0 +1,143 @@
1
+ import clingo
2
+ import networkx as nx
3
+
4
+ from pyeda.boolalg.minimization import *
5
+ import pyeda.boolalg.expr
6
+ from pyeda.inter import expr
7
+ from pyeda.boolalg import bdd
8
+
9
+ from colomoto import minibn
10
+
11
+ def expr2str(ex):
12
+ """
13
+ converts a pyeda Boolean expression to string representation
14
+ """
15
+ def _protect(e):
16
+ if isinstance(e, (pyeda.boolalg.expr.OrOp, pyeda.boolalg.expr.AndOp)):
17
+ return f"({expr2str(e)})"
18
+ return expr2str(e)
19
+ if isinstance(ex, pyeda.boolalg.expr.Variable):
20
+ return str(ex)
21
+ elif isinstance(ex, pyeda.boolalg.expr._One):
22
+ return "1"
23
+ elif isinstance(ex, pyeda.boolalg.expr._Zero):
24
+ return "0"
25
+ elif isinstance(ex, pyeda.boolalg.expr.Complement):
26
+ return f"!{_protect(ex.__invert__())}"
27
+ elif isinstance(ex, pyeda.boolalg.expr.NotOp):
28
+ return f"!{_protect(ex.x)}"
29
+ elif isinstance(ex, pyeda.boolalg.expr.OrOp):
30
+ return " | ".join(map(_protect, ex.xs))
31
+ elif isinstance(ex, pyeda.boolalg.expr.AndOp):
32
+ return " & ".join(map(_protect, ex.xs))
33
+ raise NotImplementedError(str(ex), type(ex))
34
+
35
+ def expr2bpy(ex, ba):
36
+ """
37
+ converts a pyeda Boolean expression into a boolean.py one
38
+ """
39
+ if isinstance(ex, pyeda.boolalg.expr.Variable):
40
+ return ba.Symbol(str(ex))
41
+ elif isinstance(ex, pyeda.boolalg.expr._One):
42
+ return ba.TRUE
43
+ elif isinstance(ex, pyeda.boolalg.expr._Zero):
44
+ return ba.FALSE
45
+ elif isinstance(ex, pyeda.boolalg.expr.Complement):
46
+ return ba.NOT(ba.Symbol(str(ex.__invert__())))
47
+ elif isinstance(ex, pyeda.boolalg.expr.NotOp):
48
+ return ba.NOT(expr2bpy(ex.x, ba))
49
+ elif isinstance(ex, pyeda.boolalg.expr.OrOp):
50
+ return ba.OR(*(expr2bpy(x, ba) for x in ex.xs))
51
+ elif isinstance(ex, pyeda.boolalg.expr.AndOp):
52
+ return ba.AND(*(expr2bpy(x, ba) for x in ex.xs))
53
+ raise NotImplementedError(str(ex), type(ex))
54
+
55
+ def asp_of_bdd(bid, b):
56
+ _rules = dict()
57
+ def register(node, nid=None):
58
+ if node is bdd.BDDNODEONE:
59
+ if nid is not None:
60
+ _rules[bid] = f"bdd({clingo.String(nid)},1)"
61
+ return 1
62
+ elif node is bdd.BDDNODEZERO:
63
+ if nid is not None:
64
+ _rules[bid] = f"bdd({clingo.String(nid)},-1)"
65
+ return -1
66
+ nid = clingo.String(f"{bid}_n{id(node)}" if nid is None else nid)
67
+ if nid not in _rules:
68
+ var = clingo.String(bdd._VARS[node.root].qualname)
69
+ lo = register(node.lo)
70
+ hi = register(node.hi)
71
+ a = f"bdd({nid},{var},{lo},{hi})"
72
+ _rules[nid] = a
73
+ return nid
74
+ register(b.node, bid)
75
+ return _rules.values()
76
+
77
+ def bddasp_of_boolfunc(ba, f, i):
78
+ e = expr(str(f).replace("!","~"))
79
+ b = bdd.expr2bdd(e)
80
+ atoms = asp_of_bdd(i, b)
81
+ return "\n".join((f"{a}." for a in atoms))
82
+
83
+ def make_dnf_boolfunc(ba, f, try_unate_hard=False, simplify=True):
84
+ """
85
+ try_unate_hard: use costly CNF/DNF transformations
86
+ simplify: use boolean.py simplification method
87
+ """
88
+ e = expr(str(f).replace("!","~"))
89
+ e = e.to_dnf()
90
+ e = e.simplify()
91
+ e = expr2bpy(e, ba)
92
+ if try_unate_hard:
93
+ e = minibn.simplify_dnf(self.ba, e)
94
+ elif simplify:
95
+ e = e.simplify()
96
+ return e
97
+
98
+ def bn_of_asynchronous_transition_graph(adyn, names,
99
+ parse_node=(lambda n: tuple(map(int, n))),
100
+ bn_class=minibn.BooleanNetwork,
101
+ simplify=True):
102
+ """
103
+ Convert the transition graph of a (fully) asynchronous Boolean network to
104
+ a propositional logic representation.
105
+
106
+ The object `adyn` must be an instance of `networkx.DiGraph`.
107
+ The `parse_node` function must return a tuple of 0 and 1 from an `adyn`
108
+ node. By default, it is assumed that nodes are strings of binary values.
109
+ Returned object will be of `bn_class`, instantiated with a dictionnary
110
+ mapping component names to a string representation of their Boolean expression.
111
+ """
112
+ relabel = {label: parse_node(label) for label in adyn.nodes()}
113
+ adyn = nx.relabel_nodes(adyn, relabel)
114
+ n = len(next(iter(adyn.nodes)))
115
+ assert n == len(names), "list of component names and dimension of configuraitons seem different"
116
+ assert adyn.number_of_nodes() == 2**n, "unexpected number of nodes in the transition graph"
117
+
118
+ def expr_of_cfg(x):
119
+ e = "&".join(f"{'~' if not v else ''}{names[i]}" for i, v in enumerate(x))
120
+ return f"({e})"
121
+
122
+ f = []
123
+ for i in range(n):
124
+ pos = []
125
+ for x in adyn.nodes():
126
+ dx = list(x)
127
+ dx[i] = 1-x[i]
128
+ y = dx if tuple(dx) in adyn[x] else x
129
+ target = y[i]
130
+ if target:
131
+ pos.append(x)
132
+ if not pos:
133
+ f.append(expr("0"))
134
+ else:
135
+ e = expr("|".join(map(expr_of_cfg,pos)))
136
+ e, = espresso_exprs(e.to_dnf())
137
+ f.append(e)
138
+ f = map(expr2str, f)
139
+ f = bn_class(dict(zip(names, f)))
140
+ if simplify:
141
+ f = f.simplify()
142
+ return f
143
+
mpbn/cli/__init__.py CHANGED
@@ -1,10 +1,14 @@
1
1
 
2
2
  import mpbn
3
3
 
4
+ import os
4
5
  import sys
5
6
  from argparse import ArgumentParser
6
7
 
7
8
  def main():
9
+ if "CLINGO_OPTS" in os.environ:
10
+ mpbn.clingo_options += os.environ["CLINGO_OPTS"].split(" ")
11
+
8
12
  ap = ArgumentParser(prog=sys.argv[0])
9
13
  ap.add_argument("bnet_file")
10
14
  ap.add_argument("method", choices=["attractors", "fixedpoints", "bn2asp"])
@@ -13,19 +17,30 @@ def main():
13
17
  ap.add_argument("--encoding", default=mpbn.DEFAULT_ENCODING,
14
18
  choices=mpbn.MPBooleanNetwork.supported_encodings,
15
19
  help=f"Encoding method (default: {mpbn.DEFAULT_ENCODING})")
20
+ ap.add_argument("--boolfunclib", default="aeon",
21
+ choices=mpbn.SUPPORTED_BOOLFUNCLIBS,
22
+ help=f"Backend lib for Boolean functions (default: {mpbn.DEFAULT_BOOLFUNCLIB})")
23
+ ap.add_argument("--input-is-dnf", action="store_true", default=False,
24
+ help="Functions are already in DNF form")
16
25
  ap.add_argument("--simplify", action="store_true", default=False,
17
26
  help="Try costly Boolean function simplifications to improve encoding")
18
27
  ap.add_argument("--try-unate-hard", action="store_true", default=False,
19
28
  help="Try even more costly Boolean function simplifications")
29
+ ap.add_argument("--count", action="store_true",
30
+ help="Returns only the number of solutions")
20
31
  args = ap.parse_args()
21
32
  mbn = mpbn.MPBooleanNetwork(args.bnet_file, encoding=args.encoding,
33
+ boolfunclib=args.boolfunclib,
34
+ auto_dnf=not args.input_is_dnf,
22
35
  simplify=args.simplify,
23
36
  try_unate_hard=args.try_unate_hard)
24
- if args.method == "attractors":
25
- for attractor in mbn.attractors(limit=args.limit):
26
- print(attractor)
27
- elif args.method == "fixedpoints":
28
- for attractor in mbn.fixedpoints(limit=args.limit):
29
- print(attractor)
37
+ if args.method in ["attractors", "fixedpoints"]:
38
+ if args.count:
39
+ func = getattr(mbn, f"count_{args.method}")
40
+ print(func(limit=args.limit))
41
+ else:
42
+ func = getattr(mbn, args.method)
43
+ for obj in func(limit=args.limit):
44
+ print(obj)
30
45
  elif args.method == "bn2asp":
31
46
  print(mbn.asp_of_bn())
mpbn/converters.py CHANGED
@@ -1,80 +1,7 @@
1
- import networkx as nx
2
- from pyeda.boolalg.minimization import *
3
- import pyeda.boolalg.expr
4
- from pyeda.inter import expr
5
-
6
- from colomoto import minibn
7
-
8
- def expr2str(ex):
9
- """
10
- converts a pyeda Boolean expression to string representation
11
- """
12
- def _protect(e):
13
- if isinstance(e, (pyeda.boolalg.expr.OrOp, pyeda.boolalg.expr.AndOp)):
14
- return f"({expr2str(e)})"
15
- return expr2str(e)
16
- if isinstance(ex, pyeda.boolalg.expr.Variable):
17
- return str(ex)
18
- elif isinstance(ex, pyeda.boolalg.expr._One):
19
- return "1"
20
- elif isinstance(ex, pyeda.boolalg.expr._Zero):
21
- return "0"
22
- elif isinstance(ex, pyeda.boolalg.expr.Complement):
23
- return f"!{_protect(ex.__invert__())}"
24
- elif isinstance(ex, pyeda.boolalg.expr.NotOp):
25
- return f"!{_protect(ex.x)}"
26
- elif isinstance(ex, pyeda.boolalg.expr.OrOp):
27
- return " | ".join(map(_protect, ex.xs))
28
- elif isinstance(ex, pyeda.boolalg.expr.AndOp):
29
- return " & ".join(map(_protect, ex.xs))
30
- raise NotImplementedError(str(ex), type(ex))
31
-
32
-
33
- def bn_of_asynchronous_transition_graph(adyn, names,
34
- parse_node=(lambda n: tuple(map(int, n))),
35
- bn_class=minibn.BooleanNetwork,
36
- simplify=True):
37
- """
38
- Convert the transition graph of a (fully) asynchronous Boolean network to
39
- a propositional logic representation.
40
-
41
- The object `adyn` must be an instance of `networkx.DiGraph`.
42
- The `parse_node` function must return a tuple of 0 and 1 from an `adyn`
43
- node. By default, it is assumed that nodes are strings of binary values.
44
- Returned object will be of `bn_class`, instantiated with a dictionnary
45
- mapping component names to a string representation of their Boolean expression.
46
- """
47
- relabel = {label: parse_node(label) for label in adyn.nodes()}
48
- adyn = nx.relabel_nodes(adyn, relabel)
49
- n = len(next(iter(adyn.nodes)))
50
- assert n == len(names), "list of component names and dimension of configuraitons seem different"
51
- assert adyn.number_of_nodes() == 2**n, "unexpected number of nodes in the transition graph"
52
-
53
- def expr_of_cfg(x):
54
- e = "&".join(f"{'~' if not v else ''}{names[i]}" for i, v in enumerate(x))
55
- return f"({e})"
56
-
57
- f = []
58
- for i in range(n):
59
- pos = []
60
- for x in adyn.nodes():
61
- dx = list(x)
62
- dx[i] = 1-x[i]
63
- y = dx if tuple(dx) in adyn[x] else x
64
- target = y[i]
65
- if target:
66
- pos.append(x)
67
- if not pos:
68
- f.append(expr("0"))
69
- else:
70
- e = expr("|".join(map(expr_of_cfg,pos)))
71
- e, = espresso_exprs(e.to_dnf())
72
- f.append(e)
73
- f = map(expr2str, f)
74
- f = bn_class(dict(zip(names, f)))
75
- if simplify:
76
- f = f.simplify()
77
- return f
1
+ try:
2
+ from mpbn.boolfunclib.aeon_impl import bn_of_asynchronous_transition_graph
3
+ except ImportError:
4
+ from mpbn.boolfunclib.pyeda_impl import bn_of_asynchronous_transition_graph
78
5
 
79
6
  if __name__ == "__main__":
80
7
  import mpbn
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: mpbn
3
- Version: 3.5
3
+ Version: 4.0
4
4
  Summary: Simple implementation of Most Permissive Boolean networks
5
5
  Home-page: https://github.com/bnediction/mpbn
6
6
  Author: Loïc Paulevé
@@ -13,11 +13,21 @@ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
13
  Description-Content-Type: text/markdown
14
14
  Requires-Dist: boolean.py
15
15
  Requires-Dist: clingo
16
- Requires-Dist: colomoto-jupyter >=0.8.0
16
+ Requires-Dist: colomoto_jupyter>=0.8.0
17
17
  Requires-Dist: numpy
18
- Requires-Dist: pyeda
18
+ Requires-Dist: biodivine_aeon>=1.0.1
19
19
  Requires-Dist: scipy
20
20
  Requires-Dist: tqdm
21
+ Dynamic: author
22
+ Dynamic: author-email
23
+ Dynamic: classifier
24
+ Dynamic: description
25
+ Dynamic: description-content-type
26
+ Dynamic: home-page
27
+ Dynamic: keywords
28
+ Dynamic: license
29
+ Dynamic: requires-dist
30
+ Dynamic: summary
21
31
 
22
32
 
23
33
  The `mpbn` Python module offers a simple implementation of reachability and attractor analysis (minimal trap spaces) in *Most Permissive Boolean Networks* ([doi:10.1038/s41467-020-18112-5](https://doi.org/10.1038/s41467-020-18112-5)). The `mpbn` Python module also offers a *Most Permissive* simulator, which provides trajectory sampling and computes attractor propensities (see paper [Variable-Depth Simulation of Most Permissive Boolean Networks](https://link.springer.com/chapter/10.1007/978-3-031-15034-0_7) for more details).
@@ -38,7 +48,7 @@ pip install mpbn
38
48
 
39
49
  ### Using conda
40
50
  ```
41
- conda install -c colomoto -c potassco mpbn
51
+ conda install -c colomoto -c potassco -c daemontus mpbn
42
52
  ```
43
53
 
44
54
  ## Usage
@@ -1,12 +1,15 @@
1
- mpbn/__init__.py,sha256=oPey2_tk0ymBGwiL9dcG88Hk-vUuWH-gpmP7ZDwXyu4,20578
2
- mpbn/converters.py,sha256=33mhsGnFkJL4PlqYMIKu3zNb5oCNT6ZLaK6xoj9WsI4,3034
1
+ mpbn/__init__.py,sha256=kbHB9JR5z2XP3FNaf2F06X9TaJM3PVVHzAsltT0_awg,22903
2
+ mpbn/converters.py,sha256=6uCjxaJ2MI19_I1h0Pk9YN6PcDx42_C-Ml8ZRMA9uxg,416
3
3
  mpbn/simulation.py,sha256=3lk6yu7k2POrSytmtnsGIh596B-qcAgBtl16iZ0vzM4,10594
4
4
  mpbn/asplib/eval_circuit.asp,sha256=5rIbkVmvobaQwE_gHr0_USccPuI0QUPuzfw_1011hSE,989
5
5
  mpbn/asplib/eval_mixed.asp,sha256=n23pbjtxuWSCerL36Gb6O32bx8_1ukc52l1hQMMs8OE,787
6
6
  mpbn/asplib/mp_attractor.asp,sha256=CawLNxlYBZG2cl8XQAxA6EXEZ89Y9iw20CZ73n97Nu8,185
7
7
  mpbn/asplib/mp_eval.asp,sha256=PNa1APcKS9bgtCB3BtAdxq0mkg3p_x3G6fWSF3uAgzw,769
8
8
  mpbn/asplib/mp_positivereach-np.asp,sha256=SxLzZMszZxy_J14WTO7Me4aXldzxhqnL-ihqcnHCb8I,453
9
- mpbn/cli/__init__.py,sha256=OR7EO_BbDpkbBqNq1gZPgxrvTlEiMOgG1LZOyhiulGk,1388
9
+ mpbn/boolfunclib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
+ mpbn/boolfunclib/aeon_impl.py,sha256=X41J1tIeKIHomOpL9zrTzYaW12dkE79t_Wp9VXrP7XA,7660
11
+ mpbn/boolfunclib/pyeda_impl.py,sha256=NkgE46MKrSOVInAF5W0QnTbAjVMtLM8ox05oJUMtPHo,5003
12
+ mpbn/cli/__init__.py,sha256=v5C9U2YLR-J02JV-ciZkZoCvEcx7emgEy402mv4CdAg,2124
10
13
  mpbn/cli/sim.py,sha256=CHsbE4qp1L0iXyGLL1_zPontFjHL58qlt1_QVSWna68,6086
11
14
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
15
  tests/test_attractors.py,sha256=0ga4j0T6db6ljXlAs5jJG0BLB5xZchSdlhwbC-4lj8w,255
@@ -15,8 +18,8 @@ tests/test_fixpoints.py,sha256=bdDttzimiby48nVkVL-HXZ2rBldICBRbIZrVVbTGfso,786
15
18
  tests/test_input.py,sha256=mwMKd5UwAIY4Z1uZjYr09Ue8DLxD52CiPSoE-iXytfo,337
16
19
  tests/test_reachability.py,sha256=X7anTwFSIYcV2rltJBOW8TcAJnrt3SjYLqttB0eIL_Q,588
17
20
  tests/test_reachable_attractors.py,sha256=h78kvgmx9rTJWi3r2DZe_abYDxr02MLJ2iLDJUdLESY,540
18
- mpbn-3.5.dist-info/METADATA,sha256=I6viVUknNQ532Vya9eUX-6u_QuwxrQx-w59njI7srYQ,2129
19
- mpbn-3.5.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
20
- mpbn-3.5.dist-info/entry_points.txt,sha256=CpzAc9SkB-mH_dojzt1N3YgDxy8hniDrIGzSHcPDo8g,68
21
- mpbn-3.5.dist-info/top_level.txt,sha256=oe3jlFHbQ6oIXyE1q7yAAnf1m49oP_jBPUU05d71n74,11
22
- mpbn-3.5.dist-info/RECORD,,
21
+ mpbn-4.0.dist-info/METADATA,sha256=DJlh3LPEMO30F7gkqQMPlfCfBrrSuQBmG-1ty-Chlew,2364
22
+ mpbn-4.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
23
+ mpbn-4.0.dist-info/entry_points.txt,sha256=CpzAc9SkB-mH_dojzt1N3YgDxy8hniDrIGzSHcPDo8g,68
24
+ mpbn-4.0.dist-info/top_level.txt,sha256=oe3jlFHbQ6oIXyE1q7yAAnf1m49oP_jBPUU05d71n74,11
25
+ mpbn-4.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: bdist_wheel (0.43.0)
2
+ Generator: setuptools (75.8.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
File without changes