mpbn 3.4__tar.gz → 3.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mpbn
3
- Version: 3.4
3
+ Version: 3.8
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é
@@ -46,23 +46,20 @@ if hasattr(clingo, "version") and clingo.version() >= (5,5,0):
46
46
  def aspf(basename):
47
47
  return os.path.join(__asplibdir__, basename)
48
48
 
49
- def clingo_subsets(limit=0):
50
- s = clingo.Control(clingo_options)
49
+ def _clingo_domrec(mod, limit=0, project=False, extra_opts=[]):
50
+ s = clingo.Control(clingo_options + extra_opts)
51
51
  s.configuration.solve.models = limit
52
- s.configuration.solve.project = 1
52
+ if project:
53
+ s.configuration.solve.project = 1
53
54
  s.configuration.solve.enum_mode = "domRec"
54
55
  s.configuration.solver[0].heuristic = "Domain"
55
- s.configuration.solver[0].dom_mod = "5,16"
56
+ s.configuration.solver[0].dom_mod = f"{mod},{16 if project else 0}"
56
57
  return s
57
58
 
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
59
+ def clingo_subsets(**opts):
60
+ return _clingo_domrec(5, **opts)
61
+ def clingo_supsets(**opts):
62
+ return _clingo_domrec(3, **opts)
66
63
 
67
64
  def clingo_exists():
68
65
  s = clingo.Control(clingo_options)
@@ -325,13 +322,22 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
325
322
  facts.append(circuitasp_of_boolfunc(f, n, self.ba))
326
323
  return "".join(facts)
327
324
 
328
- def load_eval(self, solver):
325
+ def _file_eval(self):
329
326
  if self.encoding == "circuit":
330
- solver.load(aspf("eval_circuit.asp"))
327
+ f = aspf("eval_circuit.asp")
331
328
  elif self.encoding == "mixed-dnf-bdd":
332
- solver.load(aspf("eval_mixed.asp"))
329
+ f = aspf("eval_mixed.asp")
333
330
  else:
334
- solver.load(aspf("mp_eval.asp"))
331
+ f = aspf("mp_eval.asp")
332
+ return f
333
+
334
+ def rules_eval(self):
335
+ f = self._file_eval()
336
+ with open(f, "r") as fp:
337
+ return fp.read()
338
+ def load_eval(self, solver):
339
+ f = self._file_eval()
340
+ solver.load(f)
335
341
 
336
342
  def assert_pc_encoding(self):
337
343
  assert self.encoding not in self.nonpc_encodings, "Unsupported encoding"
@@ -340,8 +346,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
340
346
  facts = ["timepoint({},{}).".format(e,t)]
341
347
  facts += [" mp_state({},{},\"{}\",{}).".format(e,t,n,s2v(s))
342
348
  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]
349
+ facts += [f"1 {{mp_state({e},{t},N,(-1;1))}} 1 :- node(N)."]
350
+ #facts += [" 1 {{mp_state({},{},\"{}\",(-1;1))}} 1 :- node(N).".format(e,t,n)
351
+ # for n in self if n not in c]
345
352
  return "".join(facts)
346
353
 
347
354
  def reachability(self, x, y):
@@ -371,6 +378,32 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
371
378
  res = s.solve()
372
379
  return res.satisfiable
373
380
 
381
+ def _ground_rules(self, ctl, rules):
382
+ rules = "\n".join(rules)
383
+ ctl.add("base", [], rules)
384
+ ctl.ground([("base",[])])
385
+
386
+ def _fixedpoints(self, reachable_from=None, constraints={}, limit=0):
387
+ e = "fp"
388
+ t2 = "fp"
389
+ rules = [self.asp_of_cfg(e, t2, constraints)]
390
+ rules.append(f"mp_reach({e},{t2},N,V) :- mp_state({e},{t2},N,V).")
391
+ rules.append(f":- mp_state({e},{t2},N,V), mp_eval({e},{t2},N,-V).")
392
+ rules.append(self.asp_of_bn())
393
+ if reachable_from:
394
+ self.assert_pc_encoding()
395
+ t1 = "0"
396
+ rules.append(open(aspf("mp_positivereach-np.asp")).read())
397
+ rules.append(self.asp_of_cfg(e,t1,reachable_from))
398
+ rules.append("is_reachable({},{},{}).".format(e,t1,t2))
399
+ rules.append(f"#show. #show fixpoint(N,V) : mp_state({e},{t2},N,V).")
400
+ rules.append(open(aspf("mp_eval.asp")).read())
401
+
402
+ project = reachable_from and set(self.keys()).difference(reachable_from)
403
+ s = clingo_enum(limit=limit, project=project)
404
+ self._ground_rules(s, rules)
405
+ return s
406
+
374
407
  def fixedpoints(self, reachable_from=None, constraints={}, limit=0):
