mpbn 3.1.1__tar.gz → 4.3__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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: mpbn
3
- Version: 3.1.1
3
+ Version: 4.3
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é
@@ -11,9 +11,26 @@ Classifier: Topic :: Scientific/Engineering
11
11
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
12
12
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
13
13
  Description-Content-Type: text/markdown
14
-
15
-
16
- 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)).
14
+ Requires-Dist: boolean.py
15
+ Requires-Dist: clingo
16
+ Requires-Dist: colomoto_jupyter>=0.8.0
17
+ Requires-Dist: numpy
18
+ Requires-Dist: biodivine_aeon>=1.0.1
19
+ Requires-Dist: scipy
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
31
+
32
+
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).
17
34
 
18
35
  It is built on the `minibn` module from [colomoto-jupyter](https://github.com/colomoto/colomoto-jupyter) which allows importation of Boolean networks in many formats. See http://colomoto.org/notebook.
19
36
 
@@ -31,7 +48,7 @@ pip install mpbn
31
48
 
32
49
  ### Using conda
33
50
  ```
34
- conda install -c colomoto -c potassco mpbn
51
+ conda install -c colomoto -c potassco -c daemontus mpbn
35
52
  ```
36
53
 
37
54
  ## Usage
@@ -1,5 +1,5 @@
1
1
 
2
- 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)).
2
+ 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).
3
3
 
