rafkit 0.1.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.1.0/src/rafkit.egg-info → rafkit-0.3.0}/PKG-INFO +66 -1
- {rafkit-0.1.0 → rafkit-0.3.0}/README.md +65 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/pyproject.toml +1 -1
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit/__init__.py +14 -4
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit/binary_polymer.py +4 -0
- rafkit-0.3.0/src/rafkit/catalysis.py +78 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit/crs.py +58 -12
- rafkit-0.3.0/src/rafkit/gillespie.py +206 -0
- rafkit-0.3.0/src/rafkit/inhibition.py +114 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit/network.py +16 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit/raf.py +52 -9
- {rafkit-0.1.0 → rafkit-0.3.0/src/rafkit.egg-info}/PKG-INFO +66 -1
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit.egg-info/SOURCES.txt +8 -1
- rafkit-0.3.0/tests/test_gillespie.py +112 -0
- rafkit-0.3.0/tests/test_inhibition.py +239 -0
- rafkit-0.3.0/tests/test_published_examples.py +197 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/tests/test_raf.py +1 -0
- rafkit-0.3.0/tests/test_seeding.py +83 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/LICENSE +0 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/setup.cfg +0 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit.egg-info/dependency_links.txt +0 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit.egg-info/requires.txt +0 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/src/rafkit.egg-info/top_level.txt +0 -0
- {rafkit-0.1.0 → rafkit-0.3.0}/tests/test_crs.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
|
|
@@ -33,6 +33,7 @@ Dynamic: license-file
|
|
|
33
33
|
[](https://pypi.org/project/rafkit/)
|
|
34
34
|
[](https://pypi.org/project/rafkit/)
|
|
35
35
|
[](LICENSE)
|
|
36
|
+
[](https://doi.org/10.5281/zenodo.21954795)
|
|
36
37
|
|
|
37
38
|
Autocatalytic (RAF) sets in catalytic reaction networks — maximal RAFs, irreducible
|
|
38
39
|
cores, Kauffman binary polymer models, and interoperability with
|
|
@@ -109,13 +110,70 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
109
110
|
| `irrraf_census` | how many *distinct* irreducible cores a network carries |
|
|
110
111
|
| `exploitability` | share of RAF products contributing no catalysis back |
|
|
111
112
|
| `is_food_catalysed` | whether a core runs on food catalysis alone, and so carries no heredity |
|
|
113
|
+
| `core_raf` / `has_unique_irraf` | Huson, Xavier & Steel's polynomial test for a *unique* irreducible RAF |
|
|
114
|
+
| `catalytically_reachable` | what can be made without any spontaneous reaction |
|
|
112
115
|
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
113
116
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
114
117
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
118
|
+
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
119
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
115
120
|
|
|
116
121
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
117
122
|
is subtly wrong produces plausible numbers rather than errors.
|
|
118
123
|
|
|
124
|
+
## Catalysis is a relation, not a list
|
|
125
|
+
|
|
126
|
+
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
127
|
+
Steel (2024). Any one set being fully present suffices, and each set is a conjunctive
|
|
128
|
+
requirement:
|
|
129
|
+
|
|
130
|
+
| `catalysts[r]` | meaning |
|
|
131
|
+
|---|---|
|
|
132
|
+
| `{{a}, {b}}` | *a* **or** *b* — the simple case, and what a flat list of catalysts meant |
|
|
133
|
+
| `{{a, d}, {e}}` | (*a* **and** *d*) **or** *e* |
|
|
134
|
+
| `{}` | **must** be catalysed, and nothing does: never in a RAF |
|
|
135
|
+
| `{frozenset()}` | **may proceed uncatalysed**; always satisfied |
|
|
136
|
+
|
|
137
|
+
The last two rows are a real distinction rather than a technicality — in the §2.4 system
|
|
138
|
+
of that paper it decides which reactions can join an RAF — and a flat list collapses
|
|
139
|
+
them. In CRS, a braced group is conjunctive: `[{a,d}, e]`, with `[]` and `[{}]` for the
|
|
140
|
+
last two rows.
|
|
141
|
+
|
|
142
|
+
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
143
|
+
systems stay simple to write.
|
|
144
|
+
|
|
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.
|
|
176
|
+
|
|
119
177
|
## Notes on irreducible cores
|
|
120
178
|
|
|
121
179
|
There may be **exponentially many** irreducible RAFs inside one maximal RAF, and
|
|
@@ -145,6 +203,13 @@ pytest -q
|
|
|
145
203
|
Releases are documented in [CHANGELOG.md](CHANGELOG.md); the release procedure is
|
|
146
204
|
[docs/RELEASING.md](docs/RELEASING.md).
|
|
147
205
|
|
|
206
|
+
## Citing
|
|
207
|
+
|
|
208
|
+
Cite the concept DOI [10.5281/zenodo.21954795](https://doi.org/10.5281/zenodo.21954795),
|
|
209
|
+
which always resolves to the latest version; `CITATION.cff` also lists the per-version
|
|
210
|
+
DOI. If you use the CatReNet interoperability or the validation fixture, please cite
|
|
211
|
+
CatReNet too.
|
|
212
|
+
|
|
148
213
|
## References
|
|
149
214
|
|
|
150
215
|
- Hordijk & Steel, "Detecting autocatalytic, self-sustaining sets in chemical reaction
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
[](https://pypi.org/project/rafkit/)
|
|
6
6
|
[](https://pypi.org/project/rafkit/)
|
|
7
7
|
[](LICENSE)
|
|
8
|
+
[](https://doi.org/10.5281/zenodo.21954795)
|
|
8
9
|
|
|
9
10
|
Autocatalytic (RAF) sets in catalytic reaction networks — maximal RAFs, irreducible
|
|
10
11
|
cores, Kauffman binary polymer models, and interoperability with
|
|
@@ -81,13 +82,70 @@ counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — no
|
|
|
81
82
|
| `irrraf_census` | how many *distinct* irreducible cores a network carries |
|
|
82
83
|
| `exploitability` | share of RAF products contributing no catalysis back |
|
|
83
84
|
| `is_food_catalysed` | whether a core runs on food catalysis alone, and so carries no heredity |
|
|
85
|
+
| `core_raf` / `has_unique_irraf` | Huson, Xavier & Steel's polynomial test for a *unique* irreducible RAF |
|
|
86
|
+
| `catalytically_reachable` | what can be made without any spontaneous reaction |
|
|
84
87
|
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
85
88
|
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
86
89
|
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
90
|
+
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
|
|
91
|
+
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
87
92
|
|
|
88
93
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
89
94
|
is subtly wrong produces plausible numbers rather than errors.
|
|
90
95
|
|
|
96
|
+
## Catalysis is a relation, not a list
|
|
97
|
+
|
|
98
|
+
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
99
|
+
Steel (2024). Any one set being fully present suffices, and each set is a conjunctive
|
|
100
|
+
requirement:
|
|
101
|
+
|
|
102
|
+
| `catalysts[r]` | meaning |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `{{a}, {b}}` | *a* **or** *b* — the simple case, and what a flat list of catalysts meant |
|
|
105
|
+
| `{{a, d}, {e}}` | (*a* **and** *d*) **or** *e* |
|
|
106
|
+
| `{}` | **must** be catalysed, and nothing does: never in a RAF |
|
|
107
|
+
| `{frozenset()}` | **may proceed uncatalysed**; always satisfied |
|
|
108
|
+
|
|
109
|
+
The last two rows are a real distinction rather than a technicality — in the §2.4 system
|
|
110
|
+
of that paper it decides which reactions can join an RAF — and a flat list collapses
|
|
111
|
+
them. In CRS, a braced group is conjunctive: `[{a,d}, e]`, with `[]` and `[{}]` for the
|
|
112
|
+
last two rows.
|
|
113
|
+
|
|
114
|
+
Constructors still accept a plain iterable of molecules and normalise it, so simple
|
|
115
|
+
systems stay simple to write.
|
|
116
|
+
|
|
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.
|
|
148
|
+
|
|
91
149
|
## Notes on irreducible cores
|
|
92
150
|
|
|
93
151
|
There may be **exponentially many** irreducible RAFs inside one maximal RAF, and
|
|
@@ -117,6 +175,13 @@ pytest -q
|
|
|
117
175
|
Releases are documented in [CHANGELOG.md](CHANGELOG.md); the release procedure is
|
|
118
176
|
[docs/RELEASING.md](docs/RELEASING.md).
|
|
119
177
|
|
|
178
|
+
## Citing
|
|
179
|
+
|
|
180
|
+
Cite the concept DOI [10.5281/zenodo.21954795](https://doi.org/10.5281/zenodo.21954795),
|
|
181
|
+
which always resolves to the latest version; `CITATION.cff` also lists the per-version
|
|
182
|
+
DOI. If you use the CatReNet interoperability or the validation fixture, please cite
|
|
183
|
+
CatReNet too.
|
|
184
|
+
|
|
120
185
|
## References
|
|
121
186
|
|
|
122
187
|
- Hordijk & Steel, "Detecting autocatalytic, self-sustaining sets in chemical reaction
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A small, dependency-light implementation of RAF theory (Hordijk & Steel 2004):
|
|
4
4
|
maximal RAFs, the self-referential ("strictly autocatalytic") variant, irreducible
|
|
5
|
-
RAF sampling,
|
|
5
|
+
RAF sampling, Kauffman's binary polymer model as a generator, and stochastic simulation
|
|
6
|
+
of a network so that subRAFs can be watched seeding themselves into existence.
|
|
6
7
|
|
|
7
8
|
Every algorithm here is written from the published papers and carries hand-computed
|
|
8
9
|
known-answer tests, because a RAF algorithm that is subtly wrong produces plausible
|
|
@@ -12,18 +13,27 @@ See the README for the calibration against Steel, Hordijk & Smith (2012) and for
|
|
|
12
13
|
CatReNet interoperability.
|
|
13
14
|
"""
|
|
14
15
|
from rafkit.binary_polymer import BinaryPolymerNetwork, binary_polymer
|
|
16
|
+
from rafkit.catalysis import catalysing_molecules, is_catalysed, normalise
|
|
15
17
|
from rafkit.crs import parse_crs, read_crs, to_crs, write_crs
|
|
18
|
+
from rafkit.gillespie import Trajectory, propensities, simulate
|
|
19
|
+
from rafkit.inhibition import (classes_from_inhibitors, is_uninhibited, is_uraf,
|
|
20
|
+
max_urafs, support)
|
|
16
21
|
from rafkit.network import ReactionNetwork
|
|
17
22
|
from rafkit.raf import (
|
|
18
|
-
RafResult, catrenet_strictly_autocatalytic,
|
|
19
|
-
is_food_catalysed, max_raf, max_raf_strict,
|
|
23
|
+
RafResult, catrenet_strictly_autocatalytic, core_raf, exploitability,
|
|
24
|
+
has_unique_irraf, irrraf_census, is_food_catalysed, max_raf, max_raf_strict,
|
|
25
|
+
sample_irrraf,
|
|
20
26
|
)
|
|
21
27
|
|
|
22
|
-
__version__ = "0.
|
|
28
|
+
__version__ = "0.3.0"
|
|
23
29
|
|
|
24
30
|
__all__ = [
|
|
25
31
|
"BinaryPolymerNetwork", "binary_polymer", "ReactionNetwork",
|
|
26
32
|
"RafResult", "max_raf", "max_raf_strict", "sample_irrraf", "irrraf_census",
|
|
27
33
|
"exploitability", "is_food_catalysed", "catrenet_strictly_autocatalytic",
|
|
34
|
+
"core_raf", "has_unique_irraf",
|
|
35
|
+
"is_catalysed", "catalysing_molecules", "normalise",
|
|
28
36
|
"parse_crs", "read_crs", "to_crs", "write_crs",
|
|
37
|
+
"simulate", "propensities", "Trajectory",
|
|
38
|
+
"max_urafs", "is_uraf", "is_uninhibited", "support", "classes_from_inhibitors",
|
|
29
39
|
]
|
|
@@ -44,6 +44,8 @@ from typing import Iterator
|
|
|
44
44
|
|
|
45
45
|
import numpy as np
|
|
46
46
|
|
|
47
|
+
from rafkit.catalysis import normalise
|
|
48
|
+
|
|
47
49
|
|
|
48
50
|
@dataclass(frozen=True)
|
|
49
51
|
class BinaryPolymerNetwork:
|
|
@@ -66,6 +68,8 @@ class BinaryPolymerNetwork:
|
|
|
66
68
|
directions: tuple[int, ...] = ()
|
|
67
69
|
|
|
68
70
|
def __post_init__(self):
|
|
71
|
+
object.__setattr__(self, "catalysts",
|
|
72
|
+
tuple(normalise(c) for c in self.catalysts))
|
|
69
73
|
if not self.directions: # default: all ligations
|
|
70
74
|
object.__setattr__(self, "directions", (1,) * len(self.reactions))
|
|
71
75
|
elif len(self.directions) != len(self.reactions):
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""The catalysis relation χ, and the one predicate everything else is built on.
|
|
2
|
+
|
|
3
|
+
Huson, Xavier & Steel (2024) treat catalysis as a relation between **sets** of molecules
|
|
4
|
+
and reactions: reaction ``r`` proceeds when *some* catalyst set ``U`` in ``chi(r)`` is
|
|
5
|
+
entirely available. That single structure expresses three things a flat set of catalysts
|
|
6
|
+
cannot:
|
|
7
|
+
|
|
8
|
+
=========================== ============================================================
|
|
9
|
+
``chi(r)`` meaning
|
|
10
|
+
=========================== ============================================================
|
|
11
|
+
``{{a}, {b}}`` *a* **or** *b* -- the simple, disjunctive case
|
|
12
|
+
``{{a, d}, {e}}`` (*a* **and** *d*) **or** *e* -- conjunctive requirements
|
|
13
|
+
``{}`` (no sets at all) **must** be catalysed, and nothing catalyses it: never in a RAF
|
|
14
|
+
``{frozenset()}`` **may proceed uncatalysed**; always satisfied
|
|
15
|
+
=========================== ============================================================
|
|
16
|
+
|
|
17
|
+
The last two are a genuine distinction rather than a technicality -- in their §2.4 system
|
|
18
|
+
it is what separates a reaction that can join an RAF from one that cannot -- and writing
|
|
19
|
+
catalysts as a flat set collapses them.
|
|
20
|
+
|
|
21
|
+
`is_catalysed` handles all four rows without special-casing, because ``any()`` over no
|
|
22
|
+
sets is False and the empty set is a subset of everything.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Iterable
|
|
27
|
+
|
|
28
|
+
CatalystSets = frozenset[frozenset[int]]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalise(spec) -> CatalystSets:
|
|
32
|
+
"""Accept either the simple or the general form and return the general one.
|
|
33
|
+
|
|
34
|
+
A flat iterable of molecule indices -- the form used everywhere before conjunctive
|
|
35
|
+
catalysis existed, and still the right one to write by hand for simple systems --
|
|
36
|
+
becomes one singleton set per molecule, which is exactly equivalent.
|
|
37
|
+
"""
|
|
38
|
+
if spec is None:
|
|
39
|
+
return frozenset()
|
|
40
|
+
out = []
|
|
41
|
+
for item in spec:
|
|
42
|
+
if isinstance(item, (frozenset, set, tuple, list)):
|
|
43
|
+
out.append(frozenset(int(x) for x in item))
|
|
44
|
+
else:
|
|
45
|
+
out.append(frozenset({int(item)}))
|
|
46
|
+
return frozenset(out)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def is_catalysed(chi: CatalystSets, available: Iterable[int]) -> bool:
|
|
50
|
+
"""Whether some catalyst set of a reaction is fully present.
|
|
51
|
+
|
|
52
|
+
Note the two edge cases fall out rather than being handled: with no catalyst sets
|
|
53
|
+
`any()` is False, so the reaction can never run; with the empty set present,
|
|
54
|
+
``frozenset() <= available`` is True, so it always can.
|
|
55
|
+
"""
|
|
56
|
+
avail = available if isinstance(available, (set, frozenset)) else frozenset(available)
|
|
57
|
+
return any(U <= avail for U in chi)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def catalysing_molecules(chi: CatalystSets) -> frozenset[int]:
|
|
61
|
+
"""Every molecule that appears in any catalyst set.
|
|
62
|
+
|
|
63
|
+
The right notion of "x catalyses r" when catalysis is conjunctive: x may be
|
|
64
|
+
necessary without being sufficient, and it still counts as catalysing.
|
|
65
|
+
"""
|
|
66
|
+
return frozenset().union(*chi) if chi else frozenset()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def requires_non_food(chi: CatalystSets, available: Iterable[int], food) -> bool:
|
|
70
|
+
"""Whether some *satisfiable* catalyst set is not contained in the food set.
|
|
71
|
+
|
|
72
|
+
This is the strictly-autocatalytic condition of Huson, Xavier & Steel (2024) §3.1
|
|
73
|
+
stated exactly: ``U subset-of cl(F)`` and ``U not-subset-of F``. Under simple
|
|
74
|
+
catalysis it reduces to "has a catalyst that is a non-food product", which is what
|
|
75
|
+
`max_raf_strict` meant before this module existed.
|
|
76
|
+
"""
|
|
77
|
+
avail = available if isinstance(available, (set, frozenset)) else frozenset(available)
|
|
78
|
+
return any(U <= avail and not U <= food for U in chi)
|
|
@@ -14,7 +14,16 @@ The format is a food line and one line per reaction::
|
|
|
14
14
|
r2 : ab [aa] => abab
|
|
15
15
|
|
|
16
16
|
`[...]` lists catalysts, `<->` marks a reversible reaction and `=>` (or `->`) an
|
|
17
|
-
irreversible one.
|
|
17
|
+
irreversible one.
|
|
18
|
+
|
|
19
|
+
Catalysts are alternatives, any one of which suffices. A braced group is a
|
|
20
|
+
**conjunctive** requirement -- `[{a,d}, e]` means *a and d together*, or *e* -- which
|
|
21
|
+
is the notation Huson, Xavier & Steel (2024) use. Two edge cases carry meaning and are
|
|
22
|
+
not interchangeable: `[]` means the reaction **must** be catalysed and nothing does so,
|
|
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
|
|
18
27
|
reverse, sharing a catalyst set -- which is the reading its own generator uses.
|
|
19
28
|
|
|
20
29
|
`X + X -> Y` is written `X ... -> Y`, with the repeated reactant collapsed. Since
|
|
@@ -39,6 +48,20 @@ def _split_list(text: str) -> list[str]:
|
|
|
39
48
|
return [s for s in (t.strip() for t in re.split(r"[,+]", text)) if s]
|
|
40
49
|
|
|
41
50
|
|
|
51
|
+
def _parse_catalysts(text: str) -> list[list[str]]:
|
|
52
|
+
"""Parse a catalyst list into alternative sets, honouring braced conjunctions.
|
|
53
|
+
|
|
54
|
+
Splitting on commas alone is wrong the moment a braced group appears -- `{a,d}`
|
|
55
|
+
would become `{a` and `d}` -- so groups are pulled out first.
|
|
56
|
+
"""
|
|
57
|
+
groups, rest = [], text
|
|
58
|
+
for m in re.finditer(r"\{([^}]*)\}", text):
|
|
59
|
+
groups.append(_split_list(m.group(1))) # may be empty: {} means "uncatalysed"
|
|
60
|
+
rest = re.sub(r"\{[^}]*\}", " ", text)
|
|
61
|
+
groups += [[name] for name in _split_list(rest)]
|
|
62
|
+
return groups
|
|
63
|
+
|
|
64
|
+
|
|
42
65
|
def parse_crs(text: str) -> ReactionNetwork:
|
|
43
66
|
"""Parse CRS text into a `ReactionNetwork`."""
|
|
44
67
|
food_names: list[str] = []
|
|
@@ -56,44 +79,54 @@ def parse_crs(text: str) -> ReactionNetwork:
|
|
|
56
79
|
continue
|
|
57
80
|
name, body = m.group("name"), m.group("body")
|
|
58
81
|
|
|
59
|
-
|
|
82
|
+
inhib: list[str] = []
|
|
83
|
+
cats: list[list[str]] = []
|
|
60
84
|
if "[" in body:
|
|
61
85
|
pre, rest = body.split("[", 1)
|
|
62
86
|
inside, post = rest.split("]", 1)
|
|
63
|
-
cats =
|
|
87
|
+
cats = _parse_catalysts(inside)
|
|
64
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():]
|
|
65
94
|
|
|
66
95
|
arrow = _ARROW.search(body)
|
|
67
96
|
if not arrow:
|
|
68
97
|
continue
|
|
69
98
|
lhs, rhs = body[:arrow.start()], body[arrow.end():]
|
|
70
|
-
parsed.append((name, _split_list(lhs), _split_list(rhs), cats,
|
|
99
|
+
parsed.append((name, _split_list(lhs), _split_list(rhs), cats, inhib,
|
|
71
100
|
arrow.group(1) in ("<->", "<=>")))
|
|
72
101
|
|
|
73
102
|
# Stable molecule indexing: food first, then order of appearance.
|
|
74
103
|
index: dict[str, int] = {}
|
|
75
104
|
for n in food_names:
|
|
76
105
|
index.setdefault(n, len(index))
|
|
77
|
-
for _, lhs, rhs, cats, _ in parsed:
|
|
78
|
-
for n in (*lhs, *rhs, *cats):
|
|
106
|
+
for _, lhs, rhs, cats, inhib, _ in parsed:
|
|
107
|
+
for n in (*lhs, *rhs, *(x for g in cats for x in g), *inhib):
|
|
79
108
|
index.setdefault(n, len(index))
|
|
80
109
|
|
|
81
|
-
pairs, catalysts, names = [], [], []
|
|
82
|
-
for name, lhs, rhs, cats, reversible in parsed:
|
|
83
|
-
cat = frozenset(index[c] for c in cats)
|
|
110
|
+
pairs, catalysts, names, inhibitors = [], [], [], []
|
|
111
|
+
for name, lhs, rhs, cats, inhib, reversible in parsed:
|
|
112
|
+
cat = frozenset(frozenset(index[c] for c in g) for g in cats)
|
|
113
|
+
inh = frozenset(index[x] for x in inhib)
|
|
84
114
|
fwd = (tuple(index[x] for x in lhs), tuple(index[x] for x in rhs))
|
|
85
115
|
pairs.append(fwd); catalysts.append(cat); names.append(name)
|
|
116
|
+
inhibitors.append(inh)
|
|
86
117
|
if reversible:
|
|
87
118
|
pairs.append((fwd[1], fwd[0]))
|
|
88
119
|
catalysts.append(cat)
|
|
89
120
|
names.append(f"{name}_rev")
|
|
121
|
+
inhibitors.append(inh)
|
|
90
122
|
|
|
91
123
|
molecules = tuple(sorted(index, key=index.get))
|
|
92
124
|
return ReactionNetwork(molecules=molecules,
|
|
93
125
|
food=frozenset(index[n] for n in food_names),
|
|
94
126
|
reaction_pairs=tuple(pairs),
|
|
95
127
|
catalysts=tuple(catalysts),
|
|
96
|
-
names=tuple(names)
|
|
128
|
+
names=tuple(names),
|
|
129
|
+
inhibitors=tuple(inhibitors))
|
|
97
130
|
|
|
98
131
|
|
|
99
132
|
def read_crs(path: str | Path) -> ReactionNetwork:
|
|
@@ -119,12 +152,25 @@ def to_crs(net, comment: str = "") -> str:
|
|
|
119
152
|
for r in range(net.n_reactions):
|
|
120
153
|
lhs = " + ".join(dict.fromkeys(name(x) for x in net.reactants(r)))
|
|
121
154
|
rhs = " + ".join(dict.fromkeys(name(x) for x in net.products(r)))
|
|
122
|
-
cats =
|
|
123
|
-
|
|
155
|
+
cats = _format_catalysts(net, r)
|
|
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}")
|
|
124
161
|
out.append("")
|
|
125
162
|
return "\n".join(out)
|
|
126
163
|
|
|
127
164
|
|
|
165
|
+
def _format_catalysts(net, r: int) -> str:
|
|
166
|
+
"""Render a reaction's catalyst sets, using braces only where they are needed."""
|
|
167
|
+
parts = []
|
|
168
|
+
for U in sorted(net.catalysts[r], key=lambda u: sorted(u)):
|
|
169
|
+
names = sorted(net.molecules[c] for c in U)
|
|
170
|
+
parts.append(names[0] if len(names) == 1 else "{" + ",".join(names) + "}")
|
|
171
|
+
return ",".join(parts)
|
|
172
|
+
|
|
173
|
+
|
|
128
174
|
def write_crs(net, path: str | Path, comment: str = "") -> None:
|
|
129
175
|
"""Write a network to a CRS file."""
|
|
130
176
|
Path(path).write_text(to_crs(net, comment))
|