375
408
  """
376
409
  Iterator over fixed points of the MPBN (i.e., of f)
@@ -383,23 +416,8 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
383
416
  the given constraints.
384
417
  :param int limit: maximum number of solutions, ``0`` for unlimited.
385
418
  """
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",[])])
419
+ s = self._fixedpoints(reachable_from=reachable_from,
420
+ constraints=constraints, limit=limit)
403
421
  for sol in s.solve(yield_=True):
404
422
  x = {n: None for n in self}
405
423
  data = sol.symbols(shown=True)
@@ -413,36 +431,59 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
413
431
  x[n] = v
414
432
  yield x
415
433
 
434
+ def count_fixedpoints(self, reachable_from=None, constraints={}, limit=0):
435
+ """
436
+ Returns number of fixed points
416
437
 
417
- def _trapspaces(self, reachable_from=None, subcube={}, limit=0, star="*",
438
+ :param dict[str,int] reachable_from: restrict to the attractors
439
+ reachable from the given configuration. Whenever partial, restrict
440
+ attractors to the one reachable by at least one matching
441
+ configuration.
442
+ :param dict[str,int] constraints: consider only attractors matching with
443
+ the given constraints.
444
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
445
+ """
446
+ s = self._fixedpoints(reachable_from=reachable_from,
447
+ constraints=constraints, limit=limit)
448
+ return sum((1 for _ in s.solve(yield_=True)))
449
+
450
+
451
+ def _trapspaces(self, reachable_from=None, subcube={}, limit=0,
418
452
  mode="min", exclude_full=False):
419
453
  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())
454
+
455
+ rules = []
456
+ rules.append(self.asp_of_bn())
457
+ rules.append(self.rules_eval())
458
+ rules.append(open(aspf("mp_attractor.asp")).read())
459
+ rules.append("#show attractor/2.")
460
+
425
461
  e = "__a"
426
462
  t2 = "final"
427
463
  if exclude_full and not subcube:
428
- s.add("base", [], f"{{ mp_reach({e},{t2},N,(-1;1)): node(N) }} {len(self)*2-1}.")
464
+ rules.append(f"{{ mp_reach({e},{t2},N,(-1;1)): node(N) }} {len(self)*2-1}.")
429
465
  if reachable_from:
430
466
  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))
467
+ rules.append(open(aspf("mp_positivereach-np.asp")).read())
468
+ rules.append(self.asp_of_cfg(e,t1,reachable_from))
469
+ rules.append("is_reachable({},{},{}).".format(e,t1,t2))
470
+ rules.append("mp_state({},{},N,V) :- attractor(N,V).".format(e,t2))
435
471
 
436
472
  for n, b in subcube.items():
437
473
  if isinstance(b, str):
438
474
  b = int(b)
439
475
  if b not in [0,1]:
440
476
  continue
