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 +0 -0
- pykappa/algebra.py +255 -0
- pykappa/grammar.py +414 -0
- pykappa/kappa.lark +201 -0
- pykappa/mixture.py +620 -0
- pykappa/pattern.py +733 -0
- pykappa/rule.py +516 -0
- pykappa/system.py +620 -0
- pykappa/utils.py +264 -0
- pykappa-0.1.5.dist-info/METADATA +66 -0
- pykappa-0.1.5.dist-info/RECORD +13 -0
- pykappa-0.1.5.dist-info/WHEEL +5 -0
- pykappa-0.1.5.dist-info/top_level.txt +1 -0
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)
|