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 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
+ [![PyPI](https://img.shields.io/pypi/v/pykappa)](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
+
@@ -0,0 +1,52 @@
1
+ # PyKappa
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/pykappa)](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