hyperbase 0.8.0__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.
- hyperbase/__init__.py +6 -0
- hyperbase/constants.py +4 -0
- hyperbase/hyperedge.py +1127 -0
- hyperbase/parsers/__init__.py +39 -0
- hyperbase/parsers/correctness.py +265 -0
- hyperbase/parsers/parser.py +41 -0
- hyperbase/parsers/utils.py +19 -0
- hyperbase/patterns/__init__.py +29 -0
- hyperbase/patterns/argroles.py +142 -0
- hyperbase/patterns/atoms.py +98 -0
- hyperbase/patterns/common.py +172 -0
- hyperbase/patterns/counter.py +153 -0
- hyperbase/patterns/entrypoints.py +87 -0
- hyperbase/patterns/matcher.py +245 -0
- hyperbase/patterns/merge.py +52 -0
- hyperbase/patterns/properties.py +59 -0
- hyperbase/patterns/utils.py +118 -0
- hyperbase/patterns/variables.py +161 -0
- hyperbase-0.8.0.dist-info/METADATA +64 -0
- hyperbase-0.8.0.dist-info/RECORD +23 -0
- hyperbase-0.8.0.dist-info/WHEEL +4 -0
- hyperbase-0.8.0.dist-info/licenses/AUTHORS +5 -0
- hyperbase-0.8.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from importlib.metadata import entry_points, EntryPoint
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from hyperbase.parsers.parser import Parser
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def list_parsers() -> dict[str, EntryPoint]:
|
|
8
|
+
"""Return all installed parser plugins.
|
|
9
|
+
|
|
10
|
+
Each plugin registers via the ``hyperbase.parsers`` entry-point group
|
|
11
|
+
in its ``pyproject.toml``::
|
|
12
|
+
|
|
13
|
+
[project.entry-points."hyperbase.parsers"]
|
|
14
|
+
alphabeta = "hyperparser_alphabeta:ParserAlphaBeta"
|
|
15
|
+
"""
|
|
16
|
+
eps = entry_points(group="hyperbase.parsers")
|
|
17
|
+
return {ep.name: ep for ep in eps}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_parser(name: str, **kwargs: Any) -> Parser:
|
|
21
|
+
"""Instantiate a parser plugin by name.
|
|
22
|
+
|
|
23
|
+
Looks up *name* in the ``hyperbase.parsers`` entry-point group and
|
|
24
|
+
returns an instance of the registered :class:`Parser` subclass.
|
|
25
|
+
|
|
26
|
+
Raises :class:`ValueError` if the parser is not installed.
|
|
27
|
+
"""
|
|
28
|
+
parsers = list_parsers()
|
|
29
|
+
if name not in parsers:
|
|
30
|
+
available = ", ".join(sorted(parsers)) or "(none)"
|
|
31
|
+
raise ValueError(
|
|
32
|
+
f"Parser {name!r} is not installed. "
|
|
33
|
+
f"Available parsers: {available}"
|
|
34
|
+
)
|
|
35
|
+
cls = parsers[name].load()
|
|
36
|
+
return cls(**kwargs) # type: ignore[no-any-return]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__all__ = ["Parser", "get_parser", "list_parsers"]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from hyperbase.hyperedge import Hyperedge
|
|
7
|
+
from hyperbase.parsers.utils import filter_alphanumeric_strings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def check_structural_quality(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str, int]]]:
|
|
11
|
+
errors: dict[Hyperedge, list[tuple[str, str, int]]] = {}
|
|
12
|
+
|
|
13
|
+
def _visit(current_edge: Hyperedge) -> None:
|
|
14
|
+
if not current_edge or current_edge.atom:
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
current_errors: list[tuple[str, str, int]] = []
|
|
18
|
+
|
|
19
|
+
# Argrole checks
|
|
20
|
+
try:
|
|
21
|
+
ars = current_edge.argroles()
|
|
22
|
+
ar_counts: Counter[str] = Counter()
|
|
23
|
+
for ar in ars:
|
|
24
|
+
if ar not in 'mspaoixtjrc':
|
|
25
|
+
current_errors.append(('bad-argrole', f"Bad argument role '{ar}'. Should be one of 'mspaoixtjrc'.", 2))
|
|
26
|
+
ar_counts[ar] += 1
|
|
27
|
+
|
|
28
|
+
for role in 'spoiamc':
|
|
29
|
+
if ar_counts[role] > 1:
|
|
30
|
+
current_errors.append((f'duplicate-argrole-{role}', f"Argument role '{role}' should only be used once.", 2))
|
|
31
|
+
except Exception:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
# Junction checks
|
|
35
|
+
try:
|
|
36
|
+
if current_edge[0].mt == 'J':
|
|
37
|
+
types = set([child.mt for child in current_edge[1:]])
|
|
38
|
+
if types != {'R'} and types != {'C'} and types != {'R', 'S'}:
|
|
39
|
+
current_errors.append(('bad-junction-types', "Junction arguments should ideally be all of type 'R[S]' or all of type 'C'.", 3))
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
if current_errors:
|
|
44
|
+
errors[current_edge] = current_errors
|
|
45
|
+
|
|
46
|
+
for child in current_edge:
|
|
47
|
+
_visit(child)
|
|
48
|
+
|
|
49
|
+
if edge:
|
|
50
|
+
_visit(edge)
|
|
51
|
+
return errors
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def badness_check(
|
|
55
|
+
edge: Hyperedge,
|
|
56
|
+
tokens: list[str]
|
|
57
|
+
) -> dict[Any, list[tuple[str, str, int]]]:
|
|
58
|
+
|
|
59
|
+
raw_errors = edge.check_correctness()
|
|
60
|
+
errors: dict[Any, list[tuple[str, str, int]]] = {}
|
|
61
|
+
for k, v in raw_errors.items():
|
|
62
|
+
errors[k] = [(err_type, err_msg, 0) for err_type, err_msg in v]
|
|
63
|
+
|
|
64
|
+
structural_errors = check_structural_quality(edge)
|
|
65
|
+
for k, v2 in structural_errors.items():
|
|
66
|
+
if k in errors:
|
|
67
|
+
errors[k].extend(v2)
|
|
68
|
+
else:
|
|
69
|
+
errors[k] = v2
|
|
70
|
+
|
|
71
|
+
# Only check token matching if we have a valid edge
|
|
72
|
+
if edge:
|
|
73
|
+
try:
|
|
74
|
+
tokens = filter_alphanumeric_strings(tokens)
|
|
75
|
+
roots = filter_alphanumeric_strings([atom.label() for atom in edge.all_atoms()])
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
# Track which tokens and roots have been matched
|
|
79
|
+
matched_tokens: set[int] = set()
|
|
80
|
+
matched_roots: set[int] = set()
|
|
81
|
+
|
|
82
|
+
# Count remaining unmatched instances of each root
|
|
83
|
+
def count_unmatched_roots(root_value: str) -> int:
|
|
84
|
+
count = 0
|
|
85
|
+
for root_idx, root in enumerate(roots):
|
|
86
|
+
if root == root_value and root_idx not in matched_roots:
|
|
87
|
+
count += 1
|
|
88
|
+
return count
|
|
89
|
+
|
|
90
|
+
# Go through each token and try to find matching roots
|
|
91
|
+
for token_idx, token in enumerate(tokens):
|
|
92
|
+
if token_idx in matched_tokens:
|
|
93
|
+
continue # Already matched this token
|
|
94
|
+
|
|
95
|
+
# Try exact match first
|
|
96
|
+
unmatched_root_count = count_unmatched_roots(token)
|
|
97
|
+
if unmatched_root_count > 0:
|
|
98
|
+
matched_tokens.add(token_idx)
|
|
99
|
+
# Find an unmatched instance of this root
|
|
100
|
+
for root_idx, root in enumerate(roots):
|
|
101
|
+
if root == token and root_idx not in matched_roots:
|
|
102
|
+
matched_roots.add(root_idx)
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
else:
|
|
106
|
+
# Try to find a root that matches this token exactly (case (a))
|
|
107
|
+
for root_idx, root in enumerate(roots):
|
|
108
|
+
if root_idx in matched_roots:
|
|
109
|
+
continue # Already matched this root
|
|
110
|
+
|
|
111
|
+
if root == token:
|
|
112
|
+
matched_tokens.add(token_idx)
|
|
113
|
+
matched_roots.add(root_idx)
|
|
114
|
+
break
|
|
115
|
+
|
|
116
|
+
# If no exact match, try to find combination of roots that form this token (case (b))
|
|
117
|
+
if token_idx not in matched_tokens:
|
|
118
|
+
# Look for sequence of consecutive roots that concatenate to form the token
|
|
119
|
+
for root_start_idx in range(len(roots)):
|
|
120
|
+
if root_start_idx in matched_roots:
|
|
121
|
+
continue # This root is already matched
|
|
122
|
+
|
|
123
|
+
concatenated = ""
|
|
124
|
+
root_sequence: list[int] = []
|
|
125
|
+
|
|
126
|
+
for root_idx in range(root_start_idx, len(roots)):
|
|
127
|
+
if root_idx in matched_roots:
|
|
128
|
+
# Can't use matched roots in sequence
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
root = roots[root_idx]
|
|
132
|
+
concatenated += root
|
|
133
|
+
root_sequence.append(root_idx)
|
|
134
|
+
|
|
135
|
+
if concatenated == token:
|
|
136
|
+
# Found a matching sequence
|
|
137
|
+
matched_tokens.add(token_idx)
|
|
138
|
+
for idx in root_sequence:
|
|
139
|
+
matched_roots.add(idx)
|
|
140
|
+
break
|
|
141
|
+
|
|
142
|
+
if len(concatenated) >= len(token):
|
|
143
|
+
# Gone too far or exact match found
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
if token_idx in matched_tokens:
|
|
147
|
+
break # Found a match, no need to try other starting positions
|
|
148
|
+
|
|
149
|
+
# If still no match, try case (c): root that matches this token and subsequent tokens
|
|
150
|
+
if token_idx not in matched_tokens:
|
|
151
|
+
# Look for a root that can match this token plus some following tokens
|
|
152
|
+
for root_idx, root in enumerate(roots):
|
|
153
|
+
if root_idx in matched_roots:
|
|
154
|
+
continue # Already matched
|
|
155
|
+
|
|
156
|
+
concatenated = ""
|
|
157
|
+
token_sequence: list[int] = []
|
|
158
|
+
|
|
159
|
+
for next_token_idx in range(token_idx, len(tokens)):
|
|
160
|
+
if next_token_idx in matched_tokens:
|
|
161
|
+
continue # Already matched
|
|
162
|
+
|
|
163
|
+
concatenated += tokens[next_token_idx]
|
|
164
|
+
token_sequence.append(next_token_idx)
|
|
165
|
+
|
|
166
|
+
if concatenated == root:
|
|
167
|
+
# Found a root that matches multiple tokens
|
|
168
|
+
matched_roots.add(root_idx)
|
|
169
|
+
for idx in token_sequence:
|
|
170
|
+
matched_tokens.add(idx)
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
if len(concatenated) >= len(root):
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
# If still no match, try case (d): multi-token to multi-root concatenation matching
|
|
177
|
+
if token_idx not in matched_tokens:
|
|
178
|
+
# First, try positional matching (existing logic)
|
|
179
|
+
for root_start_idx in range(len(roots)):
|
|
180
|
+
if root_start_idx in matched_roots:
|
|
181
|
+
continue # This root is already matched
|
|
182
|
+
|
|
183
|
+
tokens_concatenated = ""
|
|
184
|
+
roots_concatenated = ""
|
|
185
|
+
token_sequence = []
|
|
186
|
+
root_sequence = []
|
|
187
|
+
|
|
188
|
+
max_tokens = min(len(tokens) - token_idx, len(roots) - root_start_idx)
|
|
189
|
+
|
|
190
|
+
for i in range(max_tokens):
|
|
191
|
+
current_token_idx = token_idx + i
|
|
192
|
+
current_root_idx = root_start_idx + i
|
|
193
|
+
|
|
194
|
+
if current_token_idx in matched_tokens or current_root_idx in matched_roots:
|
|
195
|
+
break # Can't use already matched items
|
|
196
|
+
|
|
197
|
+
tokens_concatenated += tokens[current_token_idx]
|
|
198
|
+
roots_concatenated += roots[current_root_idx]
|
|
199
|
+
token_sequence.append(current_token_idx)
|
|
200
|
+
root_sequence.append(current_root_idx)
|
|
201
|
+
|
|
202
|
+
# Check if concatenations match
|
|
203
|
+
if tokens_concatenated == roots_concatenated and tokens_concatenated:
|
|
204
|
+
# Found a match - mark all as matched
|
|
205
|
+
for idx in token_sequence:
|
|
206
|
+
matched_tokens.add(idx)
|
|
207
|
+
for idx in root_sequence:
|
|
208
|
+
matched_roots.add(idx)
|
|
209
|
+
break
|
|
210
|
+
|
|
211
|
+
# Stop if we've gone too far (tokens longer than reasonable)
|
|
212
|
+
if len(tokens_concatenated) > 10 or len(roots_concatenated) > 10:
|
|
213
|
+
break
|
|
214
|
+
|
|
215
|
+
if token_idx in matched_tokens:
|
|
216
|
+
break # Found a match, no need to try other root positions
|
|
217
|
+
|
|
218
|
+
# If still no match, try non-positional contraction matching (new logic)
|
|
219
|
+
if token_idx not in matched_tokens:
|
|
220
|
+
# Look for contractions by trying to combine this token with the next one
|
|
221
|
+
# and matching against any two available roots in the roots list (not necessarily consecutive)
|
|
222
|
+
if token_idx + 1 < len(tokens) and token_idx + 1 not in matched_tokens:
|
|
223
|
+
token_concat = tokens[token_idx] + tokens[token_idx + 1]
|
|
224
|
+
|
|
225
|
+
# Try to find any two available roots (not necessarily consecutive) that concatenate to the same value
|
|
226
|
+
for root_idx1 in range(len(roots)):
|
|
227
|
+
if root_idx1 in matched_roots:
|
|
228
|
+
continue # Can't use already matched roots
|
|
229
|
+
|
|
230
|
+
for root_idx2 in range(len(roots)):
|
|
231
|
+
if root_idx2 in matched_roots or root_idx2 == root_idx1:
|
|
232
|
+
continue # Can't use already matched roots or same root
|
|
233
|
+
|
|
234
|
+
root_concat = roots[root_idx1] + roots[root_idx2]
|
|
235
|
+
|
|
236
|
+
if token_concat == root_concat:
|
|
237
|
+
# Found a contraction match!
|
|
238
|
+
matched_tokens.add(token_idx)
|
|
239
|
+
matched_tokens.add(token_idx + 1)
|
|
240
|
+
matched_roots.add(root_idx1)
|
|
241
|
+
matched_roots.add(root_idx2)
|
|
242
|
+
break
|
|
243
|
+
|
|
244
|
+
if token_idx in matched_tokens:
|
|
245
|
+
break # Found a match, no need to try other combinations
|
|
246
|
+
|
|
247
|
+
token_matching_errors: list[tuple[str, str, int]] = []
|
|
248
|
+
# Report unmatched roots
|
|
249
|
+
for root_idx, root in enumerate(roots):
|
|
250
|
+
if root_idx not in matched_roots:
|
|
251
|
+
token_matching_errors.append(('root-without-token', f"Atom root '{root}' is used more times than it appears in the original text.", 1))
|
|
252
|
+
|
|
253
|
+
# Report unmatched tokens
|
|
254
|
+
for token_idx, token in enumerate(tokens):
|
|
255
|
+
if token_idx not in matched_tokens:
|
|
256
|
+
token_matching_errors.append(('token-unused', f"Atom root '{token}' is not used, but it appears in the original text.", 1))
|
|
257
|
+
|
|
258
|
+
if len(token_matching_errors) > 0:
|
|
259
|
+
errors['token-matching'] = token_matching_errors
|
|
260
|
+
|
|
261
|
+
except (AttributeError, Exception):
|
|
262
|
+
# If token counting fails (e.g., edge is invalid), skip it
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
return errors
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Iterator
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Parser:
|
|
8
|
+
def sentensize(self, text: str) -> list[str]:
|
|
9
|
+
raise NotImplementedError
|
|
10
|
+
|
|
11
|
+
def parse(self, text: str) -> Iterator[dict[str, Any]]:
|
|
12
|
+
for sentence in self.sentensize(text):
|
|
13
|
+
for parse in self.parse_sentence(sentence):
|
|
14
|
+
yield parse
|
|
15
|
+
|
|
16
|
+
def parse_sentence(self, sentence: str) -> list[dict[str, Any]]:
|
|
17
|
+
raise NotImplementedError
|
|
18
|
+
|
|
19
|
+
def parse_batch(self, sentences: list[str]) -> list[list[dict[str, Any]]]:
|
|
20
|
+
"""Parse multiple sentences. Subclasses may override with a
|
|
21
|
+
true batched implementation (e.g. a single CT2 call)."""
|
|
22
|
+
return [self.parse_sentence(sentence) for sentence in sentences]
|
|
23
|
+
|
|
24
|
+
def parse_text(
|
|
25
|
+
self, text: str, batch_size: int = 8, progress: bool = False
|
|
26
|
+
) -> list[dict[str, Any]]:
|
|
27
|
+
"""Sentensize text, then parse all sentences in batches.
|
|
28
|
+
|
|
29
|
+
Returns a flat list of parse results across all sentences.
|
|
30
|
+
"""
|
|
31
|
+
sentences = [s for s in self.sentensize(text) if len(s.split()) > 1]
|
|
32
|
+
batch_range = range(0, len(sentences), batch_size)
|
|
33
|
+
if progress:
|
|
34
|
+
from tqdm import tqdm # type: ignore[import-untyped]
|
|
35
|
+
batch_range = tqdm(batch_range, desc="Parsing batches", leave=False)
|
|
36
|
+
results: list[dict[str, Any]] = []
|
|
37
|
+
for i in batch_range:
|
|
38
|
+
batch = sentences[i:i + batch_size]
|
|
39
|
+
for sentence_results in self.parse_batch(batch):
|
|
40
|
+
results.extend(sentence_results)
|
|
41
|
+
return results
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
def filter_alphanumeric_strings(strings: list[str]) -> list[str]:
|
|
2
|
+
"""
|
|
3
|
+
Filter a list of strings to include only those containing alphanumeric characters,
|
|
4
|
+
and remove all non-alphanumeric characters from each string.
|
|
5
|
+
|
|
6
|
+
Args:
|
|
7
|
+
strings: List of strings to filter
|
|
8
|
+
|
|
9
|
+
Returns:
|
|
10
|
+
Filtered list containing only lowercased alphanumeric characters
|
|
11
|
+
"""
|
|
12
|
+
filtered: list[str] = []
|
|
13
|
+
for s in strings:
|
|
14
|
+
# Remove non-alphanumeric characters and lowercase
|
|
15
|
+
cleaned = ''.join(c.lower() for c in s if c.isalnum())
|
|
16
|
+
# Only include if result is non-empty
|
|
17
|
+
if cleaned:
|
|
18
|
+
filtered.append(cleaned)
|
|
19
|
+
return filtered
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from hyperbase.patterns.common import common_pattern
|
|
2
|
+
from hyperbase.patterns.entrypoints import match_pattern, edge_matches_pattern
|
|
3
|
+
from hyperbase.patterns.merge import merge_patterns
|
|
4
|
+
from hyperbase.patterns.properties import (is_wildcard, is_pattern, is_full_pattern, is_fun_pattern,
|
|
5
|
+
is_unordered_pattern)
|
|
6
|
+
from hyperbase.patterns.utils import more_general
|
|
7
|
+
from hyperbase.patterns.variables import (all_variables, apply_vars, apply_variables, extract_vars_map, is_variable,
|
|
8
|
+
contains_variable, remove_variables)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
'all_variables',
|
|
13
|
+
'apply_vars',
|
|
14
|
+
'apply_variables',
|
|
15
|
+
'common_pattern',
|
|
16
|
+
'contains_variable',
|
|
17
|
+
'edge_matches_pattern',
|
|
18
|
+
'extract_vars_map',
|
|
19
|
+
'is_full_pattern',
|
|
20
|
+
'is_fun_pattern',
|
|
21
|
+
'is_pattern',
|
|
22
|
+
'is_unordered_pattern',
|
|
23
|
+
'is_variable',
|
|
24
|
+
'is_wildcard',
|
|
25
|
+
'match_pattern',
|
|
26
|
+
'merge_patterns',
|
|
27
|
+
'more_general',
|
|
28
|
+
'remove_variables'
|
|
29
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import itertools
|
|
4
|
+
from collections.abc import Iterator, Mapping, Sequence
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from hyperbase.hyperedge import Hyperedge, hedge
|
|
8
|
+
from hyperbase.patterns.utils import _defun_pattern_argroles
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from hyperbase.patterns.matcher import Matcher
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _match_by_argroles(
|
|
15
|
+
matcher: Matcher,
|
|
16
|
+
edge: Hyperedge,
|
|
17
|
+
pattern: Hyperedge,
|
|
18
|
+
role_counts: list[tuple[str, int]],
|
|
19
|
+
min_vars: int,
|
|
20
|
+
matched: tuple[Hyperedge, ...] = (),
|
|
21
|
+
curvars: dict[str, Hyperedge] | None = None,
|
|
22
|
+
tok_pos: list[int] | None = None
|
|
23
|
+
) -> list[dict[str, Hyperedge]]:
|
|
24
|
+
if curvars is None:
|
|
25
|
+
curvars = {}
|
|
26
|
+
|
|
27
|
+
if len(role_counts) == 0:
|
|
28
|
+
return [curvars]
|
|
29
|
+
|
|
30
|
+
argrole, n = role_counts[0]
|
|
31
|
+
|
|
32
|
+
# match connector
|
|
33
|
+
if argrole == 'X':
|
|
34
|
+
eitems = [edge[0]]
|
|
35
|
+
pitems = [pattern[0]]
|
|
36
|
+
# match any argrole
|
|
37
|
+
elif argrole == '*':
|
|
38
|
+
eitems = [e for e in edge if e not in matched]
|
|
39
|
+
pitems = list(pattern[-n:])
|
|
40
|
+
# match specific argrole
|
|
41
|
+
else:
|
|
42
|
+
eitems = edge.edges_with_argrole(argrole)
|
|
43
|
+
pitems = _defun_pattern_argroles(pattern).edges_with_argrole(argrole)
|
|
44
|
+
|
|
45
|
+
if len(eitems) < n:
|
|
46
|
+
if len(curvars) >= min_vars:
|
|
47
|
+
return [curvars]
|
|
48
|
+
else:
|
|
49
|
+
return []
|
|
50
|
+
|
|
51
|
+
result: list[dict[str, Hyperedge]] = []
|
|
52
|
+
|
|
53
|
+
if tok_pos:
|
|
54
|
+
tok_pos_items = [tok_pos[i] for i, subedge in enumerate(edge) if subedge in eitems]
|
|
55
|
+
tok_pos_perms = tuple(itertools.permutations(tok_pos_items, r=n))
|
|
56
|
+
|
|
57
|
+
for perm_n, perm in enumerate(tuple(itertools.permutations(eitems, r=n))):
|
|
58
|
+
if tok_pos:
|
|
59
|
+
tok_pos_perm = tok_pos_perms[perm_n]
|
|
60
|
+
perm_result: list[dict[str, Hyperedge]] = [{}]
|
|
61
|
+
for i, eitem in enumerate(perm):
|
|
62
|
+
pitem = pitems[i]
|
|
63
|
+
tok_pos_item = tok_pos_perm[i] if tok_pos else None
|
|
64
|
+
item_result: list[dict[str, Hyperedge]] = []
|
|
65
|
+
for variables in perm_result:
|
|
66
|
+
item_result += matcher.match(
|
|
67
|
+
eitem,
|
|
68
|
+
pitem,
|
|
69
|
+
{**curvars, **variables},
|
|
70
|
+
tok_pos=tok_pos_item
|
|
71
|
+
)
|
|
72
|
+
perm_result = item_result
|
|
73
|
+
if len(item_result) == 0:
|
|
74
|
+
break
|
|
75
|
+
|
|
76
|
+
for variables in perm_result:
|
|
77
|
+
result += _match_by_argroles(
|
|
78
|
+
matcher,
|
|
79
|
+
edge,
|
|
80
|
+
pattern,
|
|
81
|
+
role_counts[1:],
|
|
82
|
+
min_vars,
|
|
83
|
+
matched + perm,
|
|
84
|
+
{**curvars, **variables},
|
|
85
|
+
tok_pos=tok_pos
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def edge2rolemap(edge: Hyperedge) -> dict[str, list[Hyperedge]]:
|
|
92
|
+
argroles = edge[0].argroles()
|
|
93
|
+
if argroles[0] == '{':
|
|
94
|
+
argroles = argroles[1:-1]
|
|
95
|
+
args = list(zip(argroles, edge[1:]))
|
|
96
|
+
rolemap: dict[str, list[Hyperedge]] = {}
|
|
97
|
+
for role, subedge in args:
|
|
98
|
+
if role not in rolemap:
|
|
99
|
+
rolemap[role] = []
|
|
100
|
+
rolemap[role].append(subedge)
|
|
101
|
+
return rolemap
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def rolemap2edge(pred: Hyperedge, rm: Mapping[str, Sequence[Hyperedge]]) -> Hyperedge:
|
|
105
|
+
roles = list(rm.keys())
|
|
106
|
+
argroles = ''
|
|
107
|
+
subedges: list[Hyperedge] = [pred]
|
|
108
|
+
for role in roles:
|
|
109
|
+
for arg in rm[role]:
|
|
110
|
+
argroles += role
|
|
111
|
+
subedges.append(arg)
|
|
112
|
+
result = hedge(subedges)
|
|
113
|
+
assert result is not None
|
|
114
|
+
return result.replace_argroles(argroles)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def rolemap_pairings(
|
|
118
|
+
rm1: dict[str, list[Hyperedge]],
|
|
119
|
+
rm2: dict[str, list[Hyperedge]]
|
|
120
|
+
) -> Iterator[tuple[dict[str, tuple[Hyperedge, ...]], dict[str, tuple[Hyperedge, ...]]]]:
|
|
121
|
+
roles = list(set(rm1.keys()) & set(rm2.keys()))
|
|
122
|
+
role_counts: dict[str, int] = {}
|
|
123
|
+
for role in roles:
|
|
124
|
+
role_counts[role] = min(len(rm1[role]), len(rm2[role]))
|
|
125
|
+
|
|
126
|
+
pairings: list[list[tuple[tuple[Hyperedge, ...], tuple[Hyperedge, ...]]]] = []
|
|
127
|
+
for role in roles:
|
|
128
|
+
role_pairings: list[tuple[tuple[Hyperedge, ...], tuple[Hyperedge, ...]]] = []
|
|
129
|
+
n = role_counts[role]
|
|
130
|
+
for args1_combs in itertools.combinations(rm1[role], n):
|
|
131
|
+
for args1 in itertools.permutations(args1_combs):
|
|
132
|
+
for args2 in itertools.combinations(rm2[role], n):
|
|
133
|
+
role_pairings.append((args1, args2))
|
|
134
|
+
pairings.append(role_pairings)
|
|
135
|
+
|
|
136
|
+
for pairing in itertools.product(*pairings):
|
|
137
|
+
rm1_: dict[str, tuple[Hyperedge, ...]] = {}
|
|
138
|
+
rm2_: dict[str, tuple[Hyperedge, ...]] = {}
|
|
139
|
+
for role, role_pairing in zip(roles, pairing):
|
|
140
|
+
rm1_[role] = role_pairing[0]
|
|
141
|
+
rm2_[role] = role_pairing[1]
|
|
142
|
+
yield rm1_, rm2_
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
from hyperbase.hyperedge import Hyperedge
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _matches_atomic_pattern(edge: Hyperedge, atomic_pattern: Hyperedge) -> bool:
|
|
5
|
+
ap_parts = atomic_pattern.parts() # type: ignore[attr-defined]
|
|
6
|
+
|
|
7
|
+
if len(ap_parts) == 0 or len(ap_parts[0]) == 0:
|
|
8
|
+
return False
|
|
9
|
+
|
|
10
|
+
# structural match
|
|
11
|
+
struct_code = ap_parts[0][0]
|
|
12
|
+
if struct_code == '.':
|
|
13
|
+
if edge.not_atom:
|
|
14
|
+
return False
|
|
15
|
+
elif atomic_pattern.parens: # type: ignore[attr-defined]
|
|
16
|
+
if edge.atom:
|
|
17
|
+
return False
|
|
18
|
+
elif struct_code != '*' and not struct_code.isupper():
|
|
19
|
+
if edge.not_atom:
|
|
20
|
+
return False
|
|
21
|
+
if edge.root() != atomic_pattern.root(): # type: ignore[attr-defined]
|
|
22
|
+
return False
|
|
23
|
+
|
|
24
|
+
# role match
|
|
25
|
+
if len(ap_parts) > 1:
|
|
26
|
+
pos = 1
|
|
27
|
+
|
|
28
|
+
# type match
|
|
29
|
+
ap_role = atomic_pattern.role() # type: ignore[attr-defined]
|
|
30
|
+
ap_type = ap_role[0]
|
|
31
|
+
e_type = edge.type()
|
|
32
|
+
n = len(ap_type)
|
|
33
|
+
if len(e_type) < n or e_type[:n] != ap_type:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
e_atom = edge.inner_atom()
|
|
37
|
+
|
|
38
|
+
if len(ap_role) > 1:
|
|
39
|
+
e_role = e_atom.role()
|
|
40
|
+
# check if edge role has enough parts to satisfy the wildcard
|
|
41
|
+
# specification
|
|
42
|
+
if len(e_role) < len(ap_role):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
# argroles match
|
|
46
|
+
if ap_type[0] in {'B', 'P'}:
|
|
47
|
+
ap_argroles_parts = ap_role[1].split('-')
|
|
48
|
+
if len(ap_argroles_parts) == 1:
|
|
49
|
+
ap_argroles_parts.append('')
|
|
50
|
+
ap_negroles = ap_argroles_parts[1]
|
|
51
|
+
|
|
52
|
+
# fixed order?
|
|
53
|
+
ap_argroles_posopt = ap_argroles_parts[0]
|
|
54
|
+
e_argroles = e_role[1]
|
|
55
|
+
if len(ap_argroles_posopt) > 0 and ap_argroles_posopt[0] == '{':
|
|
56
|
+
ap_argroles_posopt = ap_argroles_posopt[1:-1]
|
|
57
|
+
else:
|
|
58
|
+
ap_argroles_posopt = ap_argroles_posopt.replace(',', '')
|
|
59
|
+
if len(e_argroles) > len(ap_argroles_posopt):
|
|
60
|
+
return False
|
|
61
|
+
else:
|
|
62
|
+
return ap_argroles_posopt.startswith(e_argroles) # type: ignore[no-any-return]
|
|
63
|
+
|
|
64
|
+
ap_argroles_parts = ap_argroles_posopt.split(',')
|
|
65
|
+
ap_posroles = ap_argroles_parts[0]
|
|
66
|
+
ap_argroles = set(ap_posroles) | set(ap_negroles)
|
|
67
|
+
for argrole in ap_argroles:
|
|
68
|
+
min_count = ap_posroles.count(argrole)
|
|
69
|
+
# if there are argrole exclusions
|
|
70
|
+
fixed = ap_negroles.count(argrole) > 0
|
|
71
|
+
count = e_argroles.count(argrole)
|
|
72
|
+
if count < min_count:
|
|
73
|
+
return False
|
|
74
|
+
# deal with exclusions
|
|
75
|
+
if fixed and count > min_count:
|
|
76
|
+
return False
|
|
77
|
+
pos = 2
|
|
78
|
+
|
|
79
|
+
# match rest of role
|
|
80
|
+
while pos < len(ap_role):
|
|
81
|
+
if e_role[pos] != ap_role[pos]:
|
|
82
|
+
return False
|
|
83
|
+
pos += 1
|
|
84
|
+
|
|
85
|
+
# match rest of atom
|
|
86
|
+
if len(ap_parts) > 2:
|
|
87
|
+
e_parts = e_atom.parts()
|
|
88
|
+
# check if edge role has enough parts to satisfy the wildcard
|
|
89
|
+
# specification
|
|
90
|
+
if len(e_parts) < len(ap_parts):
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
while pos < len(ap_parts):
|
|
94
|
+
if e_parts[pos] != ap_parts[pos]:
|
|
95
|
+
return False
|
|
96
|
+
pos += 1
|
|
97
|
+
|
|
98
|
+
return True
|