rafkit 0.1.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/LICENSE +21 -0
- rafkit-0.1.0/PKG-INFO +164 -0
- rafkit-0.1.0/README.md +136 -0
- rafkit-0.1.0/pyproject.toml +48 -0
- rafkit-0.1.0/setup.cfg +4 -0
- rafkit-0.1.0/src/rafkit/__init__.py +29 -0
- rafkit-0.1.0/src/rafkit/binary_polymer.py +185 -0
- rafkit-0.1.0/src/rafkit/crs.py +130 -0
- rafkit-0.1.0/src/rafkit/network.py +82 -0
- rafkit-0.1.0/src/rafkit/raf.py +300 -0
- rafkit-0.1.0/src/rafkit.egg-info/PKG-INFO +164 -0
- rafkit-0.1.0/src/rafkit.egg-info/SOURCES.txt +15 -0
- rafkit-0.1.0/src/rafkit.egg-info/dependency_links.txt +1 -0
- rafkit-0.1.0/src/rafkit.egg-info/requires.txt +6 -0
- rafkit-0.1.0/src/rafkit.egg-info/top_level.txt +1 -0
- rafkit-0.1.0/tests/test_crs.py +80 -0
- rafkit-0.1.0/tests/test_raf.py +304 -0
rafkit-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 James P. Galasyn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rafkit-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rafkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Autocatalytic (RAF) sets in catalytic reaction networks: maximal RAFs, irreducible cores, and Kauffman binary polymer models.
|
|
5
|
+
Author: James P. Galasyn, Claude Théodore
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/jimgalasyn/rafkit
|
|
8
|
+
Project-URL: Issues, https://github.com/jimgalasyn/rafkit/issues
|
|
9
|
+
Keywords: autocatalytic-sets,RAF,origin-of-life,chemical-reaction-networks,systems-chemistry,binary-polymer-model,catalysis,abiogenesis
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Life
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: numpy>=1.24
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov>=4; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-xdist>=3; extra == "test"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# rafkit
|
|
30
|
+
|
|
31
|
+
[](https://github.com/JimGalasyn/rafkit/actions/workflows/ci.yml)
|
|
32
|
+
[](https://codecov.io/gh/JimGalasyn/rafkit)
|
|
33
|
+
[](https://pypi.org/project/rafkit/)
|
|
34
|
+
[](https://pypi.org/project/rafkit/)
|
|
35
|
+
[](LICENSE)
|
|
36
|
+
|
|
37
|
+
Autocatalytic (RAF) sets in catalytic reaction networks — maximal RAFs, irreducible
|
|
38
|
+
cores, Kauffman binary polymer models, and interoperability with
|
|
39
|
+
[CatReNet](https://github.com/husonlab/catrenet).
|
|
40
|
+
|
|
41
|
+
Pure Python and NumPy. No Java, no GUI, no install beyond `pip`.
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from rafkit import binary_polymer, max_raf, sample_irrraf
|
|
45
|
+
import numpy as np
|
|
46
|
+
|
|
47
|
+
net = binary_polymer(max_len=8, food_len=2, p=1.5e-3, cleavage=True)
|
|
48
|
+
raf = max_raf(net)
|
|
49
|
+
print(raf.size, "reactions in the maximal RAF")
|
|
50
|
+
|
|
51
|
+
core = sample_irrraf(net, raf.reactions, np.random.default_rng(0))
|
|
52
|
+
print(len(core), "reactions in one irreducible core")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Why this exists
|
|
56
|
+
|
|
57
|
+
RAF theory (Hordijk & Steel 2004) formalises collectively autocatalytic sets: a set of
|
|
58
|
+
reactions is a RAF over a food set when every reaction is catalysed by something the
|
|
59
|
+
set can make, and every reactant can be built up from food using the set. The
|
|
60
|
+
reference implementation, **CatReNet**, is an excellent Java/JavaFX desktop
|
|
61
|
+
application. This is a small library for people who want the same algorithms inside a
|
|
62
|
+
Python analysis pipeline.
|
|
63
|
+
|
|
64
|
+
## Validated against the reference implementation
|
|
65
|
+
|
|
66
|
+
`tests/data/catrenet_polymer_n6.crs` was generated by CatReNet's own `polymer-tool`,
|
|
67
|
+
and the expected counts in `tests/test_crs.py` are what CatReNet's `catrenet-tool`
|
|
68
|
+
reports on it. The test suite therefore checks this implementation against an
|
|
69
|
+
independent one on every run, with no Java required.
|
|
70
|
+
|
|
71
|
+
| algorithm | rafkit | CatReNet 1.1.0 |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `max_raf` | 183 | 183 |
|
|
74
|
+
| `catrenet_strictly_autocatalytic` | 175 | 175 |
|
|
75
|
+
| `max_raf_strict` | 161 | *(different object — see below)* |
|
|
76
|
+
|
|
77
|
+
**A documented divergence.** CatReNet's `strictlyAutocatalyticMaxRaf` *filters* the
|
|
78
|
+
maximal RAF for reactions having a non-food catalyst, without re-refining, so its
|
|
79
|
+
result need not itself be a RAF. `max_raf_strict` imposes the same condition inside
|
|
80
|
+
the fixpoint, so its result is a RAF, and is correspondingly smaller. Both are
|
|
81
|
+
available; they answer different questions. CatReNet's behaviour was reproduced by
|
|
82
|
+
black-box inference from its output — no CatReNet source was read or used.
|
|
83
|
+
|
|
84
|
+
## Calibration
|
|
85
|
+
|
|
86
|
+
The RAF phase transition in Kauffman's binary polymer model, measured here against
|
|
87
|
+
the published value of *f* ≈ 1.20 (Steel, Hordijk & Smith 2012, n=10, t=2), where
|
|
88
|
+
*f* = p|R| is the mean number of catalysed reactions per molecule:
|
|
89
|
+
|
|
90
|
+
| model | transition |
|
|
91
|
+
|---|---|
|
|
92
|
+
| ligation only | *f* ≈ 4.7 |
|
|
93
|
+
| ligation + cleavage | *f* ≈ 3.1 |
|
|
94
|
+
| ligation + cleavage, **catalysis paired per reversible reaction** | 0 seeds at *f* ≤ 1.22, all seeds by *f* ≈ 1.59 |
|
|
95
|
+
|
|
96
|
+
Two conventions have to match before any comparison to the literature means anything:
|
|
97
|
+
the model must include **cleavage**, and a reversible cleavage–ligation pair must be
|
|
98
|
+
counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — not
|
|
99
|
+
`mean_catalysed_per_molecule` — whenever a number is placed beside a published *f*.
|
|
100
|
+
|
|
101
|
+
## What's implemented
|
|
102
|
+
|
|
103
|
+
| | |
|
|
104
|
+
|---|---|
|
|
105
|
+
| `max_raf` | maximal RAF, by fixpoint (Hordijk & Steel 2004) |
|
|
106
|
+
| `max_raf_strict` | maximal RAF whose catalysts must be non-food products |
|
|
107
|
+
| `catrenet_strictly_autocatalytic` | CatReNet's similarly-named filter, for interop |
|
|
108
|
+
| `sample_irrraf` | one irreducible RAF, by randomised shrinking (Steel, Hordijk & Smith 2012) |
|
|
109
|
+
| `irrraf_census` | how many *distinct* irreducible cores a network carries |
|
|
110
|
+
| `exploitability` | share of RAF products contributing no catalysis back |
|
|
111
|
+
| `is_food_catalysed` | whether a core runs on food catalysis alone, and so carries no heredity |
|
|
112
|
+
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
113
|
+
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
114
|
+
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
115
|
+
|
|
116
|
+
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
117
|
+
is subtly wrong produces plausible numbers rather than errors.
|
|
118
|
+
|
|
119
|
+
## Notes on irreducible cores
|
|
120
|
+
|
|
121
|
+
There may be **exponentially many** irreducible RAFs inside one maximal RAF, and
|
|
122
|
+
finding the smallest is NP-hard (Steel, Hordijk & Smith 2012). `sample_irrraf` returns
|
|
123
|
+
*one*, chosen by the random order it walks; `irrraf_census` samples repeatedly and
|
|
124
|
+
reports how many distinct ones it saw. That count is always a **lower bound**, never
|
|
125
|
+
an upper one.
|
|
126
|
+
|
|
127
|
+
`is_food_catalysed` exists because a core whose every reaction has a food catalyst
|
|
128
|
+
satisfies the letter of the RAF definition while being in no sense self-referential —
|
|
129
|
+
it runs wherever the food runs. Split those out before reading a count of cores as a
|
|
130
|
+
count of anything biological.
|
|
131
|
+
|
|
132
|
+
## Install
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install rafkit
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Development:
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
pip install -e ".[test]"
|
|
142
|
+
pytest -q
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Releases are documented in [CHANGELOG.md](CHANGELOG.md); the release procedure is
|
|
146
|
+
[docs/RELEASING.md](docs/RELEASING.md).
|
|
147
|
+
|
|
148
|
+
## References
|
|
149
|
+
|
|
150
|
+
- Hordijk & Steel, "Detecting autocatalytic, self-sustaining sets in chemical reaction
|
|
151
|
+
systems," *J. Theor. Biol.* 227, 451 (2004).
|
|
152
|
+
- Steel, Hordijk & Smith, "Minimal autocatalytic networks," *J. Theor. Biol.* 332, 96
|
|
153
|
+
(2013); arXiv:1212.4450.
|
|
154
|
+
- Hordijk & Steel, "Autocatalytic sets extended: dynamics, inhibition, and a
|
|
155
|
+
generalization," *J. Syst. Chem.* 3, 5 (2012); arXiv:1206.1017.
|
|
156
|
+
- Huson, Xavier & Steel, "CatReNet: interactive analysis of (auto-)catalytic reaction
|
|
157
|
+
networks," *Bioinformatics* 40(8), btae515 (2024).
|
|
158
|
+
- Serra & Villani, "Template-Based Catalysis and the Emergence of Collectively
|
|
159
|
+
Autocatalytic Systems," *Entropy* 28(2), 184 (2026).
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT. CatReNet is GPL v3 and is **not** a dependency — this library interoperates with
|
|
164
|
+
it only through files, and contains no code derived from it.
|
rafkit-0.1.0/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# rafkit
|
|
2
|
+
|
|
3
|
+
[](https://github.com/JimGalasyn/rafkit/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/JimGalasyn/rafkit)
|
|
5
|
+
[](https://pypi.org/project/rafkit/)
|
|
6
|
+
[](https://pypi.org/project/rafkit/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Autocatalytic (RAF) sets in catalytic reaction networks — maximal RAFs, irreducible
|
|
10
|
+
cores, Kauffman binary polymer models, and interoperability with
|
|
11
|
+
[CatReNet](https://github.com/husonlab/catrenet).
|
|
12
|
+
|
|
13
|
+
Pure Python and NumPy. No Java, no GUI, no install beyond `pip`.
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from rafkit import binary_polymer, max_raf, sample_irrraf
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
net = binary_polymer(max_len=8, food_len=2, p=1.5e-3, cleavage=True)
|
|
20
|
+
raf = max_raf(net)
|
|
21
|
+
print(raf.size, "reactions in the maximal RAF")
|
|
22
|
+
|
|
23
|
+
core = sample_irrraf(net, raf.reactions, np.random.default_rng(0))
|
|
24
|
+
print(len(core), "reactions in one irreducible core")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Why this exists
|
|
28
|
+
|
|
29
|
+
RAF theory (Hordijk & Steel 2004) formalises collectively autocatalytic sets: a set of
|
|
30
|
+
reactions is a RAF over a food set when every reaction is catalysed by something the
|
|
31
|
+
set can make, and every reactant can be built up from food using the set. The
|
|
32
|
+
reference implementation, **CatReNet**, is an excellent Java/JavaFX desktop
|
|
33
|
+
application. This is a small library for people who want the same algorithms inside a
|
|
34
|
+
Python analysis pipeline.
|
|
35
|
+
|
|
36
|
+
## Validated against the reference implementation
|
|
37
|
+
|
|
38
|
+
`tests/data/catrenet_polymer_n6.crs` was generated by CatReNet's own `polymer-tool`,
|
|
39
|
+
and the expected counts in `tests/test_crs.py` are what CatReNet's `catrenet-tool`
|
|
40
|
+
reports on it. The test suite therefore checks this implementation against an
|
|
41
|
+
independent one on every run, with no Java required.
|
|
42
|
+
|
|
43
|
+
| algorithm | rafkit | CatReNet 1.1.0 |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `max_raf` | 183 | 183 |
|
|
46
|
+
| `catrenet_strictly_autocatalytic` | 175 | 175 |
|
|
47
|
+
| `max_raf_strict` | 161 | *(different object — see below)* |
|
|
48
|
+
|
|
49
|
+
**A documented divergence.** CatReNet's `strictlyAutocatalyticMaxRaf` *filters* the
|
|
50
|
+
maximal RAF for reactions having a non-food catalyst, without re-refining, so its
|
|
51
|
+
result need not itself be a RAF. `max_raf_strict` imposes the same condition inside
|
|
52
|
+
the fixpoint, so its result is a RAF, and is correspondingly smaller. Both are
|
|
53
|
+
available; they answer different questions. CatReNet's behaviour was reproduced by
|
|
54
|
+
black-box inference from its output — no CatReNet source was read or used.
|
|
55
|
+
|
|
56
|
+
## Calibration
|
|
57
|
+
|
|
58
|
+
The RAF phase transition in Kauffman's binary polymer model, measured here against
|
|
59
|
+
the published value of *f* ≈ 1.20 (Steel, Hordijk & Smith 2012, n=10, t=2), where
|
|
60
|
+
*f* = p|R| is the mean number of catalysed reactions per molecule:
|
|
61
|
+
|
|
62
|
+
| model | transition |
|
|
63
|
+
|---|---|
|
|
64
|
+
| ligation only | *f* ≈ 4.7 |
|
|
65
|
+
| ligation + cleavage | *f* ≈ 3.1 |
|
|
66
|
+
| ligation + cleavage, **catalysis paired per reversible reaction** | 0 seeds at *f* ≤ 1.22, all seeds by *f* ≈ 1.59 |
|
|
67
|
+
|
|
68
|
+
Two conventions have to match before any comparison to the literature means anything:
|
|
69
|
+
the model must include **cleavage**, and a reversible cleavage–ligation pair must be
|
|
70
|
+
counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — not
|
|
71
|
+
`mean_catalysed_per_molecule` — whenever a number is placed beside a published *f*.
|
|
72
|
+
|
|
73
|
+
## What's implemented
|
|
74
|
+
|
|
75
|
+
| | |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `max_raf` | maximal RAF, by fixpoint (Hordijk & Steel 2004) |
|
|
78
|
+
| `max_raf_strict` | maximal RAF whose catalysts must be non-food products |
|
|
79
|
+
| `catrenet_strictly_autocatalytic` | CatReNet's similarly-named filter, for interop |
|
|
80
|
+
| `sample_irrraf` | one irreducible RAF, by randomised shrinking (Steel, Hordijk & Smith 2012) |
|
|
81
|
+
| `irrraf_census` | how many *distinct* irreducible cores a network carries |
|
|
82
|
+
| `exploitability` | share of RAF products contributing no catalysis back |
|
|
83
|
+
| `is_food_catalysed` | whether a core runs on food catalysis alone, and so carries no heredity |
|
|
84
|
+
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
|
|
85
|
+
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
|
|
86
|
+
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
|
|
87
|
+
|
|
88
|
+
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
89
|
+
is subtly wrong produces plausible numbers rather than errors.
|
|
90
|
+
|
|
91
|
+
## Notes on irreducible cores
|
|
92
|
+
|
|
93
|
+
There may be **exponentially many** irreducible RAFs inside one maximal RAF, and
|
|
94
|
+
finding the smallest is NP-hard (Steel, Hordijk & Smith 2012). `sample_irrraf` returns
|
|
95
|
+
*one*, chosen by the random order it walks; `irrraf_census` samples repeatedly and
|
|
96
|
+
reports how many distinct ones it saw. That count is always a **lower bound**, never
|
|
97
|
+
an upper one.
|
|
98
|
+
|
|
99
|
+
`is_food_catalysed` exists because a core whose every reaction has a food catalyst
|
|
100
|
+
satisfies the letter of the RAF definition while being in no sense self-referential —
|
|
101
|
+
it runs wherever the food runs. Split those out before reading a count of cores as a
|
|
102
|
+
count of anything biological.
|
|
103
|
+
|
|
104
|
+
## Install
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
pip install rafkit
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Development:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
pip install -e ".[test]"
|
|
114
|
+
pytest -q
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Releases are documented in [CHANGELOG.md](CHANGELOG.md); the release procedure is
|
|
118
|
+
[docs/RELEASING.md](docs/RELEASING.md).
|
|
119
|
+
|
|
120
|
+
## References
|
|
121
|
+
|
|
122
|
+
- Hordijk & Steel, "Detecting autocatalytic, self-sustaining sets in chemical reaction
|
|
123
|
+
systems," *J. Theor. Biol.* 227, 451 (2004).
|
|
124
|
+
- Steel, Hordijk & Smith, "Minimal autocatalytic networks," *J. Theor. Biol.* 332, 96
|
|
125
|
+
(2013); arXiv:1212.4450.
|
|
126
|
+
- Hordijk & Steel, "Autocatalytic sets extended: dynamics, inhibition, and a
|
|
127
|
+
generalization," *J. Syst. Chem.* 3, 5 (2012); arXiv:1206.1017.
|
|
128
|
+
- Huson, Xavier & Steel, "CatReNet: interactive analysis of (auto-)catalytic reaction
|
|
129
|
+
networks," *Bioinformatics* 40(8), btae515 (2024).
|
|
130
|
+
- Serra & Villani, "Template-Based Catalysis and the Emergence of Collectively
|
|
131
|
+
Autocatalytic Systems," *Entropy* 28(2), 184 (2026).
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. CatReNet is GPL v3 and is **not** a dependency — this library interoperates with
|
|
136
|
+
it only through files, and contains no code derived from it.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "rafkit"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Autocatalytic (RAF) sets in catalytic reaction networks: maximal RAFs, irreducible cores, and Kauffman binary polymer models."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "MIT" }
|
|
7
|
+
requires-python = ">=3.11"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "James P. Galasyn" },
|
|
10
|
+
{ name = "Claude Théodore" },
|
|
11
|
+
]
|
|
12
|
+
keywords = [
|
|
13
|
+
"autocatalytic-sets", "RAF", "origin-of-life", "chemical-reaction-networks",
|
|
14
|
+
"systems-chemistry", "binary-polymer-model", "catalysis", "abiogenesis",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Science/Research",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Chemistry",
|
|
25
|
+
"Topic :: Scientific/Engineering :: Artificial Life",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["numpy>=1.24"]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
test = [
|
|
31
|
+
"pytest>=8",
|
|
32
|
+
"pytest-cov>=4",
|
|
33
|
+
"pytest-xdist>=3", # parallel test execution (pytest -n auto)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/jimgalasyn/rafkit"
|
|
38
|
+
Issues = "https://github.com/jimgalasyn/rafkit/issues"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["setuptools>=68"]
|
|
42
|
+
build-backend = "setuptools.build_meta"
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.packages.find]
|
|
45
|
+
where = ["src"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
rafkit-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""rafkit -- autocatalytic (RAF) sets in catalytic reaction networks.
|
|
2
|
+
|
|
3
|
+
A small, dependency-light implementation of RAF theory (Hordijk & Steel 2004):
|
|
4
|
+
maximal RAFs, the self-referential ("strictly autocatalytic") variant, irreducible
|
|
5
|
+
RAF sampling, and Kauffman's binary polymer model as a generator.
|
|
6
|
+
|
|
7
|
+
Every algorithm here is written from the published papers and carries hand-computed
|
|
8
|
+
known-answer tests, because a RAF algorithm that is subtly wrong produces plausible
|
|
9
|
+
numbers rather than errors.
|
|
10
|
+
|
|
11
|
+
See the README for the calibration against Steel, Hordijk & Smith (2012) and for
|
|
12
|
+
CatReNet interoperability.
|
|
13
|
+
"""
|
|
14
|
+
from rafkit.binary_polymer import BinaryPolymerNetwork, binary_polymer
|
|
15
|
+
from rafkit.crs import parse_crs, read_crs, to_crs, write_crs
|
|
16
|
+
from rafkit.network import ReactionNetwork
|
|
17
|
+
from rafkit.raf import (
|
|
18
|
+
RafResult, catrenet_strictly_autocatalytic, exploitability, irrraf_census,
|
|
19
|
+
is_food_catalysed, max_raf, max_raf_strict, sample_irrraf,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"BinaryPolymerNetwork", "binary_polymer", "ReactionNetwork",
|
|
26
|
+
"RafResult", "max_raf", "max_raf_strict", "sample_irrraf", "irrraf_census",
|
|
27
|
+
"exploitability", "is_food_catalysed", "catrenet_strictly_autocatalytic",
|
|
28
|
+
"parse_crs", "read_crs", "to_crs", "write_crs",
|
|
29
|
+
]
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""E4 — Kauffman's binary polymer model.
|
|
2
|
+
|
|
3
|
+
The canonical setting for the closure phase transition (Kauffman 1986; Hordijk &
|
|
4
|
+
Steel 2004), and the setting in which the closure phase transition is usually
|
|
5
|
+
located.
|
|
6
|
+
|
|
7
|
+
Molecules are binary strings of length 1..`max_len`. The food set is every string of
|
|
8
|
+
length <= `food_len`. Reactions are **ligations** `a + b -> ab` for every ordered pair
|
|
9
|
+
whose concatenation is within `max_len`, and -- when `cleavage=True` -- the reverse
|
|
10
|
+
`ab -> a + b` as well.
|
|
11
|
+
|
|
12
|
+
Cleavage was originally omitted on the grounds that it "makes every product trivially
|
|
13
|
+
reachable". That is overstated but **directionally right**, and a first attempt to
|
|
14
|
+
overturn it here was wrong and is recorded so it is not retried: the argument was that
|
|
15
|
+
a cleavage can only fire on a molecule you already have, and a long molecule was
|
|
16
|
+
ligated from shorter ones that must therefore already be present, so the closure could
|
|
17
|
+
not grow.
|
|
18
|
+
|
|
19
|
+
**Measured, it grows a lot** -- at `max_len=6`, `p=0.004`, from 6-15 molecules to ~120
|
|
20
|
+
in three of four seeds. The flaw in the argument is that a polymer has *many* splits:
|
|
21
|
+
`0101` may be reachable only as `01`+`01`, while the cleavage `0101 -> 010 + 1` yields
|
|
22
|
+
`010`, which no catalysed ligation produces. Production and cleavage need not use the
|
|
23
|
+
same split, so cleavage genuinely enlarges reachability.
|
|
24
|
+
|
|
25
|
+
More reachable molecules means more catalysts in the closure at any catalysis level,
|
|
26
|
+
so a ligation-only model sits at a markedly higher transition than a
|
|
27
|
+
cleavage-ligation one. See the README for the measured comparison.
|
|
28
|
+
|
|
29
|
+
It matters because the published binary-polymer references use cleavage-ligation
|
|
30
|
+
chemistries -- Steel, Hordijk & Smith (2012), whose f ~ 1.20 transition this model is
|
|
31
|
+
calibrated against, and Serra & Villani (2026) -- so a ligation-only model cannot be
|
|
32
|
+
compared to either without correction.
|
|
33
|
+
|
|
34
|
+
Catalysis is assigned independently: each (molecule, reaction) pair is a catalysis
|
|
35
|
+
edge with probability `p`. That uniform assignment is the model's defining
|
|
36
|
+
simplification and also its main limitation — real catalysis is structured, which is
|
|
37
|
+
what ensembles E5/E6 exist to test.
|
|
38
|
+
"""
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from dataclasses import dataclass
|
|
42
|
+
from itertools import product as _product
|
|
43
|
+
from typing import Iterator
|
|
44
|
+
|
|
45
|
+
import numpy as np
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class BinaryPolymerNetwork:
|
|
50
|
+
"""A generated binary-polymer chemistry.
|
|
51
|
+
|
|
52
|
+
`reactions[i] = (a, b, ab)` as molecule indices, always stored in that order
|
|
53
|
+
regardless of which way the reaction runs. `directions[i]` is `+1` for a ligation
|
|
54
|
+
`a + b -> ab` and `-1` for a cleavage `ab -> a + b`; use `reactants()` and
|
|
55
|
+
`products()` rather than unpacking the triple by hand. `catalysts[i]` is the set of
|
|
56
|
+
molecule indices catalysing reaction `i`.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
molecules: tuple[str, ...]
|
|
60
|
+
food: frozenset[int]
|
|
61
|
+
reactions: tuple[tuple[int, int, int], ...]
|
|
62
|
+
catalysts: tuple[frozenset[int], ...]
|
|
63
|
+
p: float
|
|
64
|
+
max_len: int
|
|
65
|
+
food_len: int
|
|
66
|
+
directions: tuple[int, ...] = ()
|
|
67
|
+
|
|
68
|
+
def __post_init__(self):
|
|
69
|
+
if not self.directions: # default: all ligations
|
|
70
|
+
object.__setattr__(self, "directions", (1,) * len(self.reactions))
|
|
71
|
+
elif len(self.directions) != len(self.reactions):
|
|
72
|
+
raise ValueError(
|
|
73
|
+
f"directions has {len(self.directions)} entries for "
|
|
74
|
+
f"{len(self.reactions)} reactions")
|
|
75
|
+
|
|
76
|
+
def reactants(self, r: int) -> tuple[int, ...]:
|
|
77
|
+
"""Molecules consumed by reaction `r`, in its stored direction."""
|
|
78
|
+
a, b, ab = self.reactions[r]
|
|
79
|
+
return (a, b) if self.directions[r] > 0 else (ab,)
|
|
80
|
+
|
|
81
|
+
def products(self, r: int) -> tuple[int, ...]:
|
|
82
|
+
"""Molecules produced by reaction `r`, in its stored direction."""
|
|
83
|
+
a, b, ab = self.reactions[r]
|
|
84
|
+
return (ab,) if self.directions[r] > 0 else (a, b)
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def n_cleavages(self) -> int:
|
|
88
|
+
return sum(1 for d in self.directions if d < 0)
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def catalysis_level(self) -> float:
|
|
92
|
+
"""`f` in the published convention: catalysed reactions per molecule.
|
|
93
|
+
|
|
94
|
+
Steel, Hordijk & Smith (2012) define f = p|R| where **R counts reversible
|
|
95
|
+
`cleavage-ligation` reactions**, i.e. a ligation and its reverse are ONE
|
|
96
|
+
reaction. Reporting `mean_catalysed_per_molecule` against their f therefore
|
|
97
|
+
double-counts a cleavage chemistry and lands 2x too high. This property is the
|
|
98
|
+
comparable quantity; use it, and not `mean_catalysed_per_molecule`, whenever a
|
|
99
|
+
number is set beside theirs.
|
|
100
|
+
"""
|
|
101
|
+
if not self.molecules:
|
|
102
|
+
return 0.0
|
|
103
|
+
n_pairs = self.n_reactions - self.n_cleavages
|
|
104
|
+
return sum(len(c) for c in self.catalysts[:n_pairs]) / len(self.molecules)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def n_molecules(self) -> int:
|
|
108
|
+
return len(self.molecules)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def n_reactions(self) -> int:
|
|
112
|
+
return len(self.reactions)
|
|
113
|
+
|
|
114
|
+
@property
|
|
115
|
+
def mean_catalysed_per_molecule(self) -> float:
|
|
116
|
+
"""Reactions catalysed per molecule — the model's natural control variable.
|
|
117
|
+
|
|
118
|
+
Kauffman's transition is usually located in this quantity rather than in `p`
|
|
119
|
+
directly, because `p` alone is not comparable across network sizes.
|
|
120
|
+
"""
|
|
121
|
+
if not self.molecules:
|
|
122
|
+
return 0.0
|
|
123
|
+
return sum(len(c) for c in self.catalysts) / len(self.molecules)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _strings(max_len: int) -> Iterator[str]:
|
|
127
|
+
for n in range(1, max_len + 1):
|
|
128
|
+
for bits in _product("01", repeat=n):
|
|
129
|
+
yield "".join(bits)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def binary_polymer(max_len: int = 8, food_len: int = 2, p: float = 1e-3,
|
|
133
|
+
rng: np.random.Generator | None = None,
|
|
134
|
+
cleavage: bool = False,
|
|
135
|
+
paired_catalysis: bool = True) -> BinaryPolymerNetwork:
|
|
136
|
+
"""Generate one binary-polymer network with catalysis at probability `p`.
|
|
137
|
+
|
|
138
|
+
`cleavage=True` adds the reverse `ab -> a + b` of every ligation, doubling the
|
|
139
|
+
stored reaction count.
|
|
140
|
+
|
|
141
|
+
`paired_catalysis` (default True) makes a ligation and its reverse **one
|
|
142
|
+
catalysed unit**, sharing a catalyst set -- the reversible "cleavage-ligation"
|
|
143
|
+
reaction of Steel, Hordijk & Smith (2012), and the convention their f = p|R| is
|
|
144
|
+
measured in. Set False to draw the two directions independently; that is a
|
|
145
|
+
different chemistry and its f is not comparable to theirs.
|
|
146
|
+
"""
|
|
147
|
+
if max_len < 2:
|
|
148
|
+
raise ValueError(f"max_len must be at least 2, got {max_len}")
|
|
149
|
+
if not 0 <= food_len < max_len:
|
|
150
|
+
raise ValueError(f"food_len must be in [0, max_len), got {food_len}")
|
|
151
|
+
if not 0.0 <= p <= 1.0:
|
|
152
|
+
raise ValueError(f"p is a probability, got {p}")
|
|
153
|
+
rng = rng or np.random.default_rng()
|
|
154
|
+
|
|
155
|
+
molecules = tuple(_strings(max_len))
|
|
156
|
+
index = {m: i for i, m in enumerate(molecules)}
|
|
157
|
+
food = frozenset(i for i, m in enumerate(molecules) if len(m) <= food_len)
|
|
158
|
+
|
|
159
|
+
ligations = tuple(
|
|
160
|
+
(index[a], index[b], index[a + b])
|
|
161
|
+
for a in molecules for b in molecules if len(a) + len(b) <= max_len
|
|
162
|
+
)
|
|
163
|
+
if cleavage:
|
|
164
|
+
reactions = ligations + ligations
|
|
165
|
+
directions = (1,) * len(ligations) + (-1,) * len(ligations)
|
|
166
|
+
else:
|
|
167
|
+
reactions = ligations
|
|
168
|
+
directions = (1,) * len(ligations)
|
|
169
|
+
|
|
170
|
+
# Sparse Bernoulli draw: sampling the number of catalysts per reaction and then
|
|
171
|
+
# which ones is O(edges) rather than O(molecules x reactions), which matters --
|
|
172
|
+
# the dense product is ~10^8 at max_len=10.
|
|
173
|
+
n_mol = len(molecules)
|
|
174
|
+
n_draw = len(ligations) if (cleavage and paired_catalysis) else len(reactions)
|
|
175
|
+
counts = rng.binomial(n_mol, p, size=n_draw)
|
|
176
|
+
drawn = tuple(
|
|
177
|
+
frozenset(rng.choice(n_mol, size=int(k), replace=False).tolist()) if k else
|
|
178
|
+
frozenset()
|
|
179
|
+
for k in counts
|
|
180
|
+
)
|
|
181
|
+
# Paired: the cleavage half re-uses its ligation's catalysts rather than redrawing.
|
|
182
|
+
catalysts = drawn + drawn if (cleavage and paired_catalysis) else drawn
|
|
183
|
+
return BinaryPolymerNetwork(molecules=molecules, food=food, reactions=reactions,
|
|
184
|
+
catalysts=catalysts, p=p, max_len=max_len,
|
|
185
|
+
food_len=food_len, directions=directions)
|