4
4
  It is built on the `minibn` module from [colomoto-jupyter](https://github.com/colomoto/colomoto-jupyter) which allows importation of Boolean networks in many formats. See http://colomoto.org/notebook.
5
5
 
@@ -17,7 +17,7 @@ pip install mpbn
17
17
 
18
18
  ### Using conda
19
19
  ```
20
- conda install -c colomoto -c potassco mpbn
20
+ conda install -c colomoto -c potassco -c daemontus mpbn
21
21
  ```
22
22
 
23
23
  ## Usage
@@ -1,7 +1,7 @@
1
1
  """
2
2
  This module provides a simple implementation of Most Permissive Boolean Networks
3
- (MPBNs) for computing reachability properties, attractors, and reachable
4
- attractors in locally-monotonic Boolean networks.
3
+ (MPBNs) for computing reachability properties, attractors, reachable attractors.
4
+ Attractors of MPBNs are the *minimal* trap spaces of the underlying Boolean maps.
5
5
  See http://dx.doi.org/10.1101/2020.03.22.998377 and https://arxiv.org/abs/1808.10240 for technical details.
6
6
 
7
7
  It relies on clingo Answer-Set Programming solver
@@ -15,7 +15,7 @@ Quick example:
15
15
  "a": "!b",
16
16
  "b": "!a",
17
17
  "c": "!a & b"})
18
- >>> list(mbn.attractors())
18
+ >>> list(mbn.attractors()) # minimal trap spaces
19
19
  [{'a': 0, 'b': 1, 'c': 1}, {'a': 1, 'b': 0, 'c': 0}]
20
20
  >>> mbn.reachability({'a': 0, 'b': 1, 'c': 1}, {'a': 1, 'b': 0, 'c': 0})
21
21
  False
@@ -31,9 +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
- from pyeda.boolalg.expr import expr
37
34
  sys.setrecursionlimit(max(100000, sys.getrecursionlimit()))
38
35
 
39
36
  __asplibdir__ = os.path.realpath(os.path.join(os.path.dirname(__file__), "asplib"))
@@ -45,15 +42,21 @@ if hasattr(clingo, "version") and clingo.version() >= (5,5,0):
45
42
  def aspf(basename):
46
43
  return os.path.join(__asplibdir__, basename)
47
44
 
48
- def clingo_subsets(limit=0):
49
- s = clingo.Control(clingo_options)
45
+ def _clingo_domrec(mod, limit=0, project=False, extra_opts=[]):
46
+ s = clingo.Control(clingo_options + extra_opts)
50
47
  s.configuration.solve.models = limit
51
- s.configuration.solve.project = 1
48
+ if project:
49
+ s.configuration.solve.project = 1
52
50
  s.configuration.solve.enum_mode = "domRec"
53
51
  s.configuration.solver[0].heuristic = "Domain"
54
- s.configuration.solver[0].dom_mod = "5,16"
52
+ s.configuration.solver[0].dom_mod = f"{mod},{16 if project else 0}"
55
53
  return s
56
54
 
55
+ def clingo_subsets(**opts):
56
+ return _clingo_domrec(5, **opts)
57
+ def clingo_supsets(**opts):
58
+ return _clingo_domrec(3, **opts)
59
+
57
60
  def clingo_exists():
58
61
  s = clingo.Control(clingo_options)
59
62
  s.configuration.solve.models = 1
@@ -71,7 +74,8 @@ def s2v(s):
71
74
  def v2s(v):
72
75
  return 1 if v > 0 else 0
73
76
 
74
- def is_unate(ba, f):
77
+
78
+ def is_dnf(ba, f, test_unate=False):
75
79
  pos_lits = set()
76
80
  neg_lits = set()
77
81
  def is_lit(f):
@@ -95,6 +99,8 @@ def is_unate(ba, f):
95
99
  return False
96
100
 
97
101
  def test_monotonicity():
102
+ if not test_unate:
103
+ return True
98
104
  both = pos_lits.intersection(neg_lits)
99
105
  return not both
100
106
 
@@ -109,33 +115,44 @@ def is_unate(ba, f):
109
115
  return test_monotonicity()
110
116
  return False
111
117
 
112
- def asp_of_bdd(bid, b):
113
- _rules = dict()
114
- def register(node, nid=None):
115
- if node is bdd.BDDNODEONE:
116
- if nid is not None:
117
- _rules[bid] = f"bdd({clingo.String(nid)},1)"
118
- return 1
119
- elif node is bdd.BDDNODEZERO:
120
- if nid is not None:
121
- _rules[bid] = f"bdd({clingo.String(nid)},-1)"
122
- return -1
123
- nid = clingo.String(f"{bid}_n{id(node)}" if nid is None else nid)
124
- if nid not in _rules:
125
- var = clingo.String(bdd._VARS[node.root].qualname)
126
- lo = register(node.lo)
127
- hi = register(node.hi)
128
- a = f"bdd({nid},{var},{lo},{hi})"
129
- _rules[nid] = a
130
- return nid
131
- register(b.node, bid)
132
- return _rules.values()
133
-
134
- def bddasp_of_boolfunc(f, i):
135
- e = expr(str(f).replace("!","~"))
136
- b = bdd.expr2bdd(e)
137
- atoms = asp_of_bdd(i, b)
138
- return "\n".join((f"{a}." for a in atoms))
118
+ def is_dnf_unate(ba, f):
119
+ return is_dnf(ba, f, test_unate=True)
120
+
121
+ def circuitasp_of_boolfunc(f, i, ba):
122
+ atoms = []
123
+ fid = clingo.String(i)
124
+ def encode(expr):
125
+ if expr == ba.TRUE:
126
+ nodeid = "(constant,1)"
127
+ atoms.append(f"circuit({nodeid}).")
128
+ elif expr == ba.FALSE:
129
+ nodeid = "(constant,-1)"
130
+ atoms.append(f"circuit({nodeid}).")
131
+ elif isinstance(expr, ba.Symbol):
132
+ nodeid = f"(var,{clingo.String(expr.obj)})"
133
+ atoms.append(f"circuit({fid},{nodeid}).")
134
+ else:
135
+ nodeid = f"n{id(expr)}"
136
+ if isinstance(expr, ba.NOT):
137
+ nodetype = "neg"
138
+ elif isinstance(expr, ba.AND):
139
+ nodetype = "and"
140
+ elif isinstance(expr, ba.OR):
141
+ nodetype = "or"
142
+ else:
143
+ raise NotImplementedError(type(expr))
144
+ atoms.append(f"circuit({fid},{nodeid},{nodetype}).")
145
+ for child in expr.args:
146
+ cid = encode(child)
147
+ atoms.append(f"circuitedge({fid},{nodeid},{cid}).")
148
+ return nodeid
149
+ root = encode(f)
150
+ atoms.append(f"circuit({fid},root,{root}).\n")
151
+ return "\n".join(atoms)
152
+
153
+ DEFAULT_ENCODING = "mixed-dnf-bdd"
154
+ DEFAULT_BOOLFUNCLIB = os.environ.get("MPBN_BOOLFUNCLIB", "pyeda")
155
+ SUPPORTED_BOOLFUNCLIBS = ["aeon", "pyeda"]
139
156
 
140
157
  class MPBooleanNetwork(minibn.BooleanNetwork):
141
158
  """
@@ -143,17 +160,21 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
143
160
 
144
161
  Extends ``colomoto.minibn.BooleanNetwork`` class by adding methods for
145
162
  computing reachable and attractor properties with the Most Permissive
146
- semantics.
147
- It requires that the Boolean network is *locally monotonic*.
148
-
149
- Ensures that the Boolean functions are monotonic and in disjunctive normal
150
- form (DNF).
151
- The local-monotonic checking requires that a literal never appears
152
- with both signs in a same Boolean function.
163
+ update mode.
153
164
  """
165
+ supported_encodings = [
166
+ "unate-dnf", "bdd", "circuit",
167
+ "dnf-bdd", "mixed-dnf-bdd",
168
+ "force-unate-dnf"]
169
+ dnf_encodings = ["dnf-bdd", "unate-dnf", "force-unate-dnf",
170
+ "mixed-dnf-bdd"]
171
+ nonpc_encodings = ["circuit"]
172
+
154
173
  def __init__(self, bn=minibn.BooleanNetwork(), auto_dnf=True,
155
- try_unate_hard=True,
156
- encoding="auto"):
174
+ simplify=False,
175
+ try_unate_hard=False,
176
+ encoding=DEFAULT_ENCODING,
177
+ boolfunclib=DEFAULT_BOOLFUNCLIB):
157
178
  """
158
179
  Constructor for :py:class:`.MPBoooleanNetwork`.
159
180
 
@@ -162,6 +183,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
162
183
  :py:class:`colomoto.minibn.BooleanNetwork` constructor
163
184
  :param bool auto_dnf: if ``False``, turns off automatic DNF
164
185
  transformation of local functions
186
+ :param str boolfunlib: library to use for Boolean function manipulation
187
+ among ``"aeon"`` (default) or ``"pyeda"``. Default can be overriden with
188
+ ``MPBN_BOOLFUNCLIB`` environment variable.
165
189
 
166
190
  Examples:
167
191
 
@@ -170,11 +194,22 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
170
194
  >>> bn["a"] = ".."; ...
171
195
  >>> mbn = MPBooleanNetwork(bn)
172
196
  """
173
- assert encoding in ["auto", "unate-dnf", "bdd"]
174
- self.auto_dnf = auto_dnf and encoding != "bdd"
197
+ assert encoding in self.supported_encodings
198
+ assert boolfunclib in SUPPORTED_BOOLFUNCLIBS
199
+ self.auto_dnf = auto_dnf and encoding in self.dnf_encodings
175
200
  self.encoding = encoding
176
201
  self.try_unate_hard = try_unate_hard
202
+ self._simplify = simplify
177
203
  self._is_unate = dict()
204
+
205
+ self._boolfunclib = boolfunclib
206
+ __boolfunclib_symbols = (
207
+ "make_dnf_boolfunc",
208
+ "bddasp_of_boolfunc",
209
+ )
210
+ self._bf_impl = __import__(f"mpbn.boolfunclib.{boolfunclib}_impl",
211
+ fromlist=__boolfunclib_symbols)
212
+
178
213
  super(MPBooleanNetwork, self).__init__(bn)
179
214
 
180
215
  def __setitem__(self, a, f):
@@ -186,18 +221,21 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
186
221
  if isinstance(f, str):
187
222
  f = self.ba.parse(f)
188
223
  f = self._autobool(f)
189
- if self.auto_dnf:
190
- f = self.ba.dnf(f).simplify()
191
- if self.try_unate_hard:
192
- f = minibn.simplify_dnf(self.ba, f)
224
+ if self.auto_dnf and (not is_dnf(self.ba, f) or self._simplify):
225
+ f = self._bf_impl.make_dnf_boolfunc(self.ba, f,
226
+ simplify=self._simplify,
227
+ try_unate_hard=self.try_unate_hard)
193
228
  a = self._autokey(a)
194
- if self.encoding != "bdd":
195
- self._is_unate[a] = is_unate(self.ba, f)
229
+ if self.encoding in self.dnf_encodings:
230
+ self._is_unate[a] = is_dnf_unate(self.ba, f)
196
231
  if self.encoding == "unate-dnf":
197
232
  assert self._is_unate[a], f"'{f}' seems not unate. Try simplify()?"
198
233
  return super().__setitem__(a, f)
199
234
 
200
- def asp_of_bn(self):
235
+ def asp_of_bn(self, encoding=None):
236
+ if encoding is None:
237
+ encoding = self.encoding
238
+
201
239
  def clauses_of_dnf(f):
202
240
  if isinstance(f, boolean.OR):
203
241
  return f.args
@@ -221,36 +259,65 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
221
259
  facts = []
222
260
  for n, f in self.items():
223
261
  facts.append("node(\"{}\").".format(n))
224
- if self.encoding == "bdd":
225
- f_encoding = "bdd"
226
- elif self.encoding == "unate-dnf":
262
+ if encoding in ["unate-dnf", "force-unate-dnf"]:
227
263
  f_encoding = "dnf"
228
- else:
264
+ elif encoding == "dnf-bdd":
229
265
  f_encoding = "dnf" if self._is_unate[n] else "bdd"
266
+ else:
267
+ f_encoding = encoding
230
268
  if f == self.ba.FALSE:
231
269
  f = False
232
270
  elif f == self.ba.TRUE:
233
271
  f = True
234
272
  if isinstance(f, bool):
235
- facts.append(" constant(\"{}\",{}).".format(n, s2v(f)))
273
+ facts.append("constant(\"{}\",{}).".format(n, s2v(f)))
236
274
  elif f_encoding == "dnf":
237
275
  facts.extend(encode_dnf(f))
238
276
  elif f_encoding == "bdd":
239
- facts.append(bddasp_of_boolfunc(f, n))
277
+ facts.append(self._bf_impl.bddasp_of_boolfunc(self.ba, f, n))
278
+ elif f_encoding == "mixed-dnf-bdd":
279
+ facts.extend(encode_dnf(f))
280
+ if self._is_unate[n]:
281
+ facts.append(f"unate(\"{n}\").")
282
+ else:
283
+ facts.append(self._bf_impl.bddasp_of_boolfunc(self.ba, f, n))
284
+ elif f_encoding == "circuit":
285
+ facts.append(circuitasp_of_boolfunc(f, n, self.ba))
240
286
  return "".join(facts)
241
287
 
288
+ def _file_eval(self):
289
+ if self.encoding == "circuit":
290
+ f = aspf("eval_circuit.asp")
291
+ elif self.encoding == "mixed-dnf-bdd":
292
+ f = aspf("eval_mixed.asp")
293
+ else:
294
+ f = aspf("mp_eval.asp")
295
+ return f
296
+
297
+ def rules_eval(self):
298
+ f = self._file_eval()
299
+ with open(f, "r") as fp:
300
+ return fp.read()
301
+ def load_eval(self, solver):
302
+ f = self._file_eval()
303
+ solver.load(f)
304
+
305
+ def assert_pc_encoding(self):
306
+ assert self.encoding not in self.nonpc_encodings, "Unsupported encoding"
307
+
242
308
  def asp_of_cfg(self, e, t, c):
243
309
  facts = ["timepoint({},{}).".format(e,t)]
244
310
  facts += [" mp_state({},{},\"{}\",{}).".format(e,t,n,s2v(s))
245
311
  for (n,s) in c.items()]
246
- facts += [" 1 {{mp_state({},{},\"{}\",(-1;1))}} 1.".format(e,t,n)
247
- for n in self if n not in c]
312
+ facts += [f"1 {{mp_state({e},{t},N,(-1;1))}} 1 :- node(N)."]
313
+ #facts += [" 1 {{mp_state({},{},\"{}\",(-1;1))}} 1 :- node(N).".format(e,t,n)
314
+ # for n in self if n not in c]
248
315
  return "".join(facts)
249
316
 
250
317
  def reachability(self, x, y):
251
318
  """
252
319
  Returns ``True`` whenever the configuration `y` is reachable from `x`
253
- with the Most Permissive semantics.
320
+ with the Most Permissive update mode.
254
321
  Configurations can be partially defined.
255
322
  In that case, returns ``True`` whenever there exists a configuration
256
323
  matching with `y` which is reachable with at least one configuration
@@ -259,8 +326,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
259
326
  :param dict[str,int] x: initial configuration
260
327
  :param dict[str,int] y: target configuration
261
328
  """
329
+ self.assert_pc_encoding()
262
330
  s = clingo_exists()
263
- s.load(aspf("mp_eval.asp"))
331
+ self.load_eval(s)
264
332
  s.load(aspf("mp_positivereach-np.asp"))
265
333
  s.add("base", [], self.asp_of_bn())
266
334
  e = "default"
@@ -273,6 +341,32 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
273
341
  res = s.solve()
274
342
  return res.satisfiable
275
343
 
344
+ def _ground_rules(self, ctl, rules):
345
+ rules = "\n".join(rules)
346
+ ctl.add("base", [], rules)
347
+ ctl.ground([("base",[])])
348
+
349
+ def _fixedpoints(self, reachable_from=None, constraints={}, limit=0):
350
+ e = "fp"
351
+ t2 = "fp"
352
+ rules = [self.asp_of_cfg(e, t2, constraints)]
353
+ rules.append(f"mp_reach({e},{t2},N,V) :- mp_state({e},{t2},N,V).")
354
+ rules.append(f":- mp_state({e},{t2},N,V), mp_eval({e},{t2},N,-V).")
355
+ rules.append(self.asp_of_bn())
356
+ if reachable_from:
357
+ self.assert_pc_encoding()
358
+ t1 = "0"
359
+ rules.append(open(aspf("mp_positivereach-np.asp")).read())
360
+ rules.append(self.asp_of_cfg(e,t1,reachable_from))
361
+ rules.append("is_reachable({},{},{}).".format(e,t1,t2))
362
+ rules.append(f"#show. #show fixpoint(N,V) : mp_state({e},{t2},N,V).")
363
+ rules.append(open(aspf("mp_eval.asp")).read())
364
+
365
+ project = reachable_from and set(self.keys()).difference(reachable_from)
366
+ s = clingo_enum(limit=limit, project=project)
367
+ self._ground_rules(s, rules)
368
+ return s
369
+
276
370
  def fixedpoints(self, reachable_from=None, constraints={}, limit=0):
277
371
  """
278
372
  Iterator over fixed points of the MPBN (i.e., of f)
@@ -285,22 +379,8 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
285
379
  the given constraints.
286
380
  :param int limit: maximum number of solutions, ``0`` for unlimited.
287
381
  """
288
- s = clingo_enum(limit=limit)
289
- s.add("base", [], self.asp_of_bn())
290
- e = "fp"
291
- t2 = "fp"
292
- s.add("base", [], self.asp_of_cfg(e, t2, constraints))
293
- s.load(aspf("mp_eval.asp"))
294
- s.add("base", [], f"mp_reach({e},{t2},N,V) :- mp_state({e},{t2},N,V).")
295
- s.add("base", [], f":- mp_state({e},{t2},N,V), mp_eval({e},{t2},N,-V).")
296
- if reachable_from:
297
- t1 = "0"
298
- s.load(aspf("mp_positivereach-np.asp"))
299
- s.add("base", [], self.asp_of_cfg(e,t1,reachable_from))
300
- s.add("base", [], "is_reachable({},{},{}).".format(e,t1,t2))
301
- s.add("base", [], f"#show. #show fixpoint(N,V) : mp_state({e},{t2},N,V).")
302
-
303
- s.ground([("base",[])])
382
+ s = self._fixedpoints(reachable_from=reachable_from,
383
+ constraints=constraints, limit=limit)
304
384
  for sol in s.solve(yield_=True):
305
385
  x = {n: None for n in self}
306
386
  data = sol.symbols(shown=True)
@@ -314,11 +394,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
314
394
  x[n] = v
315
395
  yield x
316
396
 
317
- def attractors(self, reachable_from=None, constraints={}, limit=0, star='*'):
397
+ def count_fixedpoints(self, reachable_from=None, constraints={}, limit=0):
318
398
  """
319
- Iterator over attractors of the MPBN.
320
- An attractor is an hypercube, represented by a dictionnary mapping every
321
- component of the network to either ``0``, ``1``, or ``star``.
399
+ Returns number of fixed points
322
400
 
323
401
  :param dict[str,int] reachable_from: restrict to the attractors
324
402
  reachable from the given configuration. Whenever partial, restrict
@@ -327,29 +405,48 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
327
405
  :param dict[str,int] constraints: consider only attractors matching with
328
406
  the given constraints.
329
407
  :param int limit: maximum number of solutions, ``0`` for unlimited.
330
- :param str star: value to use for components which are free in the
331
- attractor
332
408
  """
333
- s = clingo_subsets(limit=limit)
334
- s.load(aspf("mp_eval.asp"))
335
- s.load(aspf("mp_attractor.asp"))
336
- s.add("base", [], self.asp_of_bn())
409
+ s = self._fixedpoints(reachable_from=reachable_from,
410
+ constraints=constraints, limit=limit)
411
+ return sum((1 for _ in s.solve(yield_=True)))
412
+
413
+
414
+ def _trapspaces(self, reachable_from=None, subcube={}, limit=0,
415
+ mode="min", exclude_full=False):
416
+ self.assert_pc_encoding()
417
+
418
+ rules = []
419
+ rules.append(self.asp_of_bn())
420
+ rules.append(self.rules_eval())
421
+ rules.append(open(aspf("mp_attractor.asp")).read())
422
+ rules.append("#show attractor/2.")
423
+
337
424
  e = "__a"
338
425
  t2 = "final"
426
+ if exclude_full and not subcube:
427
+ rules.append(f"{{ mp_reach({e},{t2},N,(-1;1)): node(N) }} {len(self)*2-1}.")
339
428
  if reachable_from:
340
429
  t1 = "0"
341
- s.load(aspf("mp_positivereach-np.asp"))
342
- s.add("base", [], self.asp_of_cfg(e,t1,reachable_from))
343
- s.add("base", [], "is_reachable({},{},{}).".format(e,t1,t2))
344
- s.add("base", [], "mp_state({},{},N,V) :- attractor(N,V).".format(e,t2))
345
-
346
- for n, b in constraints.items():
347
- s.add("base", [], "mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(b)))
348
- s.add("base", [], ":- mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(1-b)))
349
-
350
- s.add("base", [], "#show attractor/2.")
351
-
352
- s.ground([("base",[])])
430
+ rules.append(open(aspf("mp_positivereach-np.asp")).read())
431
+ rules.append(self.asp_of_cfg(e,t1,reachable_from))
432
+ rules.append("is_reachable({},{},{}).".format(e,t1,t2))
433
+ rules.append("mp_state({},{},N,V) :- attractor(N,V).".format(e,t2))
434
+
435
+ for n, b in subcube.items():
436
+ if isinstance(b, str):
437
+ b = int(b)
438
+ if b not in [0,1]:
439
+ continue
440
+ rules.append(":- mp_reach({},{},\"{}\",{}).".format(e,t2,n,s2v(1-b)))
441
+
442
+ project = reachable_from and set(self.keys()).difference(reachable_from)
443
+ solver = clingo_subsets if mode == "min" else clingo_supsets
444
+ s = solver(limit=limit, project=project)
445
+ self._ground_rules(s, rules)
446
+ return s
447
+
448
+ def _yield_trapspaces(self, *args, star="*", **kwargs):
449
+ s = self._trapspaces(*args, **kwargs)
353
450
  for sol in s.solve(yield_=True):
354
451
  attractor = {n: None for n in self}
355
452
  data = sol.symbols(shown=True)
@@ -364,11 +461,77 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
364
461
  else:
365
462
  v = 1 if v == 1 else 0
366
463
  if attractor[n] is not None:
367
- attractor[n] = star
464
+ if star is not None:
465
+ attractor[n] = star
466
+ else:
467
+ del attractor[n]
368
468
  else:
369
469
  attractor[n] = v
370
470
  yield attractor
371
471
 
472
+ def _count_trapspaces(self, *args, **kwargs):
473
+ s = self._trapspaces(*args, **kwargs)
474
+ return sum((1 for _ in s.solve(yield_=True)))
475
+
476
+ def attractors(self, reachable_from=None, constraints={}, limit=0, star='*'):
477
+ """
478
+ Iterator over attractors of the MPBN (minimal trap spaces of the BN).
479
+ An attractor is an hypercube, represented by a dictionnary mapping every
480
+ component of the network to either ``0``, ``1``, or ``star``.
481
+
482
+ :param dict[str,int] reachable_from: restrict to the attractors
483
+ reachable from the given configuration. Whenever partial, restrict
484
+ attractors to the one reachable by at least one matching
485
+ configuration.
486
+ :param dict[str,int] constraints: consider only attractors matching with
487
+ the given constraints.
488
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
489
+ :param str star: value to use for components which are free in the
490
+ attractor
491
+ """
492
+ return self._yield_trapspaces(reachable_from=reachable_from,
493
+ subcube=constraints, limit=limit, star=star,
494
+ mode="min")
495
+ minimal_trapspaces = attractors
496
+
497
+ def maximal_trapspaces(self, limit=0, subcube={}, star="*",
498
+ exclude_full=True):
499
+ return self._yield_trapspaces(subcube=subcube, limit=limit, star=star,
500
+ mode="max", exclude_full=exclude_full)
501
+
502
+ def count_attractors(self, reachable_from=None, constraints={}, limit=0):
503
+ """
504
+ Returns number of attractors of the MPBN (minimal trap spaces of the BN).
505
+
506
+ :param dict[str,int] reachable_from: restrict to the attractors
507
+ reachable from the given configuration. Whenever partial, restrict
508
+ attractors to the one reachable by at least one matching
509
+ configuration.
510
+ :param dict[str,int] constraints: consider only attractors matching with
511
+ the given constraints.
512
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
513
+ """
514
+ return self._count_trapspaces(reachable_from=reachable_from,
515
+ subcube=constraints, limit=limit,
516
+ mode="min")
517
+ count_minimal_trapspaces = count_attractors
518
+
519
+ def count_maximal_trapspaces(self, reachable_from=None, constraints={}, limit=0):
520
+ """
521
+ Returns number of attractors of the MPBN (minimal trap spaces of the BN).
522
+
523
+ :param dict[str,int] reachable_from: restrict to the attractors
524
+ reachable from the given configuration. Whenever partial, restrict
525
+ attractors to the one reachable by at least one matching
526
+ configuration.
527
+ :param dict[str,int] constraints: consider only attractors matching with
528
+ the given constraints.
529
+ :param int limit: maximum number of solutions, ``0`` for unlimited.
530
+ """
531
+ return self._count_trapspaces(reachable_from=reachable_from,
532
+ subcube=constraints, limit=limit,
533
+ mode="max")
534
+
372
535
  def has_cyclic_attractor(self):
373
536
  for a in self.attractors():
374
537
  if "*" in a.values():
@@ -378,7 +541,7 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
378
541
  def reachable_from(self, x, reversed=False):
379
542
  """
380
543
  Returns an iterator over the configurations reachable from ``x`` with the
381
- Most Permissive semantics.
544
+ Most Permissive update mode.
382
545
  Configuration ``x`` can be partially defined: in that case a configuration
383
546
  is yielded whnever it is reachable from at least one configuration
384
547
  matching with ``x``.
@@ -386,8 +549,9 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
386
549
  Whenever ``reversed`` is ``True``, yields over the configurations that can
387
550
  reach `x` instead.
388
551
  """
552
+ self.assert_pc_encoding()
389
553
  s = clingo_enum()
390
- s.load(aspf("mp_eval.asp"))
554
+ self.load_eval(s)
391
555
  s.load(aspf("mp_positivereach-np.asp"))
392
556
  s.add("base", [], self.asp_of_bn())
393
557
  e = "default"
@@ -410,7 +574,7 @@ class MPBooleanNetwork(minibn.BooleanNetwork):
410
574
  def dynamics(self, update_mode="mp", **kwargs):
411
575
  """
412
576
  Returns a :py:class:`networkx.DiGraph` object representing the transitions between
413
- the configurations using the Most Permissive semantics by default.
577
+ the configurations using the Most Permissive update mode by default.
414
578
  See :py:meth:`colomoto.minibn.BooleanNetwork.dynamics`.
415
579
  """
416
580
  if update_mode in ["mp", "most-permissive"]:
@@ -0,0 +1,28 @@
1
+ mp_eval(E,T,N,V) :- timepoint(E,T), node(N), circuit(N,root,C), evalcircuit(E,T,N,C,V).
2
+ mp_eval(E,T,N,V) :- timepoint(E,T), node(N), constant(N,V).
3
+
4
+ %
5
+ % leafs
6
+ %
7
+ evalcircuit(E,T,N,(var,M),V) :- circuit(N,(var,M)), mp_reach(E,T,M,V).
8
+ evalcircuit(E,T,N,(constant,V),V) :- timepoint(E,T),node(N),circuit((constant,V)).
9
+
10
+ %
11
+ % operators
12
+ %
13
+
14
+ % negation
15
+ evalcircuit(E,T,N,C,-V) :- circuit(N,C,neg), circuitedge(N,C,D),
16
+ evalcircuit(E,T,N,D,V).
17
+
18
+ % disjunction
19
+ evalcircuit(E,T,N,C,1) :- circuit(N,C,or), circuitedge(N,C,D),
20
+ evalcircuit(E,T,N,D,1).
21
+ evalcircuit(E,T,N,C,-1) :- timepoint(E,T), circuit(N,C,or);
22
+ evalcircuit(E,T,N,D,-1): circuitedge(N,C,D).
23
+
24
+ % conjunction
25
+ evalcircuit(E,T,N,C,-1) :- circuit(N,C,and), circuitedge(N,C,D),
26
+ evalcircuit(E,T,N,D,-1).
27
+ evalcircuit(E,T,N,C,1) :- timepoint(E,T), circuit(N,C,and);
28
+ evalcircuit(E,T,N,D,1): circuitedge(N,C,D).