441
- s.add("base", [], ":- mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(1-b)))
477
+ rules.append(":- mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(1-b)))
442
478
 
443
- s.add("base", [], "#show attractor/2.")
479
+ project = reachable_from and set(self.keys()).difference(reachable_from)
480
+ solver = clingo_subsets if mode == "min" else clingo_supsets
481
+ s = solver(limit=limit, project=project)
482
+ self._ground_rules(s, rules)
483
+ return s
444
484
 
445
- s.ground([("base",[])])
485
+ def _yield_trapspaces(self, *args, star="*", **kwargs):
486
+ s = self._trapspaces(*args, **kwargs)
446
487
  for sol in s.solve(yield_=True):
447
488
  attractor = {n: None for n in self}
448
489
  data = sol.symbols(shown=True)
@@ -465,6 +506,10 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
465
506
  attractor[n] = v
466
507
  yield attractor
467
508
 
509
+ def _count_trapspaces(self, *args, **kwargs):
510
+ s = self._trapspaces(*args, **kwargs)
511
+ return sum((1 for _ in s.solve(yield_=True)))
512
+
468
513
  def attractors(self, reachable_from=None, constraints={}, limit=0, star='*'):
469
514
  """
470
515
  Iterator over attractors of the MPBN (minimal trap spaces of the BN).
@@ -481,17 +526,49 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
481
526
  :param str star: value to use for components which are free in the
482
527
  attractor
483
528
  """
484
- return self._trapspaces(reachable_from=reachable_from,
529
+ return self._yield_trapspaces(reachable_from=reachable_from,
485
530
  subcube=constraints, limit=limit, star=star,
486
531
  mode="min")
487
-
488
532
  minimal_trapspaces = attractors
489
533
 
490
534
  def maximal_trapspaces(self, limit=0, subcube={}, star="*",
491
535
  exclude_full=True):
492
- return self._trapspaces(subcube=subcube, limit=limit, star=star,
536
+ return self._yield_trapspaces(subcube=subcube, limit=limit, star=star,
493
537
  mode="max", exclude_full=exclude_full)
494
538
 
539
+ def count_attractors(self, reachable_from=None, constraints={}, limit=0):
540
+ """
541
+ Returns number of attractors of the MPBN (minimal trap spaces of the BN).
542
+
543
+ :param dict[str,int] reachable_from: restrict to the attractors
544
+ reachable from the given configuration. Whenever partial, restrict
545
+ attractors to the one reachable by at least one matching
546
+ configuration.
547
+ :param dict[str,int] constraints: consider only attractors matching with
548
+ the given constraints.
549
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
550
+ """
551
+ return self._count_trapspaces(reachable_from=reachable_from,
552
+ subcube=constraints, limit=limit,
553
+ mode="min")
554
+ count_minimal_trapspaces = count_attractors
555
+
556
+ def count_maximal_trapspaces(self, reachable_from=None, constraints={}, limit=0):
557
+ """
558
+ Returns number of attractors of the MPBN (minimal trap spaces of the BN).
559
+
560
+ :param dict[str,int] reachable_from: restrict to the attractors
561
+ reachable from the given configuration. Whenever partial, restrict
562
+ attractors to the one reachable by at least one matching
563
+ configuration.
564
+ :param dict[str,int] constraints: consider only attractors matching with
565
+ the given constraints.
566
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
567
+ """
568
+ return self._count_trapspaces(reachable_from=reachable_from,
569
+ subcube=constraints, limit=limit,
570
+ mode="max")
571
+
495
572
  def has_cyclic_attractor(self):
496
573
  for a in self.attractors():
497
574
  if "*" in a.values():
@@ -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,26 @@ 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("--input-is-dnf", action="store_true", default=False,
21
+ help="Functions are already in DNF form")
16
22
  ap.add_argument("--simplify", action="store_true", default=False,
17
23
  help="Try costly Boolean function simplifications to improve encoding")
18
24
  ap.add_argument("--try-unate-hard", action="store_true", default=False,
19
25
  help="Try even more costly Boolean function simplifications")
26
+ ap.add_argument("--count", action="store_true",
27
+ help="Returns only the number of solutions")
20
28
  args = ap.parse_args()
21
29
  mbn = mpbn.MPBooleanNetwork(args.bnet_file, encoding=args.encoding,
30
+ auto_dnf=not args.input_is_dnf,
22
31
  simplify=args.simplify,
23
32
  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)
33
+ if args.method in ["attractors", "fixedpoints"]:
34
+ if args.count:
35
+ func = getattr(mbn, f"count_{args.method}")
36
+ print(func(limit=args.limit))
37
+ else:
38
+ func = getattr(mbn, args.method)
39
+ for obj in func(limit=args.limit):
40
+ print(obj)
30
41
  elif args.method == "bn2asp":
31
42
  print(mbn.asp_of_bn())
@@ -0,0 +1,87 @@
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
78
+
79
+ if __name__ == "__main__":
80
+ import mpbn
81
+
82
+ f = mpbn.MPBooleanNetwork({
83
+ "x1": "x2",
84
+ "x2": "x3",
85
+ "x3": "x1"})
86
+ g = f.dynamics("asynchronous")
87
+ print(bn_of_asynchronous_transition_graph(g, list(f)))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mpbn
3
- Version: 3.4
3
+ Version: 3.8
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é
@@ -2,6 +2,7 @@ MANIFEST.in
2
2
  README.md
3
3
  setup.py
4
4
  mpbn/__init__.py
5
+ mpbn/converters.py
5
6
  mpbn/simulation.py
6
7
  mpbn.egg-info/PKG-INFO
7
8
  mpbn.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@
4
4
  from setuptools import setup, find_packages
5
5
 
6
6
  NAME = "mpbn"
7
- VERSION = "3.4"
7
+ VERSION = "3.8"
8
8
 
9
9
  setup(name=NAME,
10
10
  version=VERSION,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes