rafkit 0.3.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.3.0/src/rafkit.egg-info → rafkit-0.4.0}/PKG-INFO +22 -1
- {rafkit-0.3.0 → rafkit-0.4.0}/README.md +21 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/pyproject.toml +1 -1
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/__init__.py +3 -1
- rafkit-0.4.0/src/rafkit/pnml.py +139 -0
- {rafkit-0.3.0 → rafkit-0.4.0/src/rafkit.egg-info}/PKG-INFO +22 -1
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit.egg-info/SOURCES.txt +2 -0
- rafkit-0.4.0/tests/test_pnml.py +124 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/LICENSE +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/setup.cfg +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/binary_polymer.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/catalysis.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/crs.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/gillespie.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/inhibition.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/network.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit/raf.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit.egg-info/dependency_links.txt +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit.egg-info/requires.txt +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/src/rafkit.egg-info/top_level.txt +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/tests/test_crs.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/tests/test_gillespie.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/tests/test_inhibition.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/tests/test_published_examples.py +0 -0
- {rafkit-0.3.0 → rafkit-0.4.0}/tests/test_raf.py +0 -0
- {rafkit-0.3.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,12 +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 |
|
|
119
120
|
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
120
121
|
|
|
121
122
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
122
123
|
is subtly wrong produces plausible numbers rather than errors.
|
|
123
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
|
+
|
|
124
145
|
## Catalysis is a relation, not a list
|
|
125
146
|
|
|
126
147
|
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
@@ -87,12 +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 |
|
|
91
92
|
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
92
93
|
|
|
93
94
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
94
95
|
is subtly wrong produces plausible numbers rather than errors.
|
|
95
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
|
+
|
|
96
117
|
## Catalysis is a relation, not a list
|
|
97
118
|
|
|
98
119
|
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
@@ -19,13 +19,14 @@ from rafkit.gillespie import Trajectory, propensities, simulate
|
|
|
19
19
|
from rafkit.inhibition import (classes_from_inhibitors, is_uninhibited, is_uraf,
|
|
20
20
|
max_urafs, support)
|
|
21
21
|
from rafkit.network import ReactionNetwork
|
|
22
|
+
from rafkit.pnml import to_pnml, write_pnml
|
|
22
23
|
from rafkit.raf import (
|
|
23
24
|
RafResult, catrenet_strictly_autocatalytic, core_raf, exploitability,
|
|
24
25
|
has_unique_irraf, irrraf_census, is_food_catalysed, max_raf, max_raf_strict,
|
|
25
26
|
sample_irrraf,
|
|
26
27
|
)
|
|
27
28
|
|
|
28
|
-
__version__ = "0.
|
|
29
|
+
__version__ = "0.4.0"
|
|
29
30
|
|
|
30
31
|
__all__ = [
|
|
31
32
|
"BinaryPolymerNetwork", "binary_polymer", "ReactionNetwork",
|
|
@@ -34,6 +35,7 @@ __all__ = [
|
|
|
34
35
|
"core_raf", "has_unique_irraf",
|
|
35
36
|
"is_catalysed", "catalysing_molecules", "normalise",
|
|
36
37
|
"parse_crs", "read_crs", "to_crs", "write_crs",
|
|
38
|
+
"to_pnml", "write_pnml",
|
|
37
39
|
"simulate", "propensities", "Trajectory",
|
|
38
40
|
"max_urafs", "is_uraf", "is_uninhibited", "support", "classes_from_inhibitors",
|
|
39
41
|
]
|
|
@@ -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))
|
|
@@ -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,12 +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 |
|
|
119
120
|
| `max_urafs` | uninhibited RAFs, when a molecule can prevent a reaction |
|
|
120
121
|
|
|
121
122
|
Every algorithm carries hand-computed known-answer tests, because a RAF algorithm that
|
|
122
123
|
is subtly wrong produces plausible numbers rather than errors.
|
|
123
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
|
+
|
|
124
145
|
## Catalysis is a relation, not a list
|
|
125
146
|
|
|
126
147
|
`catalysts[r]` is a set of **alternative catalyst sets**, following Huson, Xavier &
|
|
@@ -8,6 +8,7 @@ src/rafkit/crs.py
|
|
|
8
8
|
src/rafkit/gillespie.py
|
|
9
9
|
src/rafkit/inhibition.py
|
|
10
10
|
src/rafkit/network.py
|
|
11
|
+
src/rafkit/pnml.py
|
|
11
12
|
src/rafkit/raf.py
|
|
12
13
|
src/rafkit.egg-info/PKG-INFO
|
|
13
14
|
src/rafkit.egg-info/SOURCES.txt
|
|
@@ -17,6 +18,7 @@ src/rafkit.egg-info/top_level.txt
|
|
|
17
18
|
tests/test_crs.py
|
|
18
19
|
tests/test_gillespie.py
|
|
19
20
|
tests/test_inhibition.py
|
|
21
|
+
tests/test_pnml.py
|
|
20
22
|
tests/test_published_examples.py
|
|
21
23
|
tests/test_raf.py
|
|
22
24
|
tests/test_seeding.py
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|