pykappa 0.1.5__py3-none-any.whl

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/__init__.py ADDED
File without changes
pykappa/algebra.py ADDED
@@ -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
pykappa/grammar.py ADDED
@@ -0,0 +1,414 @@
1
+ from pathlib import Path
2
+ from dataclasses import dataclass
3
+ from typing import Optional
4
+ from lark import Lark, ParseTree, Tree, Visitor, Token, Transformer_NonRecursive
5
+
6
+ from pykappa.pattern import Site, Agent, Pattern, SiteType, Partner
7
+ from pykappa.rule import Rule, KappaRule, KappaRuleUnimolecular, KappaRuleBimolecular
8
+ from pykappa.algebra import Expression
9
+
10
+
11
+ class KappaParser:
12
+ """Parser for Kappa language files and expressions.
13
+
14
+ Note:
15
+ Don't instantiate directly: use the global kappa_parser instance.
16
+ """
17
+
18
+ def __init__(self):
19
+ """Initialize the Lark parser with Kappa grammar."""
20
+ self._parser = Lark.open(
21
+ str(Path(__file__).parent / "kappa.lark"),
22
+ rel_to=__file__,
23
+ parser="earley",
24
+ # The basic lexer isn't required and isn't usually recommended
25
+ lexer="dynamic",
26
+ start="kappa_input",
27
+ # Disabling these slightly improves speed
28
+ propagate_positions=False,
29
+ maybe_placeholders=False,
30
+ )
31
+
32
+ def parse(self, text: str) -> ParseTree:
33
+ return self._parser.parse(text)
34
+
35
+ def parse_file(self, filepath: str) -> ParseTree:
36
+ with open(filepath, "r") as file:
37
+ return self._parser.parse(file.read())
38
+
39
+
40
+ kappa_parser = KappaParser()
41
+
42
+
43
+ @dataclass
44
+ class SiteBuilder(Visitor):
45
+ """Builds Site objects from Lark parse trees.
46
+
47
+ Attributes:
48
+ parsed_site_name: Name of the site being built.
49
+ parsed_state: Internal state of the site.
50
+ parsed_partner: Partner specification for the site.
51
+ """
52
+
53
+ parsed_site_name: str
54
+ parsed_state: str
55
+ parsed_partner: Partner
56
+
57
+ def __init__(self, tree: ParseTree):
58
+ super().__init__()
59
+
60
+ self.parsed_agents: list["Agent"] = []
61
+
62
+ assert tree.data == "site"
63
+ self.visit(tree)
64
+
65
+ # Visitor method for Lark
66
+ def site_name(self, tree: ParseTree) -> None:
67
+ self.parsed_site_name = str(tree.children[0])
68
+
69
+ # Visitor method for Lark
70
+ def state(self, tree: ParseTree) -> None:
71
+ match tree.children[0]:
72
+ case "#":
73
+ self.parsed_state = "#"
74
+ case str(state):
75
+ self.parsed_state = str(state)
76
+ case Tree(data="unspecified"):
77
+ self.parsed_state = "?"
78
+ case _:
79
+ raise ValueError(
80
+ f"Unexpected internal state in site parse tree: {tree}"
81
+ )
82
+
83
+ # Visitor method for Lark
84
+ def partner(self, tree: ParseTree) -> None:
85
+ match tree.children:
86
+ case ["#"]:
87
+ self.parsed_partner = "#"
88
+ case ["_"]:
89
+ self.parsed_partner = "_"
90
+ case ["."]:
91
+ self.parsed_partner = "."
92
+ case [Token("INT", x)]:
93
+ self.parsed_partner = int(x)
94
+ case [
95
+ Tree(data="site_name", children=[site_name]),
96
+ Tree(data="agent_name", children=[agent_name]),
97
+ ]:
98
+ self.parsed_partner = SiteType(str(site_name), str(agent_name))
99
+ case [Tree(data="unspecified")]:
100
+ self.parsed_partner = "?"
101
+ case _:
102
+ raise ValueError(f"Unexpected link state in site parse tree: {tree}")
103
+
104
+ @property
105
+ def object(self) -> Site:
106
+ return Site(
107
+ label=self.parsed_site_name,
108
+ state=self.parsed_state,
109
+ partner=self.parsed_partner,
110
+ )
111
+
112
+
113
+ @dataclass
114
+ class AgentBuilder(Visitor):
115
+ """Builds Agent objects from Lark parse trees.
116
+
117
+ Attributes:
118
+ parsed_type: Type name of the agent.
119
+ parsed_interface: List of sites belonging to the agent.
120
+ """
121
+
122
+ parsed_type: str
123
+ parsed_interface: list[Site]
124
+
125
+ def __init__(self, tree: ParseTree):
126
+ super().__init__()
127
+
128
+ self.parsed_type = None
129
+ self.parsed_interface: list[Site] = []
130
+
131
+ assert tree.data == "agent"
132
+ self.visit(tree)
133
+
134
+ # Visitor method for Lark
135
+ def agent_name(self, tree: ParseTree) -> None:
136
+ self.parsed_type = str(tree.children[0])
137
+
138
+ # Visitor method for Lark
139
+ def site(self, tree: ParseTree) -> None:
140
+ self.parsed_interface.append(SiteBuilder(tree).object)
141
+
142
+ @property
143
+ def object(self) -> Agent:
144
+ agent = Agent(type=self.parsed_type, sites=self.parsed_interface)
145
+ for site in agent:
146
+ site.agent = agent
147
+ return agent
148
+
149
+
150
+ @dataclass
151
+ class PatternBuilder(Visitor):
152
+ """Builds Pattern objects from Lark parse trees.
153
+
154
+ Attributes:
155
+ parsed_agents: List of agents in the pattern.
156
+ """
157
+
158
+ parsed_agents: list[Agent]
159
+
160
+ def __init__(self, tree: ParseTree):
161
+ super().__init__()
162
+
163
+ self.parsed_agents: list[Agent] = []
164
+
165
+ assert tree.data == "pattern"
166
+ self.visit(tree)
167
+
168
+ # Visitor method for Lark
169
+ def agent(self, tree: ParseTree) -> None:
170
+ self.parsed_agents.append(AgentBuilder(tree).object)
171
+
172
+ @property
173
+ def object(self) -> Pattern:
174
+ return Pattern(agents=self.parsed_agents)
175
+
176
+
177
+ @dataclass
178
+ class RuleBuilder(Visitor):
179
+ """Builds Rule objects from Lark parse trees.
180
+
181
+ Attributes:
182
+ parsed_label: Optional label for the rule.
183
+ left_agents: Agents on the left side of the rule.
184
+ right_agents: Agents on the right side of the rule.
185
+ parsed_rates: Rate expressions for the rule.
186
+ tree_data: Type of rule being built.
187
+ """
188
+
189
+ parsed_label: Optional[str]
190
+ left_agents: list[Optional[Agent]]
191
+ right_agents: list[Optional[Agent]]
192
+ parsed_rates: list[Expression]
193
+ tree_data: str
194
+
195
+ def __init__(self, tree: ParseTree):
196
+ super().__init__()
197
+
198
+ self.parsed_label = None
199
+ self.left_agents = []
200
+ self.right_agents = []
201
+ self.parsed_rates = []
202
+
203
+ assert tree.data in ["f_rule", "fr_rule", "ambi_rule", "ambi_fr_rule"]
204
+ self.tree_data = tree.data
205
+
206
+ self.visit(tree)
207
+
208
+ # Visitor method for Lark
209
+ def rate(self, tree: ParseTree) -> None:
210
+ assert tree.data == "rate"
211
+
212
+ expr = tree.children[0]
213
+ assert expr.data == "algebraic_expression"
214
+
215
+ rate = parse_tree_to_expression(expr)
216
+ self.parsed_rates.append(rate)
217
+
218
+ # Visitor method for Lark
219
+ def rule_expression(self, tree: ParseTree) -> None:
220
+ assert tree.data in ["rule_expression", "rev_rule_expression"]
221
+ mid_idx = next(
222
+ (i for i, child in enumerate(tree.children) if child in ["->", "<->"])
223
+ ) # Locate the arrow in the expression
224
+
225
+ for i, child in enumerate(tree.children):
226
+ if i == mid_idx:
227
+ continue
228
+
229
+ if child == ".":
230
+ agent = None
231
+ elif child.data == "agent":
232
+ agent = AgentBuilder(child).object
233
+
234
+ if i < mid_idx:
235
+ self.left_agents.append(agent)
236
+ else:
237
+ self.right_agents.append(agent)
238
+
239
+ # Visitor method for Lark
240
+ def rev_rule_expression(self, tree: ParseTree) -> None:
241
+ self.rule_expression(tree)
242
+
243
+ @property
244
+ def objects(self) -> list[Rule]:
245
+ rules = []
246
+ left = Pattern(self.left_agents)
247
+ right = Pattern(self.right_agents)
248
+ rates = self.parsed_rates
249
+
250
+ match self.tree_data:
251
+ case "f_rule":
252
+ assert len(rates) == 1
253
+ rules.append(KappaRule(left, right, rates[0]))
254
+ case "fr_rule":
255
+ assert len(rates) == 2
256
+ rules.append(KappaRule(left, right, rates[0]))
257
+ rules.append(KappaRule(right, left, rates[1]))
258
+ case "ambi_rule":
259
+ # TODO: check that the order of the rates is right
260
+ assert len(rates) == 2
261
+ try:
262
+ assert rates[0].evaluate() == 0
263
+ except:
264
+ rules.append(KappaRuleBimolecular(left, right, rates[0]))
265
+ try:
266
+ assert rates[1].evaluate() == 0
267
+ except:
268
+ rules.append(KappaRuleUnimolecular(left, right, rates[1]))
269
+ case "ambi_fr_rule":
270
+ assert len(rates) == 3
271
+ try:
272
+ assert rates[0].evaluate() == 0
273
+ except:
274
+ rules.append(KappaRuleBimolecular(left, right, rates[0]))
275
+ try:
276
+ assert rates[1].evaluate() == 0
277
+ except:
278
+ rules.append(KappaRuleUnimolecular(left, right, rates[1]))
279
+ rules.append(KappaRule(right, left, rates[2]))
280
+
281
+ return [r for r in rules if r is not None]
282
+
283
+
284
+ class LarkTreetoExpression(Transformer_NonRecursive):
285
+ """Transforms a Lark ParseTree into an Expression object.
286
+
287
+ Note:
288
+ Uses a Transformer to preserve the tree structure of the original
289
+ ParseTree. This doesn't need to use Transformer_NonRecursive anymore
290
+ due to grammar changes, but methods explicitly call transform on children.
291
+ """
292
+
293
+ def algebraic_expression(self, children):
294
+ children = [self.transform(c) for c in children]
295
+ if len(children) == 1:
296
+ return children[0]
297
+ elif len(children) == 3 and children[0] == "(" and children[2] == ")":
298
+ return children[1]
299
+ else:
300
+ raise Exception(f"Invalid algebraic expression: {children}")
301
+
302
+ # --- Literals ---
303
+ def SIGNED_FLOAT(self, token):
304
+ return Expression("literal", value=float(token.value))
305
+
306
+ def SIGNED_INT(self, token):
307
+ return Expression("literal", value=int(token.value))
308
+
309
+ # --- Variables/Constants ---
310
+ def declared_variable_name(self, children):
311
+ child = self.transform(children[0])
312
+ return Expression("variable", name=child.value.strip("'\""))
313
+
314
+ def reserved_variable_name(self, children):
315
+ child = self.transform(children[0])
316
+ return Expression("reserved_variable", value=child)
317
+
318
+ def pattern(self, children):
319
+ tree = Tree("pattern", children)
320
+ pattern = PatternBuilder(tree).object
321
+ assert (
322
+ len(pattern.components) == 1
323
+ ), "The pattern {pattern} must consist of a single component, since it is part of an Expression."
324
+ component = pattern.components[0]
325
+
326
+ return Expression("component_pattern", value=component)
327
+
328
+ def defined_constant(self, children):
329
+ child = self.transform(children[0])
330
+ return Expression("defined_constant", name=child.value)
331
+
332
+ # --- Operations ---
333
+ def binary_op_expression(self, children):
334
+ children = [self.transform(c) for c in children]
335
+ left, op, right = children
336
+ return Expression("binary_op", operator=op, left=left, right=right)
337
+
338
+ def binary_op(self, children):
339
+ return children[0]
340
+
341
+ def unary_op_expression(self, children):
342
+ children = [self.transform(c) for c in children]
343
+ op, child = children
344
+ return Expression("unary_op", operator=op, child=child)
345
+
346
+ def unary_op(self, children):
347
+ return children[0]
348
+
349
+ def list_op_expression(self, children):
350
+ children = [self.transform(c) for c in children]
351
+ op_token, *args = children
352
+ return Expression("list_op", operator=op_token.children[0], children=args)
353
+
354
+ # --- Parentheses ---
355
+ def parentheses(self, children):
356
+ children = [self.transform(c) for c in children]
357
+ return Expression("parentheses", child=children[0])
358
+
359
+ # --- Ternary Conditional ---
360
+ def conditional_expression(self, children):
361
+ children = [self.transform(c) for c in children]
362
+ cond, true_expr, false_expr = children
363
+ cond = cond.children[0]
364
+ return Expression(
365
+ "conditional", condition=cond, true_expr=true_expr, false_expr=false_expr
366
+ )
367
+
368
+ # --- Boolean Logic ---
369
+ def comparison(self, children):
370
+ children = [self.transform(c) for c in children]
371
+ left, op, right = children
372
+ return Expression("comparison", operator=op.value, left=left, right=right)
373
+
374
+ def logical_or(self, children):
375
+ children = [self.transform(c) for c in children]
376
+ left, right = children
377
+ return Expression("logical_or", left=left, right=right)
378
+
379
+ def logical_and(self, children):
380
+ children = [self.transform(c) for c in children]
381
+ left, right = children
382
+ return Expression("logical_and", left=left, right=right)
383
+
384
+ def logical_not(self, children):
385
+ children = [self.transform(c) for c in children]
386
+ return Expression("logical_not", child=children[0])
387
+
388
+ # --- Boolean Literals ---
389
+ def TRUE(self, token):
390
+ return Expression("boolean_literal", value=True)
391
+
392
+ def FALSE(self, token):
393
+ return Expression("boolean_literal", value=False)
394
+
395
+ # --- Default Fallthrough ---
396
+ def __default__(self, data, children, meta):
397
+ return Tree(data, children, meta)
398
+
399
+
400
+ def parse_tree_to_expression(tree: Tree) -> Expression:
401
+ """Convert a Lark ParseTree to an Expression object.
402
+
403
+ Note:
404
+ Since there isn't extra logic when converting algebraic expressions,
405
+ we can convert from the Lark representation in-place, without creating
406
+ a new object, hence a Transformer instead of Visitor.
407
+
408
+ Args:
409
+ tree: Lark ParseTree rooted at algebraic_expression.
410
+
411
+ Returns:
412
+ Expression object representing the parsed expression.
413
+ """
414
+ return LarkTreetoExpression().transform(tree)