rafkit 0.2.0__tar.gz → 0.3.0__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.
- {rafkit-0.2.0/src/rafkit.egg-info → rafkit-0.3.0}/PKG-INFO +33 -3
- {rafkit-0.2.0 → rafkit-0.3.0}/README.md +32 -2
- {rafkit-0.2.0 → rafkit-0.3.0}/pyproject.toml +1 -1
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/__init__.py +4 -1
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/crs.py +25 -8
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/gillespie.py +10 -0
- rafkit-0.3.0/src/rafkit/inhibition.py +114 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/network.py +12 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/raf.py +10 -1
- {rafkit-0.2.0 → rafkit-0.3.0/src/rafkit.egg-info}/PKG-INFO +33 -3
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit.egg-info/SOURCES.txt +2 -0
- rafkit-0.3.0/tests/test_inhibition.py +239 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/LICENSE +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/setup.cfg +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/binary_polymer.py +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit/catalysis.py +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit.egg-info/dependency_links.txt +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit.egg-info/requires.txt +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/src/rafkit.egg-info/top_level.txt +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/tests/test_crs.py +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/tests/test_gillespie.py +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/tests/test_published_examples.py +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/tests/test_raf.py +0 -0
- {rafkit-0.2.0 → rafkit-0.3.0}/tests/test_seeding.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rafkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Autocatalytic (RAF) sets in catalytic reaction networks: maximal RAFs, irreducible cores, and Kauffman binary polymer models.
|
|
5
5
|
Author: James P. Galasyn, Claude Théodore
|
|
6
6
|
License: MIT
|
|
@@ -116,6 +116,7 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
116
116
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
117
117
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
118
118
|
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
119
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
119
120
|
|
|
120
121
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
121
122
|
is subtly wrong produces plausible numbers rather than errors.
|
|
@@ -141,8 +142,37 @@ last two rows.
|
|
|
141
142
|
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
142
143
|
systems stay simple to write.
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
## Inhibition
|
|
146
|
+
|
|
147
|
+
A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
|
|
148
|
+
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
|
|
149
|
+
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
|
|
150
|
+
"the" maximal u-RAF.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Food: a, b
|
|
154
|
+
r1 : a + b [a] {d} => c # inhibited by d
|
|
155
|
+
r2 : a + b [b] {c} => d # inhibited by c
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
|
|
159
|
+
inhibitor, and their union is an RAF that fails the uninhibited condition.
|
|
160
|
+
|
|
161
|
+
`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
|
|
162
|
+
running network can **lose** a subRAF, not merely gain one. See
|
|
163
|
+
`examples/inhibition_dissolution.py`.
|
|
164
|
+
|
|
165
|
+
The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
|
|
166
|
+
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
|
|
167
|
+
is correct because the uninhibited property is inherited downward — every sub-RAF of a
|
|
168
|
+
u-RAF is a u-RAF.
|
|
169
|
+
|
|
170
|
+
Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
|
|
171
|
+
tractable in *k*, the number of inhibition classes — and **k is a property of how you
|
|
172
|
+
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
|
|
173
|
+
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
|
|
174
|
+
number of inhibited reactions, which is the difference between `2^k` being feasible
|
|
175
|
+
and not.
|
|
146
176
|
|
|
147
177
|
## Notes on irreducible cores
|
|
148
178
|
|
|
@@ -88,6 +88,7 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
88
88
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
89
89
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
90
90
|
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
91
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
91
92
|
|
|
92
93
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
93
94
|
is subtly wrong produces plausible numbers rather than errors.
|
|
@@ -113,8 +114,37 @@ last two rows.
|
|
|
113
114
|
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
114
115
|
systems stay simple to write.
|
|
115
116
|
|
|
116
|
-
|
|
117
|
-
|
|
117
|
+
## Inhibition
|
|
118
|
+
|
|
119
|
+
A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
|
|
120
|
+
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
|
|
121
|
+
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
|
|
122
|
+
"the" maximal u-RAF.
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Food: a, b
|
|
126
|
+
r1 : a + b [a] {d} => c # inhibited by d
|
|
127
|
+
r2 : a + b [b] {c} => d # inhibited by c
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
|
|
131
|
+
inhibitor, and their union is an RAF that fails the uninhibited condition.
|
|
132
|
+
|
|
133
|
+
`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
|
|
134
|
+
running network can **lose** a subRAF, not merely gain one. See
|
|
135
|
+
`examples/inhibition_dissolution.py`.
|
|
136
|
+
|
|
137
|
+
The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
|
|
138
|
+
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
|
|
139
|
+
is correct because the uninhibited property is inherited downward — every sub-RAF of a
|
|
140
|
+
u-RAF is a u-RAF.
|
|
141
|
+
|
|
142
|
+
Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
|
|
143
|
+
tractable in *k*, the number of inhibition classes — and **k is a property of how you
|
|
144
|
+
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
|
|
145
|
+
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
|
|
146
|
+
number of inhibited reactions, which is the difference between `2^k` being feasible
|
|
147
|
+
and not.
|
|
118
148
|
|
|
119
149
|
## Notes on irreducible cores
|
|
120
150
|
|
|
@@ -16,6 +16,8 @@ from rafkit.binary_polymer import BinaryPolymerNetwork, binary_polymer
|
|
|
16
16
|
from rafkit.catalysis import catalysing_molecules, is_catalysed, normalise
|
|
17
17
|
from rafkit.crs import parse_crs, read_crs, to_crs, write_crs
|
|
18
18
|
from rafkit.gillespie import Trajectory, propensities, simulate
|
|
19
|
+
from rafkit.inhibition import (classes_from_inhibitors, is_uninhibited, is_uraf,
|
|
20
|
+
max_urafs, support)
|
|
19
21
|
from rafkit.network import ReactionNetwork
|
|
20
22
|
from rafkit.raf import (
|
|
21
23
|
RafResult, catrenet_strictly_autocatalytic, core_raf, exploitability,
|
|
@@ -23,7 +25,7 @@ from rafkit.raf import (
|
|
|
23
25
|
sample_irrraf,
|
|
24
26
|
)
|
|
25
27
|
|
|
26
|
-
__version__ = "0.
|
|
28
|
+
__version__ = "0.3.0"
|
|
27
29
|
|
|
28
30
|
__all__ = [
|
|
29
31
|
"BinaryPolymerNetwork", "binary_polymer", "ReactionNetwork",
|
|
@@ -33,4 +35,5 @@ __all__ = [
|
|
|
33
35
|
"is_catalysed", "catalysing_molecules", "normalise",
|
|
34
36
|
"parse_crs", "read_crs", "to_crs", "write_crs",
|
|
35
37
|
"simulate", "propensities", "Trajectory",
|
|
38
|
+
"max_urafs", "is_uraf", "is_uninhibited", "support", "classes_from_inhibitors",
|
|
36
39
|
]
|
|
@@ -20,7 +20,10 @@ Catalysts are alternatives, any one of which suffices. A braced group is a
|
|
|
20
20
|
**conjunctive** requirement -- `[{a,d}, e]` means *a and d together*, or *e* -- which
|
|
21
21
|
is the notation Huson, Xavier & Steel (2024) use. Two edge cases carry meaning and are
|
|
22
22
|
not interchangeable: `[]` means the reaction **must** be catalysed and nothing does so,
|
|
23
|
-
while `[{}]` means it **may proceed uncatalysed**.
|
|
23
|
+
while `[{}]` means it **may proceed uncatalysed**.
|
|
24
|
+
|
|
25
|
+
A brace group **after** the catalyst bracket lists **inhibitors**, space- or
|
|
26
|
+
comma-separated, following CatReNet: `r1 : a + b [c] {d e} -> x`. A reversible reaction is read as **two** reactions, forward and
|
|
24
27
|
reverse, sharing a catalyst set -- which is the reading its own generator uses.
|
|
25
28
|
|
|
26
29
|
`X + X -> Y` is written `X ... -> Y`, with the repeated reactant collapsed. Since
|
|
@@ -76,44 +79,54 @@ def parse_crs(text: str) -> ReactionNetwork:
|
|
|
76
79
|
continue
|
|
77
80
|
name, body = m.group("name"), m.group("body")
|
|
78
81
|
|
|
82
|
+
inhib: list[str] = []
|
|
79
83
|
cats: list[list[str]] = []
|
|
80
84
|
if "[" in body:
|
|
81
85
|
pre, rest = body.split("[", 1)
|
|
82
86
|
inside, post = rest.split("]", 1)
|
|
83
87
|
cats = _parse_catalysts(inside)
|
|
84
88
|
body = pre + " " + post
|
|
89
|
+
# A brace group AFTER the catalysts is the inhibitor list (CatReNet).
|
|
90
|
+
m_inh = re.search(r"\{([^}]*)\}", body)
|
|
91
|
+
if m_inh:
|
|
92
|
+
inhib = [t for t in re.split(r"[,\s]+", m_inh.group(1)) if t]
|
|
93
|
+
body = body[:m_inh.start()] + " " + body[m_inh.end():]
|
|
85
94
|
|
|
86
95
|
arrow = _ARROW.search(body)
|
|
87
96
|
if not arrow:
|
|
88
97
|
continue
|
|
89
98
|
lhs, rhs = body[:arrow.start()], body[arrow.end():]
|
|
90
|
-
parsed.append((name, _split_list(lhs), _split_list(rhs), cats,
|
|
99
|
+
parsed.append((name, _split_list(lhs), _split_list(rhs), cats, inhib,
|
|
91
100
|
arrow.group(1) in ("<->", "<=>")))
|
|
92
101
|
|
|
93
102
|
# Stable molecule indexing: food first, then order of appearance.
|
|
94
103
|
index: dict[str, int] = {}
|
|
95
104
|
for n in food_names:
|
|
96
105
|
index.setdefault(n, len(index))
|
|
97
|
-
for _, lhs, rhs, cats, _ in parsed:
|
|
98
|
-
for n in (*lhs, *rhs, *(x for g in cats for x in g)):
|
|
106
|
+
for _, lhs, rhs, cats, inhib, _ in parsed:
|
|
107
|
+
for n in (*lhs, *rhs, *(x for g in cats for x in g), *inhib):
|
|
99
108
|
index.setdefault(n, len(index))
|
|
100
109
|
|
|
101
|
-
pairs, catalysts, names = [], [], []
|
|
102
|
-
for name, lhs, rhs, cats, reversible in parsed:
|
|
110
|
+
pairs, catalysts, names, inhibitors = [], [], [], []
|
|
111
|
+
for name, lhs, rhs, cats, inhib, reversible in parsed:
|
|
103
112
|
cat = frozenset(frozenset(index[c] for c in g) for g in cats)
|
|
113
|
+
inh = frozenset(index[x] for x in inhib)
|
|
104
114
|
fwd = (tuple(index[x] for x in lhs), tuple(index[x] for x in rhs))
|
|
105
115
|
pairs.append(fwd); catalysts.append(cat); names.append(name)
|
|
116
|
+
inhibitors.append(inh)
|
|
106
117
|
if reversible:
|
|
107
118
|
pairs.append((fwd[1], fwd[0]))
|
|
108
119
|
catalysts.append(cat)
|
|
109
120
|
names.append(f"{name}_rev")
|
|
121
|
+
inhibitors.append(inh)
|
|
110
122
|
|
|
111
123
|
molecules = tuple(sorted(index, key=index.get))
|
|
112
124
|
return ReactionNetwork(molecules=molecules,
|
|
113
125
|
food=frozenset(index[n] for n in food_names),
|
|
114
126
|
reaction_pairs=tuple(pairs),
|
|
115
127
|
catalysts=tuple(catalysts),
|
|
116
|
-
names=tuple(names)
|
|
128
|
+
names=tuple(names),
|
|
129
|
+
inhibitors=tuple(inhibitors))
|
|
117
130
|
|
|
118
131
|
|
|
119
132
|
def read_crs(path: str | Path) -> ReactionNetwork:
|
|
@@ -140,7 +153,11 @@ def to_crs(net, comment: str = "") -> str:
|
|
|
140
153
|
lhs = " + ".join(dict.fromkeys(name(x) for x in net.reactants(r)))
|
|
141
154
|
rhs = " + ".join(dict.fromkeys(name(x) for x in net.products(r)))
|
|
142
155
|
cats = _format_catalysts(net, r)
|
|
143
|
-
|
|
156
|
+
inh = getattr(net, "inhibitors", ())
|
|
157
|
+
inh_s = ""
|
|
158
|
+
if inh and inh[r]:
|
|
159
|
+
inh_s = " {" + " ".join(sorted(name(x) for x in inh[r])) + "}"
|
|
160
|
+
out.append(f"{names[r]} : {lhs} [{cats}]{inh_s} => {rhs}")
|
|
144
161
|
out.append("")
|
|
145
162
|
return "\n".join(out)
|
|
146
163
|
|
|
@@ -15,6 +15,8 @@ reduction factor**, which is the mechanism under test.
|
|
|
15
15
|
Conventions, all inherited from the reference rather than chosen here:
|
|
16
16
|
|
|
17
17
|
* a reaction whose catalyst is absent still proceeds, at ``1 / uncatalysed_factor``;
|
|
18
|
+
* a reaction with an **inhibitor present does not proceed at all** -- inhibition is a
|
|
19
|
+
block, not a slowdown, and it is independent of catalysis;
|
|
18
20
|
* food molecules are replenished when they fall below ``food_floor``;
|
|
19
21
|
* a ligation ``a + b -> ab`` with ``a == b`` takes the pair count ``n(n-1)/2``, not ``n^2``.
|
|
20
22
|
"""
|
|
@@ -88,6 +90,11 @@ def propensities(net, counts: np.ndarray, *,
|
|
|
88
90
|
runs at ``1 / uncatalysed_factor`` of it. That difference is the whole mechanism:
|
|
89
91
|
it makes seeding rare but not impossible.
|
|
90
92
|
|
|
93
|
+
**Inhibition is absolute**: if any molecule inhibiting a reaction is present, its
|
|
94
|
+
propensity is zero regardless of catalysis. This is what lets a running network
|
|
95
|
+
*lose* a subRAF rather than only gain one -- the effect Hordijk, Naylor, Krasnogor
|
|
96
|
+
& Fellermann (2018) report as toxic elements causing loss of autocatalytic subsets.
|
|
97
|
+
|
|
91
98
|
`reactions` restricts which reactions may fire. **This is a fidelity requirement,
|
|
92
99
|
not a convenience.** Hordijk & Steel study "the molecular flow on this maximal RAF";
|
|
93
100
|
simulating the entire generated network instead lets any reaction fire uncatalysed,
|
|
@@ -98,10 +105,13 @@ def propensities(net, counts: np.ndarray, *,
|
|
|
98
105
|
out = np.zeros(net.n_reactions)
|
|
99
106
|
allowed = range(net.n_reactions) if reactions is None else reactions
|
|
100
107
|
present = frozenset(np.flatnonzero(counts).tolist())
|
|
108
|
+
inhibitors = getattr(net, "inhibitors", ())
|
|
101
109
|
for r in allowed:
|
|
102
110
|
combos = _pair_count(counts, net.reactants(r))
|
|
103
111
|
if combos <= 0:
|
|
104
112
|
continue
|
|
113
|
+
if inhibitors and (inhibitors[r] & present):
|
|
114
|
+
continue # inhibited: blocked outright
|
|
105
115
|
catalysed = is_catalysed(net.catalysts[r], present)
|
|
106
116
|
out[r] = combos if catalysed else combos / uncatalysed_factor
|
|
107
117
|
return out
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Uninhibited RAFs, where a molecule can prevent a reaction.
|
|
2
|
+
|
|
3
|
+
Hordijk & Steel (2012), Part II. Inhibition is given as ``k`` pairs ``(X_i, R_i)``:
|
|
4
|
+
every molecule in ``X_i`` inhibits every reaction in ``R_i``. A set ``R'`` is an
|
|
5
|
+
**uninhibited RAF** (u-RAF) when
|
|
6
|
+
|
|
7
|
+
* **(u-1)** ``R'`` is an RAF, and
|
|
8
|
+
* **(u-2)** ``R' ∩ R_i != empty`` implies ``supp(R') ∩ X_i = empty``,
|
|
9
|
+
|
|
10
|
+
where ``supp(R')`` is every molecule appearing as a reactant or product in ``R'``.
|
|
11
|
+
|
|
12
|
+
**Inhibition breaks the structure the rest of this library rests on.** Adding a reaction
|
|
13
|
+
can now *disable* another, so the maximal-RAF operator is no longer monotone -- and
|
|
14
|
+
monotonicity is what gives a *unique* maximum (Huson, Xavier & Steel 2024, lemma 3.1).
|
|
15
|
+
There is therefore no "the" maximal u-RAF: `max_urafs` returns a **collection**, and
|
|
16
|
+
that difference is in the signature deliberately rather than in a footnote. Deciding
|
|
17
|
+
whether a u-RAF exists at all is NP-complete.
|
|
18
|
+
|
|
19
|
+
What rescues it is that the problem is fixed-parameter tractable in ``k``, by their
|
|
20
|
+
theorem 1: the maximal u-RAFs are exactly the non-empty sets ``s(R_J ∩ R^J)`` as ``J``
|
|
21
|
+
ranges over subsets of ``[k]``. So the cost is ``2^k`` calls to the ordinary maximal-RAF
|
|
22
|
+
algorithm, and **``k`` is a property of how inhibition is encoded, not of the
|
|
23
|
+
chemistry**. One class per inhibited reaction makes ``2^k`` hopeless immediately;
|
|
24
|
+
`classes_from_inhibitors` groups by inhibiting *molecule* instead, which is what the
|
|
25
|
+
paper means by considering "types" of molecules that inhibit "types" of reactions.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from itertools import combinations
|
|
30
|
+
|
|
31
|
+
from rafkit.raf import _refine
|
|
32
|
+
|
|
33
|
+
Inhibition = tuple[tuple[frozenset[int], frozenset[int]], ...]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def support(net, reactions) -> frozenset[int]:
|
|
37
|
+
"""Every molecule that is a reactant or product of some reaction in the set.
|
|
38
|
+
|
|
39
|
+
Catalysts are deliberately excluded: the paper's ``supp`` is over reactants and
|
|
40
|
+
products only, and including catalysts would make (u-2) strictly harder to satisfy.
|
|
41
|
+
"""
|
|
42
|
+
out: set[int] = set()
|
|
43
|
+
for r in reactions:
|
|
44
|
+
out.update(net.reactants(r))
|
|
45
|
+
out.update(net.products(r))
|
|
46
|
+
return frozenset(out)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def classes_from_inhibitors(net) -> Inhibition:
|
|
50
|
+
"""Build the ``(X_i, R_i)`` classes from a network's per-reaction inhibitors.
|
|
51
|
+
|
|
52
|
+
Grouped by inhibiting **molecule**, so ``k`` is the number of distinct inhibitors
|
|
53
|
+
rather than the number of inhibited reactions. That choice is the difference
|
|
54
|
+
between a feasible ``2^k`` and an impossible one, and it costs nothing: the pair
|
|
55
|
+
``({x}, {reactions x inhibits})`` expresses exactly the same relation.
|
|
56
|
+
"""
|
|
57
|
+
by_molecule: dict[int, set[int]] = {}
|
|
58
|
+
for r, inhibitors in enumerate(getattr(net, "inhibitors", ()) or ()):
|
|
59
|
+
for x in inhibitors:
|
|
60
|
+
by_molecule.setdefault(x, set()).add(r)
|
|
61
|
+
return tuple((frozenset({x}), frozenset(rs))
|
|
62
|
+
for x, rs in sorted(by_molecule.items()))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def is_uninhibited(net, reactions, inhibition: Inhibition) -> bool:
|
|
66
|
+
"""Condition (u-2): nothing the set makes or uses inhibits anything the set does."""
|
|
67
|
+
rs = frozenset(reactions)
|
|
68
|
+
supp = support(net, rs)
|
|
69
|
+
return all(not (rs & R_i) or not (supp & X_i) for X_i, R_i in inhibition)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_uraf(net, reactions, inhibition: Inhibition) -> bool:
|
|
73
|
+
"""Both conditions: an RAF that inhibits none of its own reactions."""
|
|
74
|
+
rs = frozenset(reactions)
|
|
75
|
+
return bool(rs) and _refine(net, rs) == rs and is_uninhibited(net, rs, inhibition)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def max_urafs(net, inhibition: Inhibition | None = None,
|
|
79
|
+
reactions=None) -> tuple[frozenset[int], ...]:
|
|
80
|
+
"""All maximal uninhibited RAFs, by Hordijk & Steel (2012) theorem 1.
|
|
81
|
+
|
|
82
|
+
Returns a tuple because there is generally more than one and none of them is
|
|
83
|
+
canonical -- see the module docstring. Empty tuple means no u-RAF exists.
|
|
84
|
+
|
|
85
|
+
Cost is ``2^k`` maximal-RAF computations, with ``k = len(inhibition)``.
|
|
86
|
+
"""
|
|
87
|
+
if inhibition is None:
|
|
88
|
+
inhibition = classes_from_inhibitors(net)
|
|
89
|
+
allowed = frozenset(range(net.n_reactions) if reactions is None else reactions)
|
|
90
|
+
if not inhibition:
|
|
91
|
+
maximal = _refine(net, allowed)
|
|
92
|
+
return (maximal,) if maximal else ()
|
|
93
|
+
|
|
94
|
+
k = len(inhibition)
|
|
95
|
+
found: set[frozenset[int]] = set()
|
|
96
|
+
for size in range(k + 1):
|
|
97
|
+
for J in combinations(range(k), size):
|
|
98
|
+
Jset = set(J)
|
|
99
|
+
# R_J: reactions untouched by every class NOT in J.
|
|
100
|
+
R_J = frozenset(
|
|
101
|
+
r for r in allowed
|
|
102
|
+
if all(not (support(net, {r}) & inhibition[j][0])
|
|
103
|
+
for j in range(k) if j not in Jset))
|
|
104
|
+
# R^J: reactions inhibited by no class IN J.
|
|
105
|
+
R_super = frozenset(
|
|
106
|
+
r for r in allowed if all(r not in inhibition[j][1] for j in Jset))
|
|
107
|
+
candidate = _refine(net, R_J & R_super)
|
|
108
|
+
if candidate:
|
|
109
|
+
found.add(candidate)
|
|
110
|
+
|
|
111
|
+
# Different J can yield nested results; only the maximal ones are u-RAFs by (iii).
|
|
112
|
+
return tuple(sorted((s for s in found
|
|
113
|
+
if not any(s < t for t in found)),
|
|
114
|
+
key=lambda s: (-len(s), sorted(s))))
|
|
@@ -43,6 +43,12 @@ class ReactionNetwork:
|
|
|
43
43
|
reaction_pairs: tuple[tuple[tuple[int, ...], tuple[int, ...]], ...]
|
|
44
44
|
catalysts: tuple[frozenset[int], ...]
|
|
45
45
|
names: tuple[str, ...] = ()
|
|
46
|
+
inhibitors: tuple[frozenset[int], ...] = ()
|
|
47
|
+
"""Per reaction, the molecules that inhibit it (CatReNet's model and CRS form).
|
|
48
|
+
|
|
49
|
+
`rafkit.inhibition.classes_from_inhibitors` converts this into the (X_i, R_i)
|
|
50
|
+
classes the algorithm needs, grouping by molecule to keep k small.
|
|
51
|
+
"""
|
|
46
52
|
|
|
47
53
|
def __post_init__(self):
|
|
48
54
|
object.__setattr__(self, "catalysts",
|
|
@@ -51,6 +57,12 @@ class ReactionNetwork:
|
|
|
51
57
|
raise ValueError(
|
|
52
58
|
f"{len(self.catalysts)} catalyst sets for "
|
|
53
59
|
f"{len(self.reaction_pairs)} reactions")
|
|
60
|
+
if not self.inhibitors:
|
|
61
|
+
object.__setattr__(self, "inhibitors",
|
|
62
|
+
(frozenset(),) * len(self.reaction_pairs))
|
|
63
|
+
else:
|
|
64
|
+
object.__setattr__(self, "inhibitors",
|
|
65
|
+
tuple(frozenset(i) for i in self.inhibitors))
|
|
54
66
|
if not self.names:
|
|
55
67
|
object.__setattr__(
|
|
56
68
|
self, "names", tuple(f"r{i + 1}" for i in range(len(self.reaction_pairs))))
|
|
@@ -218,15 +218,24 @@ def sample_irrraf(net: BinaryPolymerNetwork, reactions: frozenset[int],
|
|
|
218
218
|
return current
|
|
219
219
|
|
|
220
220
|
|
|
221
|
-
def irrraf_census(net: BinaryPolymerNetwork, raf
|
|
221
|
+
def irrraf_census(net: BinaryPolymerNetwork, raf, n_samples: int,
|
|
222
222
|
rng, strict: bool = False) -> dict:
|
|
223
223
|
"""Sample irreducible RAFs and report how many distinct ones turn up.
|
|
224
224
|
|
|
225
|
+
`raf` may be a `RafResult` or a bare set of reactions -- pass one of the u-RAFs
|
|
226
|
+
from `rafkit.inhibition.max_urafs` to take a census under inhibition. That is
|
|
227
|
+
correct without further conditions because the uninhibited property is inherited
|
|
228
|
+
downward: every sub-RAF of a u-RAF is itself a u-RAF (Hordijk & Steel 2012), so
|
|
229
|
+
every core sampled from one is uninhibited too.
|
|
230
|
+
|
|
225
231
|
The count is the quantity of interest: it upper-bounds the number of
|
|
226
232
|
distinguishable lineages the chemistry can carry, so a census of 1 means there
|
|
227
233
|
is nothing to inherit and no ecology is possible regardless of the dynamics
|
|
228
234
|
later placed on top.
|
|
229
235
|
"""
|
|
236
|
+
if not isinstance(raf, RafResult):
|
|
237
|
+
raf = RafResult(reactions=frozenset(raf),
|
|
238
|
+
closure=_closure(net, frozenset(raf)), n_rounds=0)
|
|
230
239
|
if raf.is_empty:
|
|
231
240
|
return {"n_samples": 0, "n_distinct": 0, "sizes": [], "mean_size": float("nan"),
|
|
232
241
|
"mean_jaccard": float("nan"), "min_jaccard": float("nan"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rafkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Autocatalytic (RAF) sets in catalytic reaction networks: maximal RAFs, irreducible cores, and Kauffman binary polymer models.
|
|
5
5
|
Author: James P. Galasyn, Claude Théodore
|
|
6
6
|
License: MIT
|
|
@@ -116,6 +116,7 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
116
116
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
117
117
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
118
118
|
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
119
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
119
120
|
|
|
120
121
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
121
122
|
is subtly wrong produces plausible numbers rather than errors.
|
|
@@ -141,8 +142,37 @@ last two rows.
|
|
|
141
142
|
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
142
143
|
systems stay simple to write.
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
145
|
+
## Inhibition
|
|
146
|
+
|
|
147
|
+
A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
|
|
148
|
+
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
|
|
149
|
+
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
|
|
150
|
+
"the" maximal u-RAF.
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
Food: a, b
|
|
154
|
+
r1 : a + b [a] {d} => c # inhibited by d
|
|
155
|
+
r2 : a + b [b] {c} => d # inhibited by c
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
|
|
159
|
+
inhibitor, and their union is an RAF that fails the uninhibited condition.
|
|
160
|
+
|
|
161
|
+
`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
|
|
162
|
+
running network can **lose** a subRAF, not merely gain one. See
|
|
163
|
+
`examples/inhibition_dissolution.py`.
|
|
164
|
+
|
|
165
|
+
The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
|
|
166
|
+
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
|
|
167
|
+
is correct because the uninhibited property is inherited downward — every sub-RAF of a
|
|
168
|
+
u-RAF is a u-RAF.
|
|
169
|
+
|
|
170
|
+
Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
|
|
171
|
+
tractable in *k*, the number of inhibition classes — and **k is a property of how you
|
|
172
|
+
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
|
|
173
|
+
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
|
|
174
|
+
number of inhibited reactions, which is the difference between `2^k` being feasible
|
|
175
|
+
and not.
|
|
146
176
|
|
|
147
177
|
## Notes on irreducible cores
|
|
148
178
|
|
|
@@ -6,6 +6,7 @@ src/rafkit/binary_polymer.py
|
|
|
6
6
|
src/rafkit/catalysis.py
|
|
7
7
|
src/rafkit/crs.py
|
|
8
8
|
src/rafkit/gillespie.py
|
|
9
|
+
src/rafkit/inhibition.py
|
|
9
10
|
src/rafkit/network.py
|
|
10
11
|
src/rafkit/raf.py
|
|
11
12
|
src/rafkit.egg-info/PKG-INFO
|
|
@@ -15,6 +16,7 @@ src/rafkit.egg-info/requires.txt
|
|
|
15
16
|
src/rafkit.egg-info/top_level.txt
|
|
16
17
|
tests/test_crs.py
|
|
17
18
|
tests/test_gillespie.py
|
|
19
|
+
tests/test_inhibition.py
|
|
18
20
|
tests/test_published_examples.py
|
|
19
21
|
tests/test_raf.py
|
|
20
22
|
tests/test_seeding.py
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""Uninhibited RAFs — Hordijk & Steel (2012), Part II.
|
|
2
|
+
|
|
3
|
+
The load-bearing test here is `test_matches_brute_force`: for networks small enough to
|
|
4
|
+
enumerate every subset, the fixed-parameter algorithm is checked against a direct
|
|
5
|
+
search that shares no code with it. Everything else in this file is a property from the
|
|
6
|
+
paper, asserted rather than assumed.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from itertools import chain, combinations
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from rafkit import binary_polymer, max_raf, parse_crs
|
|
16
|
+
from rafkit.inhibition import (classes_from_inhibitors, is_uninhibited, is_uraf,
|
|
17
|
+
max_urafs, support)
|
|
18
|
+
from rafkit.raf import _refine
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _powerset(items):
|
|
22
|
+
items = list(items)
|
|
23
|
+
return chain.from_iterable(combinations(items, k) for k in range(len(items) + 1))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _brute_force_max_urafs(net, inhibition):
|
|
27
|
+
"""Every maximal u-RAF, by direct enumeration. Shares no code with max_urafs."""
|
|
28
|
+
urafs = [frozenset(s) for s in _powerset(range(net.n_reactions))
|
|
29
|
+
if s and is_uraf(net, s, inhibition)]
|
|
30
|
+
return {s for s in urafs if not any(s < t for t in urafs)}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestDefinition:
|
|
34
|
+
def test_support_is_reactants_and_products_only(self):
|
|
35
|
+
net = parse_crs("Food: a, b\nr1 : a + b [z] => c\n")
|
|
36
|
+
names = {net.molecules[m] for m in support(net, {0})}
|
|
37
|
+
assert names == {"a", "b", "c"} # z is a catalyst, not in the support
|
|
38
|
+
|
|
39
|
+
def test_inhibition_by_something_absent_from_the_support_is_harmless(self):
|
|
40
|
+
# z inhibits r1, but z is neither reactant nor product of r1, so (u-2) holds.
|
|
41
|
+
net = parse_crs("Food: a, b\nr1 : a + b [a] {z} => c\n")
|
|
42
|
+
assert is_uninhibited(net, {0}, classes_from_inhibitors(net))
|
|
43
|
+
assert is_uraf(net, {0}, classes_from_inhibitors(net))
|
|
44
|
+
|
|
45
|
+
def test_a_set_that_inhibits_its_own_reaction_is_not_a_uraf(self):
|
|
46
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
47
|
+
c = net.molecules.index("c")
|
|
48
|
+
inh = ((frozenset({c}), frozenset({0})),)
|
|
49
|
+
assert _refine(net, {0}) == {0} # it IS an RAF
|
|
50
|
+
assert not is_uraf(net, {0}, inh) # but not uninhibited
|
|
51
|
+
|
|
52
|
+
def test_raf_is_unaffected_by_inhibition(self):
|
|
53
|
+
"""The paper defines RAF without reference to inhibition; (u-2) is separate.
|
|
54
|
+
CatReNet instead filters inhibited reactions inside its maxRaf, so its result
|
|
55
|
+
differs from the definition here whenever inhibitors are present."""
|
|
56
|
+
net = parse_crs("Food: a, b\nr1 : a + b [a] => c\nr2 : a + c [a] => d\n")
|
|
57
|
+
assert len(max_raf(net).reactions) == 2
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestTheorem1:
|
|
61
|
+
def test_a_subset_of_a_uraf_that_is_a_raf_is_a_uraf(self):
|
|
62
|
+
"""Stated in the paper: (u-2) is inherited downward."""
|
|
63
|
+
net = parse_crs(
|
|
64
|
+
"Food: a, b\nr1 : a + b [a] => c\nr2 : a + c [a] => d\nr3 : a + d [a] => e\n")
|
|
65
|
+
e = net.molecules.index("e")
|
|
66
|
+
inh = ((frozenset({e}), frozenset({2})),)
|
|
67
|
+
whole = max_urafs(net, inh)
|
|
68
|
+
assert whole
|
|
69
|
+
for u in whole:
|
|
70
|
+
for s in _powerset(u):
|
|
71
|
+
s = frozenset(s)
|
|
72
|
+
if s and _refine(net, s) == s:
|
|
73
|
+
assert is_uraf(net, s, inh)
|
|
74
|
+
|
|
75
|
+
# Seeds probed to give a maximal RAF of 4-12 reactions with >=2 produced
|
|
76
|
+
# molecules, so exhaustive enumeration is feasible and the constraint bites.
|
|
77
|
+
@pytest.mark.parametrize("seed", [1, 5, 7, 12, 13, 15, 16])
|
|
78
|
+
def test_matches_brute_force(self, seed):
|
|
79
|
+
"""The fixed-parameter algorithm against direct enumeration of every subset.
|
|
80
|
+
|
|
81
|
+
The two computations share no code: one walks subsets of [k] and calls the
|
|
82
|
+
maximal-RAF fixpoint, the other tests every subset of the RAF directly.
|
|
83
|
+
"""
|
|
84
|
+
net = binary_polymer(max_len=4, food_len=2, p=0.01,
|
|
85
|
+
rng=np.random.default_rng(seed), cleavage=True)
|
|
86
|
+
raf = sorted(max_raf(net).reactions)
|
|
87
|
+
assert 3 <= len(raf) <= 12, "seed no longer yields an enumerable RAF"
|
|
88
|
+
|
|
89
|
+
# Inhibit reactions by molecules the set actually makes, so (u-2) can fail.
|
|
90
|
+
produced = sorted(support(net, raf) - net.food)
|
|
91
|
+
assert len(produced) >= 2
|
|
92
|
+
inh = tuple((frozenset({produced[i]}), frozenset({raf[i]}))
|
|
93
|
+
for i in range(2))
|
|
94
|
+
|
|
95
|
+
assert set(max_urafs(net, inh, reactions=raf)) == \
|
|
96
|
+
_brute_force_max_urafs_on(net, raf, inh)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _brute_force_max_urafs_on(net, allowed, inhibition):
|
|
100
|
+
urafs = [frozenset(s) for s in _powerset(allowed)
|
|
101
|
+
if s and is_uraf(net, s, inhibition)]
|
|
102
|
+
return {s for s in urafs if not any(s < t for t in urafs)}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestCollectionSemantics:
|
|
106
|
+
def test_there_can_be_more_than_one_maximal_uraf(self):
|
|
107
|
+
"""The structural break: no unique maximum, so the API returns a collection.
|
|
108
|
+
|
|
109
|
+
Two independent always-on reactions, each inhibited by the other's product.
|
|
110
|
+
Neither can coexist with the other, and neither is canonical.
|
|
111
|
+
"""
|
|
112
|
+
net = parse_crs(
|
|
113
|
+
"Food: a, b\nr1 : a + b [a] => c\nr2 : a + b [b] => d\n")
|
|
114
|
+
c, d = net.molecules.index("c"), net.molecules.index("d")
|
|
115
|
+
inh = ((frozenset({c}), frozenset({1})), (frozenset({d}), frozenset({0})))
|
|
116
|
+
got = {frozenset(net.names[r] for r in u) for u in max_urafs(net, inh)}
|
|
117
|
+
assert got == {frozenset({"r1"}), frozenset({"r2"})}
|
|
118
|
+
|
|
119
|
+
def test_no_inhibition_gives_back_the_maximal_raf(self):
|
|
120
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
121
|
+
assert max_urafs(net, ()) == (max_raf(net).reactions,)
|
|
122
|
+
|
|
123
|
+
def test_no_uraf_returns_an_empty_collection(self):
|
|
124
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
125
|
+
c = net.molecules.index("c")
|
|
126
|
+
assert max_urafs(net, ((frozenset({c}), frozenset({0})),)) == ()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class TestClassEncoding:
|
|
130
|
+
def test_grouping_is_by_inhibiting_molecule_not_by_reaction(self):
|
|
131
|
+
"""k is the cost, and it is a property of the encoding. Grouping by molecule
|
|
132
|
+
keeps k at the number of distinct inhibitors rather than of inhibited
|
|
133
|
+
reactions, which is the difference between 2^k feasible and not."""
|
|
134
|
+
net = parse_crs(
|
|
135
|
+
"Food: a, b\nr1 : a + b [a] {z} => c\nr2 : a + b [b] {z} => d\n")
|
|
136
|
+
classes = classes_from_inhibitors(net)
|
|
137
|
+
assert len(classes) == 1 # one inhibitor, not two reactions
|
|
138
|
+
(X, R), = classes
|
|
139
|
+
assert {net.molecules[x] for x in X} == {"z"}
|
|
140
|
+
assert R == frozenset({0, 1})
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class TestDivergenceFromCatReNet:
|
|
144
|
+
"""CatReNet computes something different once inhibitors are present.
|
|
145
|
+
|
|
146
|
+
Its `maxRaf` filters inhibited reactions *during* the RAF computation, where
|
|
147
|
+
Hordijk & Steel (2012) define an RAF without reference to inhibition and add (u-2)
|
|
148
|
+
as a separate condition on u-RAFs. On the network below CatReNet's `maxRaf` and
|
|
149
|
+
`uRaf` both return nothing, while two maximal u-RAFs exist by the definition.
|
|
150
|
+
|
|
151
|
+
Verified by hand rather than asserted: `{r1}` is an RAF, its support is {a,b,c},
|
|
152
|
+
and the only class inhibiting r1 is {d} — so (u-2) holds. Likewise `{r2}`. Their
|
|
153
|
+
union is an RAF but fails (u-2), which is why neither can be extended.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
NET = "Food: a, b\nr1 : a + b [a] {d} => c\nr2 : a + b [b] {c} => d\n"
|
|
157
|
+
|
|
158
|
+
def test_two_maximal_urafs_exist_where_catrenet_reports_none(self):
|
|
159
|
+
net = parse_crs(self.NET)
|
|
160
|
+
got = {frozenset(net.names[r] for r in u) for u in max_urafs(net)}
|
|
161
|
+
assert got == {frozenset({"r1"}), frozenset({"r2"})}
|
|
162
|
+
|
|
163
|
+
def test_each_is_a_raf_satisfying_u2_but_their_union_is_not(self):
|
|
164
|
+
net = parse_crs(self.NET)
|
|
165
|
+
cls = classes_from_inhibitors(net)
|
|
166
|
+
assert is_uraf(net, {0}, cls) and is_uraf(net, {1}, cls)
|
|
167
|
+
assert _refine(net, {0, 1}) == {0, 1} # the union IS an RAF
|
|
168
|
+
assert not is_uninhibited(net, {0, 1}, cls) # but violates (u-2)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TestDynamics:
|
|
172
|
+
"""Inhibition in the simulator, which is what lets a network *lose* a subRAF."""
|
|
173
|
+
|
|
174
|
+
def test_an_inhibited_reaction_has_zero_propensity(self):
|
|
175
|
+
from rafkit.gillespie import propensities
|
|
176
|
+
net = parse_crs("Food: a, b\nr1 : a + b [a] {z} => c\n")
|
|
177
|
+
counts = np.zeros(net.n_molecules, dtype=np.int64)
|
|
178
|
+
for m in net.food:
|
|
179
|
+
counts[m] = 5
|
|
180
|
+
assert propensities(net, counts)[0] > 0 # z absent: runs
|
|
181
|
+
counts[net.molecules.index("z")] = 1
|
|
182
|
+
assert propensities(net, counts)[0] == 0.0 # z present: blocked outright
|
|
183
|
+
|
|
184
|
+
def test_inhibition_blocks_rather_than_slows(self):
|
|
185
|
+
"""Distinct from the uncatalysed case, which merely runs at a reduced rate."""
|
|
186
|
+
from rafkit.gillespie import propensities
|
|
187
|
+
net = parse_crs("Food: a, b\nr1 : a + b [q] {z} => c\n")
|
|
188
|
+
counts = np.zeros(net.n_molecules, dtype=np.int64)
|
|
189
|
+
for m in net.food:
|
|
190
|
+
counts[m] = 5
|
|
191
|
+
uncatalysed = propensities(net, counts)[0]
|
|
192
|
+
assert uncatalysed > 0 # no catalyst, still proceeds
|
|
193
|
+
counts[net.molecules.index("z")] = 1
|
|
194
|
+
assert propensities(net, counts)[0] == 0.0
|
|
195
|
+
|
|
196
|
+
def test_a_subraf_stops_producing_once_its_inhibitor_appears(self):
|
|
197
|
+
"""Dissolution. r2 runs as soon as c exists; r3 is self-catalysed so its
|
|
198
|
+
product e arrives only after a rare uncatalysed event, and blocks r2."""
|
|
199
|
+
from rafkit import simulate
|
|
200
|
+
net = parse_crs(
|
|
201
|
+
"Food: a, b\nr1 : a + b [a] => c\nr2 : a + c [c] {e} => d\n"
|
|
202
|
+
"r3 : a + b [e] => e\n")
|
|
203
|
+
tr = simulate(net, n_events=4000, rng=np.random.default_rng(2), sample_every=10)
|
|
204
|
+
t_e = tr.first_seen("e")
|
|
205
|
+
assert t_e is not None, "the inhibitor never appeared; test is vacuous"
|
|
206
|
+
|
|
207
|
+
d = tr.of("d").astype(float)
|
|
208
|
+
i = int(np.searchsorted(tr.times, t_e))
|
|
209
|
+
assert d[i] > 0, "d never accumulated before the block; test is vacuous"
|
|
210
|
+
assert d[-1] == d[i], "d kept growing after its reaction was inhibited"
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class TestDownstreamUnderInhibition:
|
|
214
|
+
"""The set-theoretic tools need no change: pass a u-RAF and they stay correct,
|
|
215
|
+
because the uninhibited property is inherited downward."""
|
|
216
|
+
|
|
217
|
+
def _setup(self, seed=7):
|
|
218
|
+
net = binary_polymer(max_len=4, food_len=2, p=0.01,
|
|
219
|
+
rng=np.random.default_rng(seed), cleavage=True)
|
|
220
|
+
raf = sorted(max_raf(net).reactions)
|
|
221
|
+
produced = sorted(support(net, raf) - net.food)
|
|
222
|
+
inh = tuple((frozenset({produced[i]}), frozenset({raf[i]})) for i in range(2))
|
|
223
|
+
return net, raf, inh
|
|
224
|
+
|
|
225
|
+
def test_cores_sampled_from_a_uraf_are_themselves_urafs(self):
|
|
226
|
+
from rafkit import sample_irrraf
|
|
227
|
+
net, raf, inh = self._setup()
|
|
228
|
+
urafs = max_urafs(net, inh, reactions=raf)
|
|
229
|
+
assert urafs
|
|
230
|
+
for u in urafs:
|
|
231
|
+
for i in range(10):
|
|
232
|
+
assert is_uraf(net, sample_irrraf(net, u, np.random.default_rng(i)), inh)
|
|
233
|
+
|
|
234
|
+
def test_census_accepts_a_bare_reaction_set(self):
|
|
235
|
+
from rafkit import irrraf_census
|
|
236
|
+
net, raf, inh = self._setup()
|
|
237
|
+
u = max_urafs(net, inh, reactions=raf)[0]
|
|
238
|
+
census = irrraf_census(net, u, n_samples=5, rng=np.random.default_rng(0))
|
|
239
|
+
assert census["n_distinct"] >= 1
|
|
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
|