Metadata-Version: 2.4
Name: rafkit
Version: 0.5.0
Summary: Autocatalytic (RAF) sets in catalytic reaction networks: maximal RAFs, irreducible cores, and Kauffman binary polymer models.
Author: James P. Galasyn, Claude Théodore
License: MIT
Project-URL: Homepage, https://github.com/jimgalasyn/rafkit
Project-URL: Issues, https://github.com/jimgalasyn/rafkit/issues
Keywords: autocatalytic-sets,RAF,origin-of-life,chemical-reaction-networks,systems-chemistry,binary-polymer-model,catalysis,abiogenesis
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Chemistry
Classifier: Topic :: Scientific/Engineering :: Artificial Life
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.24
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-cov>=4; extra == "test"
Requires-Dist: pytest-xdist>=3; extra == "test"
Dynamic: license-file

# rafkit

[![CI](https://github.com/JimGalasyn/rafkit/actions/workflows/ci.yml/badge.svg)](https://github.com/JimGalasyn/rafkit/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/JimGalasyn/rafkit/branch/main/graph/badge.svg)](https://codecov.io/gh/JimGalasyn/rafkit)
[![PyPI](https://img.shields.io/pypi/v/rafkit.svg?cacheSeconds=3600)](https://pypi.org/project/rafkit/)
[![Python](https://img.shields.io/pypi/pyversions/rafkit.svg?cacheSeconds=3600)](https://pypi.org/project/rafkit/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.21954795.svg)](https://doi.org/10.5281/zenodo.21954795)

Autocatalytic (RAF) sets in catalytic reaction networks — maximal RAFs, irreducible
cores, Kauffman binary polymer models, and interoperability with
[CatReNet](https://github.com/husonlab/catrenet).

Pure Python and NumPy. No Java, no GUI, no install beyond `pip`.

```python
from rafkit import binary_polymer, max_raf, sample_irrraf
import numpy as np

net = binary_polymer(max_len=8, food_len=2, p=1.5e-3, cleavage=True)
raf = max_raf(net)
print(raf.size, "reactions in the maximal RAF")

core = sample_irrraf(net, raf.reactions, np.random.default_rng(0))
print(len(core), "reactions in one irreducible core")
```

## Why this exists

RAF theory (Hordijk & Steel 2004) formalises collectively autocatalytic sets: a set of
reactions is a RAF over a food set when every reaction is catalysed by something the
set can make, and every reactant can be built up from food using the set. The
reference implementation, **CatReNet**, is an excellent Java/JavaFX desktop
application. This is a small library for people who want the same algorithms inside a
Python analysis pipeline.

## Validated against the reference implementation

`tests/data/catrenet_polymer_n6.crs` was generated by CatReNet's own `polymer-tool`,
and the expected counts in `tests/test_crs.py` are what CatReNet's `catrenet-tool`
reports on it. The test suite therefore checks this implementation against an
independent one on every run, with no Java required.

| algorithm | rafkit | CatReNet 1.1.0 |
|---|---|---|
| `max_raf` | 183 | 183 |
| `catrenet_strictly_autocatalytic` | 175 | 175 |
| `max_raf_strict` | 161 | *(different object — see below)* |

**A documented divergence.** CatReNet's `strictlyAutocatalyticMaxRaf` *filters* the
maximal RAF for reactions having a non-food catalyst, without re-refining, so its
result need not itself be a RAF. `max_raf_strict` imposes the same condition inside
the fixpoint, so its result is a RAF, and is correspondingly smaller. Both are
available; they answer different questions. CatReNet's behaviour was reproduced by
black-box inference from its output — no CatReNet source was read or used.

## Calibration

The RAF phase transition in Kauffman's binary polymer model, measured here against
the published value of *f* ≈ 1.20 (Steel, Hordijk & Smith 2012, n=10, t=2), where
*f* = p|R| is the mean number of catalysed reactions per molecule:

| model | transition |
|---|---|
| ligation only | *f* ≈ 4.7 |
| ligation + cleavage | *f* ≈ 3.1 |
| ligation + cleavage, **catalysis paired per reversible reaction** | 0 seeds at *f* ≤ 1.22, all seeds by *f* ≈ 1.59 |

Two conventions have to match before any comparison to the literature means anything:
the model must include **cleavage**, and a reversible cleavage–ligation pair must be
counted as **one** catalysed reaction, not two. Use `net.catalysis_level` — not
`mean_catalysed_per_molecule` — whenever a number is placed beside a published *f*.

## What's implemented

| | |
|---|---|
| `max_raf` | maximal RAF, by fixpoint (Hordijk & Steel 2004) |
| `max_raf_strict` | maximal RAF whose catalysts must be non-food products |
| `catrenet_strictly_autocatalytic` | CatReNet's similarly-named filter, for interop |
| `sample_irrraf` | one irreducible RAF, by randomised shrinking (Steel, Hordijk & Smith 2012) |
| `irrraf_census` | how many *distinct* irreducible cores a network carries |
| `exploitability` | share of RAF products contributing no catalysis back |
| `is_food_catalysed` | whether a core runs on food catalysis alone, and so carries no heredity |
| `core_raf` / `has_unique_irraf` | Huson, Xavier & Steel's polynomial test for a *unique* irreducible RAF |
| `catalytically_reachable` | what can be made without any spontaneous reaction |
| `binary_polymer` | Kauffman binary polymer generator, with optional cleavage |
| `ReactionNetwork` | arbitrary catalytic reaction systems, same protocol |
| `read_crs` / `write_crs` | CatReNet's CRS interchange format |
| `to_pnml` / `write_pnml` | PNML export (ISO/IEC 15909-2) for the Petri net ecosystem |
| `simulate` | Gillespie direct method — watch subRAFs seed themselves into existence |
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |

Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
is subtly wrong produces plausible numbers rather than errors.

## A reaction network is a Petri net

Species are places, reactions are transitions, molecule counts are tokens. `write_pnml`
exports to PNML (ISO/IEC 15909-2), so these networks open in Petri net editors, model
checkers, and the unfolding tools that compute the causal structure of a run.

Three things need care, and each is explicit rather than silent:

- **Catalysis becomes a self-loop.** P/T nets have no read arc, so a catalyst is a pair
  of arcs, consuming the token and returning it.
- **Alternative catalyst sets become separate transitions**, named `r1`, `r1#2`, …, since
  a transition's preset is a conjunction and cannot express "either set".
- **Food gets source transitions**, because RAF food is inexhaustible and no initial
  marking expresses that — a marking of *n* deadlocks after *n* uses.

Inhibition has no `ptnet` representation and is written as a `toolspecific` annotation,
with a warning in the file: a reader that ignores it gets a *different system*.
Reactions requiring a catalyst that nothing provides are omitted and counted, since
emitting them unconstrained would make them freely fireable — the opposite of the intent.

## Catalysis is a relation, not a list

`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
Steel (2024). Any one set being fully present suffices, and each set is a conjunctive
requirement:

| `catalysts[r]` | meaning |
|---|---|
| `{{a}, {b}}` | *a* **or** *b* — the simple case, and what a flat list of catalysts meant |
| `{{a, d}, {e}}` | (*a* **and** *d*) **or** *e* |
| `{}` | **must** be catalysed, and nothing does: never in a RAF |
| `{frozenset()}` | **may proceed uncatalysed**; always satisfied |

The last two rows are a real distinction rather than a technicality — in the §2.4 system
of that paper it decides which reactions can join an RAF — and a flat list collapses
them. In CRS, a braced group is conjunctive: `[{a,d}, e]`, with `[]` and `[{}]` for the
last two rows.

Constructors still accept a plain iterable of molecules and normalise it, so simple
systems stay simple to write.

## Inhibition

A molecule can prevent a reaction. `max_urafs` returns the **uninhibited RAFs** of
Hordijk & Steel (2012), and returns a *collection* rather than one set, because
inhibition destroys the monotonicity that makes a maximal RAF unique — there is no
"the" maximal u-RAF.

```
Food: a, b
r1 : a + b [a] {d} => c      # inhibited by d
r2 : a + b [b] {c} => d      # inhibited by c
```

Two maximal u-RAFs, `{r1}` and `{r2}`: each is an RAF whose support avoids its own
inhibitor, and their union is an RAF that fails the uninhibited condition.

`simulate` respects inhibition too — an inhibited reaction has propensity zero, so a
running network can **lose** a subRAF, not merely gain one. See
`examples/inhibition_dissolution.py`.

The set-theoretic tools need no special handling: `sample_irrraf`, `irrraf_census`,
`core_raf` and `catalytically_reachable` all take a reaction set, and passing a u-RAF
is correct because the uninhibited property is inherited downward — every sub-RAF of a
u-RAF is a u-RAF.

Deciding whether a u-RAF exists is NP-complete, but the problem is fixed-parameter
tractable in *k*, the number of inhibition classes — and **k is a property of how you
encode inhibition, not of the chemistry.** `classes_from_inhibitors` groups by
inhibiting *molecule*, so *k* is the number of distinct inhibitors rather than the
number of inhibited reactions, which is the difference between `2^k` being feasible
and not.

## Notes on irreducible cores

There may be **exponentially many** irreducible RAFs inside one maximal RAF, and
finding the smallest is NP-hard (Steel, Hordijk & Smith 2012). `sample_irrraf` returns
*one*, chosen by the random order it walks; `irrraf_census` samples repeatedly and
reports how many distinct ones it saw. That count is always a **lower bound**, never
an upper one.

`is_food_catalysed` exists because a core whose every reaction has a food catalyst
satisfies the letter of the RAF definition while being in no sense self-referential —
it runs wherever the food runs. Split those out before reading a count of cores as a
count of anything biological.

## Install

```bash
pip install rafkit
```

Development:

```bash
pip install -e ".[test]"
pytest -q
```

Releases are documented in [CHANGELOG.md](CHANGELOG.md); the release procedure is
[docs/RELEASING.md](docs/RELEASING.md).

## Citing

Cite the concept DOI [10.5281/zenodo.21954795](https://doi.org/10.5281/zenodo.21954795),
which always resolves to the latest version; `CITATION.cff` also lists the per-version
DOI. If you use the CatReNet interoperability or the validation fixture, please cite
CatReNet too.

## References

- Hordijk & Steel, "Detecting autocatalytic, self-sustaining sets in chemical reaction
  systems," *J. Theor. Biol.* 227, 451 (2004).
- Steel, Hordijk & Smith, "Minimal autocatalytic networks," *J. Theor. Biol.* 332, 96
  (2013); arXiv:1212.4450.
- Hordijk & Steel, "Autocatalytic sets extended: dynamics, inhibition, and a
  generalization," *J. Syst. Chem.* 3, 5 (2012); arXiv:1206.1017.
- Huson, Xavier & Steel, "CatReNet: interactive analysis of (auto-)catalytic reaction
  networks," *Bioinformatics* 40(8), btae515 (2024).
- Serra & Villani, "Template-Based Catalysis and the Emergence of Collectively
  Autocatalytic Systems," *Entropy* 28(2), 184 (2026).

## License

MIT. CatReNet is GPL v3 and is **not** a dependency — this library interoperates with
it only through files, and contains no code derived from it.
