rafkit 0.2.0__tar.gz → 0.4.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.4.0}/PKG-INFO +54 -3
- {rafkit-0.2.0 → rafkit-0.4.0}/README.md +53 -2
- {rafkit-0.2.0 → rafkit-0.4.0}/pyproject.toml +1 -1
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/__init__.py +6 -1
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/crs.py +25 -8
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/gillespie.py +10 -0
- rafkit-0.4.0/src/rafkit/inhibition.py +114 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/network.py +12 -0
- rafkit-0.4.0/src/rafkit/pnml.py +139 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/raf.py +10 -1
- {rafkit-0.2.0 → rafkit-0.4.0/src/rafkit.egg-info}/PKG-INFO +54 -3
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit.egg-info/SOURCES.txt +4 -0
- rafkit-0.4.0/tests/test_inhibition.py +239 -0
- rafkit-0.4.0/tests/test_pnml.py +124 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/LICENSE +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/setup.cfg +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/binary_polymer.py +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit/catalysis.py +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit.egg-info/dependency_links.txt +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit.egg-info/requires.txt +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/src/rafkit.egg-info/top_level.txt +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/tests/test_crs.py +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/tests/test_gillespie.py +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/tests/test_published_examples.py +0 -0
- {rafkit-0.2.0 → rafkit-0.4.0}/tests/test_raf.py +0 -0
- {rafkit-0.2.0 → rafkit-0.4.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.4.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
|
|
@@ -115,11 +115,33 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
115
115
|
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
116
116
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
117
117
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
118
|
+
| `to_pnml` / `write_pnml` | PNML export (ISO/IEC 15909-2) for the Petri net ecosystem |
|
|
118
119
|
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
120
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
119
121
|
|
|
120
122
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
121
123
|
is subtly wrong produces plausible numbers rather than errors.
|
|
122
124
|
|
|
125
|
+
## A reaction network is a Petri net
|
|
126
|
+
|
|
127
|
+
Species are places, reactions are transitions, molecule counts are tokens. `write_pnml`
|
|
128
|
+
exports to PNML (ISO/IEC 15909-2), so these networks open in Petri net editors, model
|
|
129
|
+
checkers, and the unfolding tools that compute the causal structure of a run.
|
|
130
|
+
|
|
131
|
+
Three things need care, and each is explicit rather than silent:
|
|
132
|
+
|
|
133
|
+
- **Catalysis becomes a self-loop.** P/T nets have no read arc, so a catalyst is a pair
|
|
134
|
+
of arcs, consuming the token and returning it.
|
|
135
|
+
- **Alternative catalyst sets become separate transitions**, named `r1`, `r1#2`, …, since
|
|
136
|
+
a transition's preset is a conjunction and cannot express "either set".
|
|
137
|
+
- **Food gets source transitions**, because RAF food is inexhaustible and no initial
|
|
138
|
+
marking expresses that — a marking of *n* deadlocks after *n* uses.
|
|
139
|
+
|
|
140
|
+
Inhibition has no `ptnet` representation and is written as a `toolspecific` annotation,
|
|
141
|
+
with a warning in the file: a reader that ignores it gets a *different system*.
|
|
142
|
+
Reactions requiring a catalyst that nothing provides are omitted and counted, since
|
|
143
|
+
emitting them unconstrained would make them freely fireable — the opposite of the intent.
|
|
144
|
+
|
|
123
145
|
## Catalysis is a relation, not a list
|
|
124
146
|
|
|
125
147
|
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
@@ -141,8 +163,37 @@ last two rows.
|
|
|
141
163
|
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
142
164
|
systems stay simple to write.
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
## Inhibition
|
|
167
|
+
|
|
168
|
+
A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
|
|
169
|
+
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
|
|
170
|
+
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
|
|
171
|
+
"the" maximal u-RAF.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Food: a, b
|
|
175
|
+
r1 : a + b [a] {d} => c # inhibited by d
|
|
176
|
+
r2 : a + b [b] {c} => d # inhibited by c
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
|
|
180
|
+
inhibitor, and their union is an RAF that fails the uninhibited condition.
|
|
181
|
+
|
|
182
|
+
`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
|
|
183
|
+
running network can **lose** a subRAF, not merely gain one. See
|
|
184
|
+
`examples/inhibition_dissolution.py`.
|
|
185
|
+
|
|
186
|
+
The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
|
|
187
|
+
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
|
|
188
|
+
is correct because the uninhibited property is inherited downward — every sub-RAF of a
|
|
189
|
+
u-RAF is a u-RAF.
|
|
190
|
+
|
|
191
|
+
Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
|
|
192
|
+
tractable in *k*, the number of inhibition classes — and **k is a property of how you
|
|
193
|
+
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
|
|
194
|
+
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
|
|
195
|
+
number of inhibited reactions, which is the difference between `2^k` being feasible
|
|
196
|
+
and not.
|
|
146
197
|
|
|
147
198
|
## Notes on irreducible cores
|
|
148
199
|
|
|
@@ -87,11 +87,33 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
87
87
|
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
88
88
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
89
89
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
90
|
+
| `to_pnml` / `write_pnml` | PNML export (ISO/IEC 15909-2) for the Petri net ecosystem |
|
|
90
91
|
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
92
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
91
93
|
|
|
92
94
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
93
95
|
is subtly wrong produces plausible numbers rather than errors.
|
|
94
96
|
|
|
97
|
+
## A reaction network is a Petri net
|
|
98
|
+
|
|
99
|
+
Species are places, reactions are transitions, molecule counts are tokens. `write_pnml`
|
|
100
|
+
exports to PNML (ISO/IEC 15909-2), so these networks open in Petri net editors, model
|
|
101
|
+
checkers, and the unfolding tools that compute the causal structure of a run.
|
|
102
|
+
|
|
103
|
+
Three things need care, and each is explicit rather than silent:
|
|
104
|
+
|
|
105
|
+
- **Catalysis becomes a self-loop.** P/T nets have no read arc, so a catalyst is a pair
|
|
106
|
+
of arcs, consuming the token and returning it.
|
|
107
|
+
- **Alternative catalyst sets become separate transitions**, named `r1`, `r1#2`, …, since
|
|
108
|
+
a transition's preset is a conjunction and cannot express "either set".
|
|
109
|
+
- **Food gets source transitions**, because RAF food is inexhaustible and no initial
|
|
110
|
+
marking expresses that — a marking of *n* deadlocks after *n* uses.
|
|
111
|
+
|
|
112
|
+
Inhibition has no `ptnet` representation and is written as a `toolspecific` annotation,
|
|
113
|
+
with a warning in the file: a reader that ignores it gets a *different system*.
|
|
114
|
+
Reactions requiring a catalyst that nothing provides are omitted and counted, since
|
|
115
|
+
emitting them unconstrained would make them freely fireable — the opposite of the intent.
|
|
116
|
+
|
|
95
117
|
## Catalysis is a relation, not a list
|
|
96
118
|
|
|
97
119
|
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
@@ -113,8 +135,37 @@ last two rows.
|
|
|
113
135
|
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
114
136
|
systems stay simple to write.
|
|
115
137
|
|
|
116
|
-
|
|
117
|
-
|
|
138
|
+
## Inhibition
|
|
139
|
+
|
|
140
|
+
A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
|
|
141
|
+
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
|
|
142
|
+
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
|
|
143
|
+
"the" maximal u-RAF.
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
Food: a, b
|
|
147
|
+
r1 : a + b [a] {d} => c # inhibited by d
|
|
148
|
+
r2 : a + b [b] {c} => d # inhibited by c
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
|
|
152
|
+
inhibitor, and their union is an RAF that fails the uninhibited condition.
|
|
153
|
+
|
|
154
|
+
`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
|
|
155
|
+
running network can **lose** a subRAF, not merely gain one. See
|
|
156
|
+
`examples/inhibition_dissolution.py`.
|
|
157
|
+
|
|
158
|
+
The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
|
|
159
|
+
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
|
|
160
|
+
is correct because the uninhibited property is inherited downward — every sub-RAF of a
|
|
161
|
+
u-RAF is a u-RAF.
|
|
162
|
+
|
|
163
|
+
Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
|
|
164
|
+
tractable in *k*, the number of inhibition classes — and **k is a property of how you
|
|
165
|
+
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
|
|
166
|
+
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
|
|
167
|
+
number of inhibited reactions, which is the difference between `2^k` being feasible
|
|
168
|
+
and not.
|
|
118
169
|
|
|
119
170
|
## Notes on irreducible cores
|
|
120
171
|
|
|
@@ -16,14 +16,17 @@ 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
|
|
22
|
+
from rafkit.pnml import to_pnml, write_pnml
|
|
20
23
|
from rafkit.raf import (
|
|
21
24
|
RafResult, catrenet_strictly_autocatalytic, core_raf, exploitability,
|
|
22
25
|
has_unique_irraf, irrraf_census, is_food_catalysed, max_raf, max_raf_strict,
|
|
23
26
|
sample_irrraf,
|
|
24
27
|
)
|
|
25
28
|
|
|
26
|
-
__version__ = "0.
|
|
29
|
+
__version__ = "0.4.0"
|
|
27
30
|
|
|
28
31
|
__all__ = [
|
|
29
32
|
"BinaryPolymerNetwork", "binary_polymer", "ReactionNetwork",
|
|
@@ -32,5 +35,7 @@ __all__ = [
|
|
|
32
35
|
"core_raf", "has_unique_irraf",
|
|
33
36
|
"is_catalysed", "catalysing_molecules", "normalise",
|
|
34
37
|
"parse_crs", "read_crs", "to_crs", "write_crs",
|
|
38
|
+
"to_pnml", "write_pnml",
|
|
35
39
|
"simulate", "propensities", "Trajectory",
|
|
40
|
+
"max_urafs", "is_uraf", "is_uninhibited", "support", "classes_from_inhibitors",
|
|
36
41
|
]
|
|
@@ -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))))
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Export to PNML, the ISO/IEC 15909-2 Petri net interchange format.
|
|
2
|
+
|
|
3
|
+
A catalytic reaction network *is* a Petri net: species are places, reactions are
|
|
4
|
+
transitions, molecule counts are tokens. Exporting makes these networks readable by the
|
|
5
|
+
Petri net ecosystem -- editors, model checkers, and the unfolding tools that compute the
|
|
6
|
+
causal DAG of a run.
|
|
7
|
+
|
|
8
|
+
Three things in the RAF model have no direct Place/Transition equivalent, and each is
|
|
9
|
+
handled explicitly rather than silently:
|
|
10
|
+
|
|
11
|
+
**Catalysis becomes a self-loop.** P/T nets have no read arc, so a catalyst is written
|
|
12
|
+
as a pair of arcs, place -> transition -> place: the token is consumed and immediately
|
|
13
|
+
returned, which is behaviourally what a catalyst does.
|
|
14
|
+
|
|
15
|
+
**Alternative catalyst sets become separate transitions.** A transition's preset is a
|
|
16
|
+
conjunction, so it cannot express "either of these sets". A reaction with `k` catalyst
|
|
17
|
+
sets is emitted as `k` transitions sharing reactants and products, each with self-loops
|
|
18
|
+
for one set. They are named ``r1``, ``r1#2``, ``r1#3`` … so the grouping survives.
|
|
19
|
+
|
|
20
|
+
**Food becomes a source transition.** RAF food is inexhaustible, which no initial
|
|
21
|
+
marking expresses: a marking of *n* deadlocks after *n* uses. Each food place therefore
|
|
22
|
+
gets a source transition with an empty preset, which can always fire.
|
|
23
|
+
|
|
24
|
+
Not expressible, and reported rather than dropped:
|
|
25
|
+
|
|
26
|
+
* **Inhibition** has no standard P/T representation. Inhibitor arcs exist in extended
|
|
27
|
+
formalisms but not in the ``ptnet`` grammar, so they are written as a
|
|
28
|
+
``<toolspecific>`` annotation. A tool that ignores it will read a net **without** the
|
|
29
|
+
inhibition, which is a *different system* -- so `to_pnml` says so in a comment.
|
|
30
|
+
* **χ = ∅** ("must be catalysed, and nothing does") is a RAF-theoretic condition, not a
|
|
31
|
+
Petri net one. Such reactions can never fire in any RAF, and emitting them as
|
|
32
|
+
unconstrained transitions would make them freely fireable -- the opposite. They are
|
|
33
|
+
omitted, and counted in the header comment.
|
|
34
|
+
"""
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import xml.etree.ElementTree as ET
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
PNML_NS = "http://www.pnml.org/version-2009/grammar/pnml"
|
|
41
|
+
PTNET = "http://www.pnml.org/version-2009/grammar/ptnet"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _el(parent, tag, **attrs):
|
|
45
|
+
return ET.SubElement(parent, tag, {k: str(v) for k, v in attrs.items()})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _named(parent, text):
|
|
49
|
+
name = _el(parent, "name")
|
|
50
|
+
_el(name, "text").text = text
|
|
51
|
+
return name
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def to_pnml(net, net_id: str = "rafkit", food_sources: bool = True) -> str:
|
|
55
|
+
"""Serialise a network to PNML text.
|
|
56
|
+
|
|
57
|
+
`food_sources` adds a source transition per food place so that food is
|
|
58
|
+
inexhaustible, matching RAF semantics. Turn it off to get a net whose food is
|
|
59
|
+
limited to its initial marking -- which is a different system, and will deadlock.
|
|
60
|
+
"""
|
|
61
|
+
inhibitors = getattr(net, "inhibitors", ()) or ((),) * net.n_reactions
|
|
62
|
+
names = getattr(net, "names", None) or [f"r{i + 1}" for i in range(net.n_reactions)]
|
|
63
|
+
|
|
64
|
+
root = ET.Element("pnml", {"xmlns": PNML_NS})
|
|
65
|
+
pnet = _el(root, "net", id=net_id, type=PTNET)
|
|
66
|
+
_named(pnet, net_id)
|
|
67
|
+
page = _el(pnet, "page", id="page1")
|
|
68
|
+
|
|
69
|
+
for m, mol in enumerate(net.molecules):
|
|
70
|
+
place = _el(page, "place", id=f"p{m}")
|
|
71
|
+
_named(place, mol)
|
|
72
|
+
marking = _el(place, "initialMarking")
|
|
73
|
+
_el(marking, "text").text = "1" if m in net.food else "0"
|
|
74
|
+
|
|
75
|
+
arc_id = 0
|
|
76
|
+
|
|
77
|
+
def arc(src, dst):
|
|
78
|
+
nonlocal arc_id
|
|
79
|
+
arc_id += 1
|
|
80
|
+
a = _el(page, "arc", id=f"a{arc_id}", source=src, target=dst)
|
|
81
|
+
insc = _el(a, "inscription")
|
|
82
|
+
_el(insc, "text").text = "1"
|
|
83
|
+
|
|
84
|
+
skipped = 0
|
|
85
|
+
for r in range(net.n_reactions):
|
|
86
|
+
chi = net.catalysts[r]
|
|
87
|
+
if not chi:
|
|
88
|
+
skipped += 1 # must be catalysed, nothing does: cannot ever fire
|
|
89
|
+
continue
|
|
90
|
+
for k, catalyst_set in enumerate(sorted(chi, key=lambda u: sorted(u))):
|
|
91
|
+
tid = f"t{r}" if k == 0 else f"t{r}_{k}"
|
|
92
|
+
label = names[r] if k == 0 else f"{names[r]}#{k + 1}"
|
|
93
|
+
trans = _el(page, "transition", id=tid)
|
|
94
|
+
_named(trans, label)
|
|
95
|
+
if inhibitors[r]:
|
|
96
|
+
ts = _el(trans, "toolspecific", tool="rafkit", version="1")
|
|
97
|
+
_el(ts, "inhibitors").text = " ".join(
|
|
98
|
+
sorted(net.molecules[x] for x in inhibitors[r]))
|
|
99
|
+
for x in net.reactants(r):
|
|
100
|
+
arc(f"p{x}", tid)
|
|
101
|
+
for x in net.products(r):
|
|
102
|
+
arc(tid, f"p{x}")
|
|
103
|
+
for c in catalyst_set: # read arc, as a self-loop
|
|
104
|
+
arc(f"p{c}", tid)
|
|
105
|
+
arc(tid, f"p{c}")
|
|
106
|
+
|
|
107
|
+
if food_sources:
|
|
108
|
+
for m in sorted(net.food):
|
|
109
|
+
tid = f"src{m}"
|
|
110
|
+
trans = _el(page, "transition", id=tid)
|
|
111
|
+
_named(trans, f"source:{net.molecules[m]}")
|
|
112
|
+
arc(tid, f"p{m}")
|
|
113
|
+
|
|
114
|
+
ET.indent(root, space=" ")
|
|
115
|
+
body = ET.tostring(root, encoding="unicode")
|
|
116
|
+
notes = [f"Generated by rafkit. {net.n_molecules} species, "
|
|
117
|
+
f"{net.n_reactions} reactions."]
|
|
118
|
+
if skipped:
|
|
119
|
+
notes.append(f"{skipped} reaction(s) omitted: they require a catalyst and "
|
|
120
|
+
f"nothing catalyses them, so they can never fire.")
|
|
121
|
+
if any(inhibitors):
|
|
122
|
+
notes.append("Inhibition is recorded in a toolspecific element only; the "
|
|
123
|
+
"ptnet grammar has no inhibitor arc. A tool that ignores it "
|
|
124
|
+
"reads a DIFFERENT system, without the inhibition.")
|
|
125
|
+
if food_sources:
|
|
126
|
+
notes.append("Food places have source transitions, so food is inexhaustible.")
|
|
127
|
+
# A comment may not contain "--" anywhere, nor end with "-". Sanitising here
|
|
128
|
+
# rather than trusting the call sites: this text is prepended as raw XML, so it
|
|
129
|
+
# bypasses ElementTree's escaping entirely, and an unescaped double hyphen makes
|
|
130
|
+
# the whole document unparseable. Found by an independent PNML reader, not by us.
|
|
131
|
+
safe = [n.replace("--", "\u2014").rstrip("-") for n in notes]
|
|
132
|
+
header = "<?xml version='1.0' encoding='UTF-8'?>\n<!--\n " + \
|
|
133
|
+
"\n ".join(safe) + "\n-->\n"
|
|
134
|
+
return header + body + "\n"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def write_pnml(net, path: str | Path, **kwargs) -> None:
|
|
138
|
+
"""Write a network to a PNML file."""
|
|
139
|
+
Path(path).write_text(to_pnml(net, **kwargs))
|
|
@@ -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.4.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
|
|
@@ -115,11 +115,33 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
115
115
|
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
116
116
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
117
117
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
118
|
+
| `to_pnml` / `write_pnml` | PNML export (ISO/IEC 15909-2) for the Petri net ecosystem |
|
|
118
119
|
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
120
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
119
121
|
|
|
120
122
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
121
123
|
is subtly wrong produces plausible numbers rather than errors.
|
|
122
124
|
|
|
125
|
+
## A reaction network is a Petri net
|
|
126
|
+
|
|
127
|
+
Species are places, reactions are transitions, molecule counts are tokens. `write_pnml`
|
|
128
|
+
exports to PNML (ISO/IEC 15909-2), so these networks open in Petri net editors, model
|
|
129
|
+
checkers, and the unfolding tools that compute the causal structure of a run.
|
|
130
|
+
|
|
131
|
+
Three things need care, and each is explicit rather than silent:
|
|
132
|
+
|
|
133
|
+
- **Catalysis becomes a self-loop.** P/T nets have no read arc, so a catalyst is a pair
|
|
134
|
+
of arcs, consuming the token and returning it.
|
|
135
|
+
- **Alternative catalyst sets become separate transitions**, named `r1`, `r1#2`, …, since
|
|
136
|
+
a transition's preset is a conjunction and cannot express "either set".
|
|
137
|
+
- **Food gets source transitions**, because RAF food is inexhaustible and no initial
|
|
138
|
+
marking expresses that — a marking of *n* deadlocks after *n* uses.
|
|
139
|
+
|
|
140
|
+
Inhibition has no `ptnet` representation and is written as a `toolspecific` annotation,
|
|
141
|
+
with a warning in the file: a reader that ignores it gets a *different system*.
|
|
142
|
+
Reactions requiring a catalyst that nothing provides are omitted and counted, since
|
|
143
|
+
emitting them unconstrained would make them freely fireable — the opposite of the intent.
|
|
144
|
+
|
|
123
145
|
## Catalysis is a relation, not a list
|
|
124
146
|
|
|
125
147
|
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
@@ -141,8 +163,37 @@ last two rows.
|
|
|
141
163
|
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
142
164
|
systems stay simple to write.
|
|
143
165
|
|
|
144
|
-
|
|
145
|
-
|
|
166
|
+
## Inhibition
|
|
167
|
+
|
|
168
|
+
A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
|
|
169
|
+
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
|
|
170
|
+
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
|
|
171
|
+
"the" maximal u-RAF.
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
Food: a, b
|
|
175
|
+
r1 : a + b [a] {d} => c # inhibited by d
|
|
176
|
+
r2 : a + b [b] {c} => d # inhibited by c
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
|
|
180
|
+
inhibitor, and their union is an RAF that fails the uninhibited condition.
|
|
181
|
+
|
|
182
|
+
`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
|
|
183
|
+
running network can **lose** a subRAF, not merely gain one. See
|
|
184
|
+
`examples/inhibition_dissolution.py`.
|
|
185
|
+
|
|
186
|
+
The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
|
|
187
|
+
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
|
|
188
|
+
is correct because the uninhibited property is inherited downward — every sub-RAF of a
|
|
189
|
+
u-RAF is a u-RAF.
|
|
190
|
+
|
|
191
|
+
Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
|
|
192
|
+
tractable in *k*, the number of inhibition classes — and **k is a property of how you
|
|
193
|
+
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
|
|
194
|
+
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
|
|
195
|
+
number of inhibited reactions, which is the difference between `2^k` being feasible
|
|
196
|
+
and not.
|
|
146
197
|
|
|
147
198
|
## Notes on irreducible cores
|
|
148
199
|
|
|
@@ -6,7 +6,9 @@ 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
|
|
11
|
+
src/rafkit/pnml.py
|
|
10
12
|
src/rafkit/raf.py
|
|
11
13
|
src/rafkit.egg-info/PKG-INFO
|
|
12
14
|
src/rafkit.egg-info/SOURCES.txt
|
|
@@ -15,6 +17,8 @@ src/rafkit.egg-info/requires.txt
|
|
|
15
17
|
src/rafkit.egg-info/top_level.txt
|
|
16
18
|
tests/test_crs.py
|
|
17
19
|
tests/test_gillespie.py
|
|
20
|
+
tests/test_inhibition.py
|
|
21
|
+
tests/test_pnml.py
|
|
18
22
|
tests/test_published_examples.py
|
|
19
23
|
tests/test_raf.py
|
|
20
24
|
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
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""PNML export.
|
|
2
|
+
|
|
3
|
+
A catalytic reaction network is a Petri net, and exporting one makes it readable by
|
|
4
|
+
that ecosystem. Three parts of the RAF model have no direct Place/Transition
|
|
5
|
+
equivalent, and the tests that matter are the ones checking each is handled explicitly:
|
|
6
|
+
catalysts become self-loops, alternative catalyst sets become separate transitions, and
|
|
7
|
+
food gets source transitions so it cannot run out.
|
|
8
|
+
|
|
9
|
+
Validated during development against **pm4py**, an independent PNML reader, which is
|
|
10
|
+
how the double-hyphen bug below was found. pm4py is AGPL and is deliberately not a
|
|
11
|
+
dependency; these tests use the standard library only.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
from rafkit import parse_crs
|
|
20
|
+
from rafkit.pnml import to_pnml, write_pnml
|
|
21
|
+
|
|
22
|
+
NS = {"p": "http://www.pnml.org/version-2009/grammar/pnml"}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _parse(net, **kw):
|
|
26
|
+
return ET.fromstring(to_pnml(net, **kw))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _ids(root, tag):
|
|
30
|
+
return [e.get("id") for e in root.iterfind(f".//p:{tag}", NS)]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _labels(root, tag):
|
|
34
|
+
out = []
|
|
35
|
+
for e in root.iterfind(f".//p:{tag}", NS):
|
|
36
|
+
t = e.find("p:name/p:text", NS)
|
|
37
|
+
out.append(t.text if t is not None else None)
|
|
38
|
+
return out
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestWellFormedness:
|
|
42
|
+
def test_output_parses(self):
|
|
43
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
44
|
+
assert _parse(net).tag.endswith("pnml")
|
|
45
|
+
|
|
46
|
+
def test_header_comment_has_no_double_hyphen(self):
|
|
47
|
+
"""Regression. The header is prepended as raw XML, so it bypasses
|
|
48
|
+
ElementTree's escaping entirely, and `--` inside a comment makes the whole
|
|
49
|
+
document unparseable. An independent PNML reader caught this; the export
|
|
50
|
+
itself was silent."""
|
|
51
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] {z} => c\nr2 : a [] => q\n")
|
|
52
|
+
text = to_pnml(net)
|
|
53
|
+
header = text[:text.index("<pnml")]
|
|
54
|
+
assert "--" not in header.replace("<!--", "").replace("-->", "")
|
|
55
|
+
ET.fromstring(text) # would raise if it were not well-formed
|
|
56
|
+
|
|
57
|
+
def test_every_arc_endpoint_exists(self):
|
|
58
|
+
net = parse_crs("Food: a, b\nr1 : a + b [{c,d},e] => c\nr2 : a + c [c] => d\n")
|
|
59
|
+
root = _parse(net)
|
|
60
|
+
nodes = set(_ids(root, "place")) | set(_ids(root, "transition"))
|
|
61
|
+
for arc in root.iterfind(".//p:arc", NS):
|
|
62
|
+
assert arc.get("source") in nodes
|
|
63
|
+
assert arc.get("target") in nodes
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestMapping:
|
|
67
|
+
def test_places_are_molecules(self):
|
|
68
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
69
|
+
assert set(_labels(_parse(net), "place")) == set(net.molecules)
|
|
70
|
+
|
|
71
|
+
def test_alternative_catalyst_sets_become_separate_transitions(self):
|
|
72
|
+
"""A transition's preset is a conjunction, so it cannot express "either set"."""
|
|
73
|
+
net = parse_crs("Food: a, b\nr1 : a + b [{c,d},e] => c\n")
|
|
74
|
+
labels = [l for l in _labels(_parse(net), "transition")
|
|
75
|
+
if not l.startswith("source:")]
|
|
76
|
+
assert sorted(labels) == ["r1", "r1#2"]
|
|
77
|
+
|
|
78
|
+
def test_a_catalyst_becomes_a_self_loop(self):
|
|
79
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
80
|
+
root = _parse(net)
|
|
81
|
+
place = {l: i for i, l in zip(_ids(root, "place"), _labels(root, "place"))}
|
|
82
|
+
arcs = {(a.get("source"), a.get("target"))
|
|
83
|
+
for a in root.iterfind(".//p:arc", NS)}
|
|
84
|
+
# c catalyses r1: both directions must be present.
|
|
85
|
+
assert (place["c"], "t0") in arcs and ("t0", place["c"]) in arcs
|
|
86
|
+
|
|
87
|
+
def test_food_gets_source_transitions_so_it_cannot_run_out(self):
|
|
88
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
89
|
+
labels = _labels(_parse(net), "transition")
|
|
90
|
+
assert "source:a" in labels and "source:b" in labels
|
|
91
|
+
|
|
92
|
+
def test_food_sources_can_be_turned_off(self):
|
|
93
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
94
|
+
labels = _labels(_parse(net, food_sources=False), "transition")
|
|
95
|
+
assert not any(l.startswith("source:") for l in labels)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class TestWhatCannotBeExpressed:
|
|
99
|
+
def test_reactions_that_can_never_fire_are_omitted_and_counted(self):
|
|
100
|
+
"""chi = empty means "must be catalysed, and nothing does". Emitting it as an
|
|
101
|
+
unconstrained transition would make it freely fireable, the opposite of the
|
|
102
|
+
intent, so it is dropped and the header says how many."""
|
|
103
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\nr2 : a [] => q\n")
|
|
104
|
+
text = to_pnml(net)
|
|
105
|
+
labels = [l for l in _labels(ET.fromstring(text), "transition")
|
|
106
|
+
if not l.startswith("source:")]
|
|
107
|
+
assert labels == ["r1"]
|
|
108
|
+
assert "1 reaction(s) omitted" in text
|
|
109
|
+
|
|
110
|
+
def test_inhibition_is_recorded_and_flagged_as_lossy(self):
|
|
111
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] {z} => c\n")
|
|
112
|
+
text = to_pnml(net)
|
|
113
|
+
root = ET.fromstring(text)
|
|
114
|
+
ts = root.find(".//p:transition/p:toolspecific", NS)
|
|
115
|
+
assert ts is not None and ts.get("tool") == "rafkit"
|
|
116
|
+
assert ts.find("p:inhibitors", NS).text == "z"
|
|
117
|
+
assert "DIFFERENT system" in text # the warning is not optional
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_write_pnml_round_trips_through_a_file(tmp_path):
|
|
121
|
+
net = parse_crs("Food: a, b\nr1 : a + b [c] => c\n")
|
|
122
|
+
path = tmp_path / "net.pnml"
|
|
123
|
+
write_pnml(net, path)
|
|
124
|
+
assert ET.parse(path).getroot().tag.endswith("pnml")
|
|
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
|