pykappa 0.1.5__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.
- pykappa-0.1.5/PKG-INFO +66 -0
- pykappa-0.1.5/README.md +52 -0
- pykappa-0.1.5/pykappa/__init__.py +0 -0
- pykappa-0.1.5/pykappa/algebra.py +255 -0
- pykappa-0.1.5/pykappa/grammar.py +414 -0
- pykappa-0.1.5/pykappa/kappa.lark +201 -0
- pykappa-0.1.5/pykappa/mixture.py +620 -0
- pykappa-0.1.5/pykappa/pattern.py +733 -0
- pykappa-0.1.5/pykappa/rule.py +516 -0
- pykappa-0.1.5/pykappa/system.py +620 -0
- pykappa-0.1.5/pykappa/utils.py +264 -0
- pykappa-0.1.5/pykappa.egg-info/PKG-INFO +66 -0
- pykappa-0.1.5/pykappa.egg-info/SOURCES.txt +22 -0
- pykappa-0.1.5/pykappa.egg-info/dependency_links.txt +1 -0
- pykappa-0.1.5/pykappa.egg-info/requires.txt +3 -0
- pykappa-0.1.5/pykappa.egg-info/top_level.txt +1 -0
- pykappa-0.1.5/pyproject.toml +50 -0
- pykappa-0.1.5/setup.cfg +4 -0
- pykappa-0.1.5/tests/test_algebra.py +28 -0
- pykappa-0.1.5/tests/test_grammar.py +126 -0
- pykappa-0.1.5/tests/test_mixture.py +55 -0
- pykappa-0.1.5/tests/test_pattern.py +85 -0
- pykappa-0.1.5/tests/test_rule.py +208 -0
- pykappa-0.1.5/tests/test_system.py +216 -0
pykappa-0.1.5/PKG-INFO
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pykappa
|
|
3
|
+
Version: 0.1.5
|
|
4
|
+
Summary: Work with Kappa rule-based models in Python
|
|
5
|
+
Project-URL: Homepage, https://pykappa.org
|
|
6
|
+
Project-URL: Documentation, https://pykappa.org/api/index.html
|
|
7
|
+
Project-URL: Repository, https://github.com/berkalpay/pykappa
|
|
8
|
+
Project-URL: Issues, https://github.com/berkalpay/pykappa/issues
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: lark>=1.2.2
|
|
12
|
+
Requires-Dist: pandas>=2.0.0
|
|
13
|
+
Requires-Dist: matplotlib>=3.10.3
|
|
14
|
+
|
|
15
|
+
# PyKappa
|
|
16
|
+
|
|
17
|
+
[](https://pypi.org/project/pykappa)
|
|
18
|
+
|
|
19
|
+
PyKappa is a Python package for working with rule-based models.
|
|
20
|
+
It supports simulation and analysis of a wide variety of systems whose individual components interact as described by rules that transform these components in specified ways and at specified rates.
|
|
21
|
+
See our website [pykappa.org](https://pykappa.org) for a tutorial, examples, and documentation.
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
Developer requirements can be installed via:
|
|
26
|
+
```
|
|
27
|
+
pip install -r requirements.txt
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
<details>
|
|
31
|
+
<summary> With uv (optional alternative to pip): </summary>
|
|
32
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
uv sync --dev
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
To access `uv` dependencies, run your commands through `uv` like
|
|
39
|
+
```
|
|
40
|
+
uv run python
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or, if you want to run commands normally, create a virtual environment:
|
|
44
|
+
```
|
|
45
|
+
uv venv # Do this once
|
|
46
|
+
source .venv/bin/activate # Do this every new shell
|
|
47
|
+
```
|
|
48
|
+
and run commands as usual. (`deactivate` exits the venv.)
|
|
49
|
+
|
|
50
|
+
Adding a Python package dependency (this automatically updates pyproject.toml):
|
|
51
|
+
```
|
|
52
|
+
uv add [package-name]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Adding a package as a dev dependency:
|
|
56
|
+
```
|
|
57
|
+
uv add --dev [package-name]
|
|
58
|
+
```
|
|
59
|
+
</details>
|
|
60
|
+
|
|
61
|
+
To run correctness tests, run `pytest`.
|
|
62
|
+
Running `./tests/cpu-profiles/run_profiler.sh` will CPU-profile predefined Kappa models and write the results to `tests/cpu-profiles/results`.
|
|
63
|
+
We use the Black code formatter, which can be run as `black .`
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
|
pykappa-0.1.5/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# PyKappa
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pykappa)
|
|
4
|
+
|
|
5
|
+
PyKappa is a Python package for working with rule-based models.
|
|
6
|
+
It supports simulation and analysis of a wide variety of systems whose individual components interact as described by rules that transform these components in specified ways and at specified rates.
|
|
7
|
+
See our website [pykappa.org](https://pykappa.org) for a tutorial, examples, and documentation.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Development
|
|
11
|
+
Developer requirements can be installed via:
|
|
12
|
+
```
|
|
13
|
+
pip install -r requirements.txt
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
<details>
|
|
17
|
+
<summary> With uv (optional alternative to pip): </summary>
|
|
18
|
+
Install [uv](https://docs.astral.sh/uv/getting-started/installation/), then:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
uv sync --dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
To access `uv` dependencies, run your commands through `uv` like
|
|
25
|
+
```
|
|
26
|
+
uv run python
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or, if you want to run commands normally, create a virtual environment:
|
|
30
|
+
```
|
|
31
|
+
uv venv # Do this once
|
|
32
|
+
source .venv/bin/activate # Do this every new shell
|
|
33
|
+
```
|
|
34
|
+
and run commands as usual. (`deactivate` exits the venv.)
|
|
35
|
+
|
|
36
|
+
Adding a Python package dependency (this automatically updates pyproject.toml):
|
|
37
|
+
```
|
|
38
|
+
uv add [package-name]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Adding a package as a dev dependency:
|
|
42
|
+
```
|
|
43
|
+
uv add --dev [package-name]
|
|
44
|
+
```
|
|
45
|
+
</details>
|
|
46
|
+
|
|
47
|
+
To run correctness tests, run `pytest`.
|
|
48
|
+
Running `./tests/cpu-profiles/run_profiler.sh` will CPU-profile predefined Kappa models and write the results to `tests/cpu-profiles/results`.
|
|
49
|
+
We use the Black code formatter, which can be run as `black .`
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
File without changes
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import operator
|
|
3
|
+
from collections import deque
|
|
4
|
+
from typing import Self, Optional, Callable, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from pykappa.pattern import Component
|
|
8
|
+
from pykappa.system import System
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
string_to_operator = {
|
|
12
|
+
# Unary
|
|
13
|
+
"[log]": math.log,
|
|
14
|
+
"[exp]": math.exp,
|
|
15
|
+
"[sin]": math.sin,
|
|
16
|
+
"[cos]": math.cos,
|
|
17
|
+
"[tan]": math.tan,
|
|
18
|
+
"[sqrt]": math.sqrt,
|
|
19
|
+
# Binary
|
|
20
|
+
"+": operator.add,
|
|
21
|
+
"-": operator.sub,
|
|
22
|
+
"*": operator.mul,
|
|
23
|
+
"/": operator.truediv,
|
|
24
|
+
"^": operator.pow,
|
|
25
|
+
"mod": operator.mod,
|
|
26
|
+
# Comparisons
|
|
27
|
+
"=": operator.eq,
|
|
28
|
+
"<": operator.lt,
|
|
29
|
+
">": operator.gt,
|
|
30
|
+
# List
|
|
31
|
+
"[max]": max,
|
|
32
|
+
"[min]": min,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_operator(kappa_operator: str) -> Callable:
|
|
37
|
+
"""Convert a Kappa string operator to a Python function.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
kappa_operator: Kappa language operator string.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Python function counterpart.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
ValueError: If the operator is not recognized.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
return string_to_operator[kappa_operator]
|
|
50
|
+
except KeyError:
|
|
51
|
+
raise ValueError(f"Unknown operator: {kappa_operator}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Expression:
|
|
55
|
+
"""Algebraic expressions as specified by the Kappa language.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
type: Type of expression (literal, variable, binary_op, etc.).
|
|
59
|
+
attrs: Dictionary of attributes specific to the expression type.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_kappa(cls, kappa_str: str) -> Self:
|
|
64
|
+
"""Parse an Expression from a Kappa string.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
kappa_str: Kappa expression string.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Parsed Expression object.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
AssertionError: If the string doesn't represent a valid expression.
|
|
74
|
+
"""
|
|
75
|
+
from pykappa.grammar import kappa_parser, parse_tree_to_expression
|
|
76
|
+
|
|
77
|
+
input_tree = kappa_parser.parse(kappa_str)
|
|
78
|
+
assert input_tree.data == "kappa_input"
|
|
79
|
+
expr_tree = input_tree.children[0]
|
|
80
|
+
assert expr_tree.data in ["!algebraic_expression", "algebraic_expression"]
|
|
81
|
+
return parse_tree_to_expression(expr_tree)
|
|
82
|
+
|
|
83
|
+
def __init__(self, type, **attrs):
|
|
84
|
+
self.type = type
|
|
85
|
+
self.attrs = attrs
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def kappa_str(self) -> str:
|
|
89
|
+
"""Get the expression representation in Kappa format.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Kappa string representation of the expression.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If expression type is not supported for string conversion.
|
|
96
|
+
"""
|
|
97
|
+
if self.type == "literal":
|
|
98
|
+
return str(self.evaluate())
|
|
99
|
+
|
|
100
|
+
elif self.type == "boolean_literal":
|
|
101
|
+
return "[true]" if self.attrs["value"] else "[false]"
|
|
102
|
+
|
|
103
|
+
elif self.type == "variable":
|
|
104
|
+
return f"'{self.attrs["name"]}'"
|
|
105
|
+
|
|
106
|
+
elif self.type in ("binary_op", "comparison"):
|
|
107
|
+
left_str = self.attrs["left"].kappa_str
|
|
108
|
+
right_str = self.attrs["right"].kappa_str
|
|
109
|
+
return f"({left_str}) {self.attrs['operator']} ({right_str})"
|
|
110
|
+
|
|
111
|
+
elif self.type == "unary_op":
|
|
112
|
+
return f"{self.attrs['operator']} ({self.attrs['child'].kappa_str})"
|
|
113
|
+
|
|
114
|
+
elif self.type == "list_op":
|
|
115
|
+
children_str = " ".join(
|
|
116
|
+
f"({child.kappa_str})" for child in self.attrs["children"]
|
|
117
|
+
)
|
|
118
|
+
return f"{self.attrs["operator"]} {children_str}"
|
|
119
|
+
|
|
120
|
+
elif self.type == "defined_constant":
|
|
121
|
+
return f"{self.attrs["name"]}"
|
|
122
|
+
|
|
123
|
+
elif self.type == "parentheses":
|
|
124
|
+
return self.attrs["child"].kappa_str
|
|
125
|
+
|
|
126
|
+
elif self.type == "conditional":
|
|
127
|
+
true_expr_str = self.attrs["true_expr"].kappa_str
|
|
128
|
+
false_expr_str = self.attrs["false_expr"].kappa_str
|
|
129
|
+
return f"{self.attrs["condition"].kappa_str} [?] {true_expr_str} [:] {false_expr_str}"
|
|
130
|
+
|
|
131
|
+
elif self.type in ("logical_or", "logical_and"):
|
|
132
|
+
left_str = self.attrs["left"].kappa_str
|
|
133
|
+
right_str = self.attrs["right"].kappa_str
|
|
134
|
+
op = {"logical_or": "||", "logical_and": "&&"}
|
|
135
|
+
return f"({left_str}) {op[self.type]} ({right_str})"
|
|
136
|
+
|
|
137
|
+
elif self.type == "logical_not":
|
|
138
|
+
return f"[not] ({self.attrs['child'].kappa_str})"
|
|
139
|
+
|
|
140
|
+
elif self.type == "reserved_variable":
|
|
141
|
+
return self.attrs["value"].kappa_str
|
|
142
|
+
|
|
143
|
+
elif self.type == "component_pattern":
|
|
144
|
+
return f"|{self.attrs['value'].kappa_str}|"
|
|
145
|
+
|
|
146
|
+
raise ValueError(f"Unsupported node type: {self.type}")
|
|
147
|
+
|
|
148
|
+
def evaluate(self, system: Optional["System"] = None) -> int | float:
|
|
149
|
+
"""Evaluate the expression to get its value.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
system: System context for variable evaluation (required for variables).
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Result of evaluating the expression.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
ValueError: If evaluation fails due to missing context or unsupported type.
|
|
159
|
+
"""
|
|
160
|
+
if self.type in ("literal", "boolean_literal"):
|
|
161
|
+
return self.attrs["value"]
|
|
162
|
+
|
|
163
|
+
elif self.type == "variable":
|
|
164
|
+
name = self.attrs["name"]
|
|
165
|
+
if system is None:
|
|
166
|
+
raise ValueError(f"{self} needs a System to evaluate variable '{name}'")
|
|
167
|
+
return system[name]
|
|
168
|
+
|
|
169
|
+
elif self.type in ("binary_op", "comparison"):
|
|
170
|
+
left_val = self.attrs["left"].evaluate(system)
|
|
171
|
+
right_val = self.attrs["right"].evaluate(system)
|
|
172
|
+
return parse_operator(self.attrs["operator"])(left_val, right_val)
|
|
173
|
+
|
|
174
|
+
elif self.type == "unary_op":
|
|
175
|
+
child_val = self.attrs["child"].evaluate(system)
|
|
176
|
+
return parse_operator(self.attrs["operator"])(child_val)
|
|
177
|
+
|
|
178
|
+
elif self.type == "list_op":
|
|
179
|
+
children_vals = [child.evaluate(system) for child in self.attrs["children"]]
|
|
180
|
+
return parse_operator(self.attrs["operator"])(children_vals)
|
|
181
|
+
|
|
182
|
+
elif self.type == "defined_constant":
|
|
183
|
+
const = self.attrs["name"]
|
|
184
|
+
if const == "[pi]":
|
|
185
|
+
return math.pi
|
|
186
|
+
else:
|
|
187
|
+
raise ValueError(f"Unknown constant: {const}")
|
|
188
|
+
|
|
189
|
+
elif self.type == "parentheses":
|
|
190
|
+
return self.attrs["child"].evaluate(system)
|
|
191
|
+
|
|
192
|
+
elif self.type == "conditional":
|
|
193
|
+
cond_val = self.attrs["condition"].evaluate(system)
|
|
194
|
+
return (
|
|
195
|
+
self.attrs["true_expr"].evaluate(system)
|
|
196
|
+
if cond_val
|
|
197
|
+
else self.attrs["false_expr"].evaluate(system)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
elif self.type == "logical_or":
|
|
201
|
+
left_val = self.attrs["left"].evaluate(system)
|
|
202
|
+
right_val = self.attrs["right"].evaluate(system)
|
|
203
|
+
return left_val or right_val
|
|
204
|
+
|
|
205
|
+
elif self.type == "logical_and":
|
|
206
|
+
left_val = self.attrs["left"].evaluate(system)
|
|
207
|
+
right_val = self.attrs["right"].evaluate(system)
|
|
208
|
+
return left_val and right_val
|
|
209
|
+
|
|
210
|
+
elif self.type == "logical_not":
|
|
211
|
+
return not self.attrs["child"].evaluate(system)
|
|
212
|
+
|
|
213
|
+
elif self.type == "reserved_variable":
|
|
214
|
+
value = self.attrs["value"]
|
|
215
|
+
if value.type == "component_pattern":
|
|
216
|
+
component: Component = value.attrs["value"]
|
|
217
|
+
if system is None:
|
|
218
|
+
raise ValueError(
|
|
219
|
+
f"{self} needs a System to evaluate pattern {component}"
|
|
220
|
+
)
|
|
221
|
+
return (
|
|
222
|
+
len(system.mixture.embeddings(component))
|
|
223
|
+
// component.n_automorphisms
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
raise NotImplementedError(
|
|
227
|
+
f"Reserved variable {value.type} not implemented yet."
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
raise ValueError(f"Unsupported node type: {self.type}")
|
|
231
|
+
|
|
232
|
+
def filter(self, type_str: str) -> list[Self]:
|
|
233
|
+
"""
|
|
234
|
+
Returns all nodes in the expression tree whose type matches the provided string.
|
|
235
|
+
|
|
236
|
+
Note:
|
|
237
|
+
Doesn't detect nodes indirectly nested in named variables.
|
|
238
|
+
"""
|
|
239
|
+
result = []
|
|
240
|
+
stack = deque([self]) # DFS from the root
|
|
241
|
+
|
|
242
|
+
while stack:
|
|
243
|
+
node = stack.pop()
|
|
244
|
+
if node.type == type_str:
|
|
245
|
+
result.append(node)
|
|
246
|
+
|
|
247
|
+
# Add child nodes to the stack
|
|
248
|
+
if hasattr(node, "attrs"):
|
|
249
|
+
for attr_value in node.attrs.values():
|
|
250
|
+
if isinstance(attr_value, Expression):
|
|
251
|
+
stack.append(attr_value)
|
|
252
|
+
elif isinstance(attr_value, (list, tuple)):
|
|
253
|
+
stack.extend(v for v in attr_value if isinstance(v, Expression))
|
|
254
|
+
|
|
255
|
+
return result
|