Mercurial > code > home > repos > homeauto
comparison service/mqtt_to_rdf/inference.py @ 1594:e58bcfa66093
cleanups and a few fixed cases
author | drewp@bigasterisk.com |
---|---|
date | Sun, 05 Sep 2021 01:15:55 -0700 |
parents | b0df43d5494c |
children | 4e795ed3a693 |
comparison
equal
deleted
inserted
replaced
1593:b0df43d5494c | 1594:e58bcfa66093 |
---|---|
1 """ | 1 """ |
2 copied from reasoning 2021-08-29. probably same api. should | 2 copied from reasoning 2021-08-29. probably same api. should |
3 be able to lib/ this out | 3 be able to lib/ this out |
4 """ | 4 """ |
5 from collections import defaultdict | |
6 import itertools | 5 import itertools |
7 import logging | 6 import logging |
8 from dataclasses import dataclass | 7 from collections import defaultdict |
8 from dataclasses import dataclass, field | |
9 from decimal import Decimal | 9 from decimal import Decimal |
10 from typing import Dict, Iterator, List, Set, Tuple, Union, cast | 10 from typing import Dict, Iterator, List, Set, Tuple, Union, cast |
11 from urllib.request import OpenerDirector | |
12 | 11 |
13 from prometheus_client import Summary | 12 from prometheus_client import Summary |
14 from rdflib import BNode, Graph, Literal, Namespace, URIRef, RDF | 13 from rdflib import RDF, BNode, Graph, Literal, Namespace, URIRef |
15 from rdflib.collection import Collection | |
16 from rdflib.graph import ConjunctiveGraph, ReadOnlyGraphAggregate | 14 from rdflib.graph import ConjunctiveGraph, ReadOnlyGraphAggregate |
17 from rdflib.term import Node, Variable | 15 from rdflib.term import Node, Variable |
18 | 16 |
19 log = logging.getLogger('infer') | 17 log = logging.getLogger('infer') |
20 INDENT = ' ' | 18 INDENT = ' ' |
21 | 19 |
22 Triple = Tuple[Node, Node, Node] | 20 Triple = Tuple[Node, Node, Node] |
23 Rule = Tuple[Graph, Node, Graph] | 21 Rule = Tuple[Graph, Node, Graph] |
24 BindableTerm = Union[Variable, BNode] | 22 BindableTerm = Union[Variable, BNode] |
23 ReadOnlyWorkingSet = ReadOnlyGraphAggregate | |
25 | 24 |
26 READ_RULES_CALLS = Summary('read_rules_calls', 'calls') | 25 READ_RULES_CALLS = Summary('read_rules_calls', 'calls') |
27 | 26 |
28 ROOM = Namespace("http://projects.bigasterisk.com/room/") | 27 ROOM = Namespace("http://projects.bigasterisk.com/room/") |
29 LOG = Namespace('http://www.w3.org/2000/10/swap/log#') | 28 LOG = Namespace('http://www.w3.org/2000/10/swap/log#') |
30 MATH = Namespace('http://www.w3.org/2000/10/swap/math#') | 29 MATH = Namespace('http://www.w3.org/2000/10/swap/math#') |
31 | 30 |
32 | 31 |
32 class EvaluationFailed(ValueError): | |
33 """e.g. we were given (5 math:greaterThan 6)""" | |
34 | |
35 | |
33 @dataclass | 36 @dataclass |
34 class _RuleMatch: | |
35 """one way that a rule can match the working set""" | |
36 vars: Dict[Variable, Node] | |
37 | |
38 | |
39 ReadOnlyWorkingSet = ReadOnlyGraphAggregate | |
40 | |
41 filterFuncs = { | |
42 MATH['greaterThan'], | |
43 } | |
44 | |
45 | |
46 class CandidateBinding: | 37 class CandidateBinding: |
47 | 38 binding: Dict[BindableTerm, Node] |
48 def __init__(self, binding: Dict[BindableTerm, Node]): | |
49 self.binding = binding # mutable! | |
50 | 39 |
51 def __repr__(self): | 40 def __repr__(self): |
52 b = " ".join("%s=%s" % (k, v) for k, v in sorted(self.binding.items())) | 41 b = " ".join("%s=%s" % (k, v) for k, v in sorted(self.binding.items())) |
53 return f'CandidateBinding({b})' | 42 return f'CandidateBinding({b})' |
54 | 43 |
55 def apply(self, g: Graph) -> Iterator[Triple]: | 44 def apply(self, g: Graph) -> Iterator[Triple]: |
56 for stmt in g: | 45 for stmt in g: |
57 stmt = list(stmt) | 46 yield (self._applyTerm(stmt[0]), self._applyTerm(stmt[1]), self._applyTerm(stmt[2])) |
58 for i, term in enumerate(stmt): | 47 |
59 if isinstance(term, (Variable, BNode)): | 48 def _applyTerm(self, term: Node): |
60 if term in self.binding: | 49 if isinstance(term, (Variable, BNode)): |
61 stmt[i] = self.binding[term] | 50 if term in self.binding: |
62 else: | 51 return self.binding[term] |
63 yield cast(Triple, stmt) | 52 return term |
64 | 53 |
65 def applyFunctions(self, lhs): | 54 def applyFunctions(self, lhs) -> Graph: |
66 """may grow the binding with some results""" | 55 """may grow the binding with some results""" |
67 usedByFuncs = Graph() | 56 usedByFuncs = Graph() |
68 while True: | 57 while True: |
69 before = len(self.binding) | 58 delta = self._applyFunctionsIteration(lhs, usedByFuncs) |
70 delta = 0 | |
71 for ev in Evaluation.findEvals(lhs): | |
72 log.debug(f'{INDENT*3} found Evaluation') | |
73 | |
74 newBindings, usedGraph = ev.resultBindings(self.binding) | |
75 usedByFuncs += usedGraph | |
76 for k, v in newBindings.items(): | |
77 if k in self.binding and self.binding[k] != v: | |
78 raise ValueError( | |
79 f'conflict- thought {k} would be {self.binding[k]} but another Evaluation said it should be {v}') | |
80 self.binding[k] = v | |
81 delta = len(self.binding) - before | |
82 log.debug(f'{INDENT*4} rule {graphDump(usedGraph)} made {delta} new bindings') | |
83 if delta == 0: | 59 if delta == 0: |
84 break | 60 break |
85 return usedByFuncs | 61 return usedByFuncs |
86 | 62 |
63 def _applyFunctionsIteration(self, lhs, usedByFuncs: Graph): | |
64 before = len(self.binding) | |
65 delta = 0 | |
66 for ev in Evaluation.findEvals(lhs): | |
67 log.debug(f'{INDENT*3} found Evaluation') | |
68 | |
69 newBindings, usedGraph = ev.resultBindings(self.binding) | |
70 usedByFuncs += usedGraph | |
71 self._addNewBindings(newBindings) | |
72 delta = len(self.binding) - before | |
73 dump = "(...)" | |
74 if log.isEnabledFor(logging.DEBUG) and cast(int, usedGraph.__len__()) < 20: | |
75 dump = graphDump(usedGraph) | |
76 log.debug(f'{INDENT*4} rule {dump} made {delta} new bindings') | |
77 return delta | |
78 | |
79 def _addNewBindings(self, newBindings): | |
80 for k, v in newBindings.items(): | |
81 if k in self.binding and self.binding[k] != v: | |
82 raise ValueError(f'conflict- thought {k} would be {self.binding[k]} but another Evaluation said it should be {v}') | |
83 self.binding[k] = v | |
84 | |
87 def verify(self, lhs: 'Lhs', workingSet: ReadOnlyWorkingSet, usedByFuncs: Graph) -> bool: | 85 def verify(self, lhs: 'Lhs', workingSet: ReadOnlyWorkingSet, usedByFuncs: Graph) -> bool: |
88 """Can this lhs be true all at once in workingSet? Does it match with these bindings?""" | 86 """Can this lhs be true all at once in workingSet? Does it match with these bindings?""" |
89 boundLhs = list(self.apply(lhs._g)) | 87 boundLhs = list(self.apply(lhs.graph)) |
90 boundUsedByFuncs = list(self.apply(usedByFuncs)) | 88 boundUsedByFuncs = list(self.apply(usedByFuncs)) |
91 | 89 |
92 self.logVerifyBanner(boundLhs, workingSet, boundUsedByFuncs) | 90 self.logVerifyBanner(boundLhs, workingSet, boundUsedByFuncs) |
93 | 91 |
94 for stmt in boundLhs: | 92 for stmt in boundLhs: |
95 log.debug(f'{INDENT*4} check for {stmt}') | 93 log.debug(f'{INDENT*4} check for {stmt}') |
96 | 94 |
97 if stmt[1] in filterFuncs: | 95 if stmt in boundUsedByFuncs: |
98 if not mathTest(*stmt): | |
99 log.debug(f'{INDENT*5} binding was invalid because {stmt}) is not true') | |
100 return False | |
101 elif stmt in boundUsedByFuncs: | |
102 pass | 96 pass |
103 elif stmt in workingSet: | 97 elif stmt in workingSet: |
104 pass | 98 pass |
105 else: | 99 else: |
106 log.debug(f'{INDENT*5} binding was invalid because {stmt}) is not known to be true') | 100 log.debug(f'{INDENT*5} binding was invalid because {stmt}) is not known to be true') |
123 for stmt in sorted(boundUsedByFuncs): | 117 for stmt in sorted(boundUsedByFuncs): |
124 log.debug(f'{INDENT*4}|{INDENT} {stmt}') | 118 log.debug(f'{INDENT*4}|{INDENT} {stmt}') |
125 log.debug(f'{INDENT*4}\\') | 119 log.debug(f'{INDENT*4}\\') |
126 | 120 |
127 | 121 |
122 @dataclass | |
128 class Lhs: | 123 class Lhs: |
129 | 124 graph: Graph |
130 def __init__(self, existingGraph): | 125 |
131 self._g = existingGraph | 126 staticRuleStmts: Graph = field(default_factory=Graph) |
127 lhsBindables: Set[BindableTerm] = field(default_factory=set) | |
128 lhsBnodes: Set[BNode] = field(default_factory=set) | |
129 | |
130 def __post_init__(self): | |
131 for ruleStmt in self.graph: | |
132 varsAndBnodesInStmt = [term for term in ruleStmt if isinstance(term, (Variable, BNode))] | |
133 self.lhsBindables.update(varsAndBnodesInStmt) | |
134 self.lhsBnodes.update(x for x in varsAndBnodesInStmt if isinstance(x, BNode)) | |
135 if not varsAndBnodesInStmt: | |
136 self.staticRuleStmts.add(ruleStmt) | |
132 | 137 |
133 def findCandidateBindings(self, workingSet: ReadOnlyWorkingSet) -> Iterator[CandidateBinding]: | 138 def findCandidateBindings(self, workingSet: ReadOnlyWorkingSet) -> Iterator[CandidateBinding]: |
134 """bindings that fit the LHS of a rule, using statements from workingSet and functions | 139 """bindings that fit the LHS of a rule, using statements from workingSet and functions |
135 from LHS""" | 140 from LHS""" |
136 nodesToBind = self.nodesToBind() | 141 log.debug(f'{INDENT*2} nodesToBind: {self.lhsBindables}') |
137 log.debug(f'{INDENT*2} nodesToBind: {nodesToBind}') | |
138 | 142 |
139 if not self.allStaticStatementsMatch(workingSet): | 143 if not self.allStaticStatementsMatch(workingSet): |
140 return | 144 return |
141 | 145 |
142 candidateTermMatches: Dict[BindableTerm, Set[Node]] = self.allCandidateTermMatches(workingSet) | 146 candidateTermMatches: Dict[BindableTerm, Set[Node]] = self.allCandidateTermMatches(workingSet) |
143 | |
144 # for n in nodesToBind: | |
145 # if n not in candidateTermMatches: | |
146 # candidateTermMatches[n] = set() | |
147 | 147 |
148 orderedVars, orderedValueSets = organize(candidateTermMatches) | 148 orderedVars, orderedValueSets = organize(candidateTermMatches) |
149 | 149 |
150 self.logCandidates(orderedVars, orderedValueSets) | 150 self.logCandidates(orderedVars, orderedValueSets) |
151 | 151 |
154 for perm in itertools.product(*orderedValueSets): | 154 for perm in itertools.product(*orderedValueSets): |
155 binding = CandidateBinding(dict(zip(orderedVars, perm))) | 155 binding = CandidateBinding(dict(zip(orderedVars, perm))) |
156 log.debug('') | 156 log.debug('') |
157 log.debug(f'{INDENT*3}*trying {binding}') | 157 log.debug(f'{INDENT*3}*trying {binding}') |
158 | 158 |
159 usedByFuncs = binding.applyFunctions(self) | 159 try: |
160 usedByFuncs = binding.applyFunctions(self) | |
161 except EvaluationFailed: | |
162 continue | |
160 | 163 |
161 if not binding.verify(self, workingSet, usedByFuncs): | 164 if not binding.verify(self, workingSet, usedByFuncs): |
162 log.debug(f'{INDENT*3} this binding did not verify') | 165 log.debug(f'{INDENT*3} this binding did not verify') |
163 continue | 166 continue |
164 yield binding | 167 yield binding |
165 | 168 |
166 def nodesToBind(self) -> List[BindableTerm]: | |
167 nodes: Set[BindableTerm] = set() | |
168 staticRuleStmts = Graph() | |
169 for ruleStmt in self._g: | |
170 varsInStmt = [v for v in ruleStmt if isinstance(v, (Variable, BNode))] | |
171 nodes.update(varsInStmt) | |
172 if (not varsInStmt # ok | |
173 #and not any(isinstance(t, BNode) for t in ruleStmt) # approx | |
174 ): | |
175 staticRuleStmts.add(ruleStmt) | |
176 return sorted(nodes) | |
177 | |
178 def allStaticStatementsMatch(self, workingSet: ReadOnlyWorkingSet) -> bool: | 169 def allStaticStatementsMatch(self, workingSet: ReadOnlyWorkingSet) -> bool: |
179 staticRuleStmts = Graph() | 170 for ruleStmt in self.staticRuleStmts: |
180 for ruleStmt in self._g: | |
181 varsInStmt = [v for v in ruleStmt if isinstance(v, (Variable, BNode))] | |
182 if (not varsInStmt # ok | |
183 #and not any(isinstance(t, BNode) for t in ruleStmt) # approx | |
184 ): | |
185 staticRuleStmts.add(ruleStmt) | |
186 | |
187 for ruleStmt in staticRuleStmts: | |
188 if ruleStmt not in workingSet: | 171 if ruleStmt not in workingSet: |
189 log.debug(f'{INDENT*3} {ruleStmt} not in working set- skip rule') | 172 log.debug(f'{INDENT*3} {ruleStmt} not in working set- skip rule') |
190 return False | 173 return False |
191 return True | 174 return True |
192 | 175 |
193 def allCandidateTermMatches(self, workingSet: ReadOnlyWorkingSet) -> Dict[BindableTerm, Set[Node]]: | 176 def allCandidateTermMatches(self, workingSet: ReadOnlyWorkingSet) -> Dict[BindableTerm, Set[Node]]: |
194 """the total set of terms each variable could possibly match""" | 177 """the total set of terms each variable could possibly match""" |
195 | 178 |
196 candidateTermMatches: Dict[BindableTerm, Set[Node]] = defaultdict(set) | 179 candidateTermMatches: Dict[BindableTerm, Set[Node]] = defaultdict(set) |
197 lhsBnodes: Set[BNode] = set() | 180 for lhsStmt in self.graph: |
198 for lhsStmt in self._g: | |
199 log.debug(f'{INDENT*3} possibles for this lhs stmt: {lhsStmt}') | 181 log.debug(f'{INDENT*3} possibles for this lhs stmt: {lhsStmt}') |
200 for i, trueStmt in enumerate(sorted(workingSet)): | 182 for i, trueStmt in enumerate(sorted(workingSet)): |
201 log.debug(f'{INDENT*4} consider this true stmt ({i}): {trueStmt}') | 183 log.debug(f'{INDENT*4} consider this true stmt ({i}): {trueStmt}') |
202 bindingsFromStatement: Dict[Variable, Set[Node]] = {} | 184 |
203 for lhsTerm, trueTerm in zip(lhsStmt, trueStmt): | 185 for v, vals in self._bindingsFromStatement(lhsStmt, trueStmt): |
204 if isinstance(lhsTerm, BNode): | 186 candidateTermMatches[v].update(vals) |
205 lhsBnodes.add(lhsTerm) | 187 |
206 elif isinstance(lhsTerm, Variable): | 188 for trueStmt in itertools.chain(workingSet, self.graph): |
207 bindingsFromStatement.setdefault(lhsTerm, set()).add(trueTerm) | 189 for b in self.lhsBnodes: |
208 elif lhsTerm != trueTerm: | |
209 break | |
210 else: | |
211 for v, vals in bindingsFromStatement.items(): | |
212 candidateTermMatches[v].update(vals) | |
213 | |
214 for trueStmt in itertools.chain(workingSet, self._g): | |
215 for b in lhsBnodes: | |
216 for t in [trueStmt[0], trueStmt[2]]: | 190 for t in [trueStmt[0], trueStmt[2]]: |
217 if isinstance(t, (URIRef, BNode)): | 191 if isinstance(t, (URIRef, BNode)): |
218 candidateTermMatches[b].add(t) | 192 candidateTermMatches[b].add(t) |
219 return candidateTermMatches | 193 return candidateTermMatches |
220 | 194 |
195 def _bindingsFromStatement(self, stmt1: Triple, stmt2: Triple) -> Iterator[Tuple[Variable, Set[Node]]]: | |
196 """if these stmts match otherwise, what BNode or Variable mappings do we learn? | |
197 | |
198 e.g. stmt1=(?x B ?y) and stmt2=(A B C), then we yield (?x, {A}) and (?y, {C}) | |
199 or stmt1=(_:x B C) and stmt2=(A B C), then we yield (_:x, {A}) | |
200 or stmt1=(?x B C) and stmt2=(A B D), then we yield nothing | |
201 """ | |
202 bindingsFromStatement = {} | |
203 for term1, term2 in zip(stmt1, stmt2): | |
204 if isinstance(term1, (BNode, Variable)): | |
205 bindingsFromStatement.setdefault(term1, set()).add(term2) | |
206 elif term1 != term2: | |
207 break | |
208 else: | |
209 for v, vals in bindingsFromStatement.items(): | |
210 yield v, vals | |
211 | |
221 def graphWithoutEvals(self, binding: CandidateBinding) -> Graph: | 212 def graphWithoutEvals(self, binding: CandidateBinding) -> Graph: |
222 g = Graph() | 213 g = Graph() |
223 usedByFuncs = binding.applyFunctions(self) | 214 usedByFuncs = binding.applyFunctions(self) |
224 | 215 |
225 for stmt in self._g: | 216 for stmt in self.graph: |
226 if stmt not in usedByFuncs: | 217 if stmt not in usedByFuncs: |
227 g.add(stmt) | 218 g.add(stmt) |
228 return g | 219 return g |
229 | 220 |
230 def logCandidates(self, orderedVars, orderedValueSets): | 221 def logCandidates(self, orderedVars, orderedValueSets): |
239 | 230 |
240 class Evaluation: | 231 class Evaluation: |
241 """some lhs statements need to be evaluated with a special function | 232 """some lhs statements need to be evaluated with a special function |
242 (e.g. math) and then not considered for the rest of the rule-firing | 233 (e.g. math) and then not considered for the rest of the rule-firing |
243 process. It's like they already 'matched' something, so they don't need | 234 process. It's like they already 'matched' something, so they don't need |
244 to match a statement from the known-true working set.""" | 235 to match a statement from the known-true working set. |
236 | |
237 One Evaluation instance is for one function call. | |
238 """ | |
245 | 239 |
246 @staticmethod | 240 @staticmethod |
247 def findEvals(lhs: Lhs) -> Iterator['Evaluation']: | 241 def findEvals(lhs: Lhs) -> Iterator['Evaluation']: |
248 for stmt in lhs._g.triples((None, MATH['sum'], None)): | 242 for stmt in lhs.graph.triples((None, MATH['sum'], None)): |
249 # shouldn't be redoing this here | 243 operands, operandsStmts = parseList(lhs.graph, stmt[0]) |
250 operands, operandsStmts = parseList(lhs._g, stmt[0]) | |
251 g = Graph() | 244 g = Graph() |
252 g += operandsStmts | 245 g += operandsStmts |
253 yield Evaluation(operands, g, stmt) | 246 yield Evaluation(operands, g, stmt) |
254 | 247 |
255 for stmt in lhs._g.triples((None, ROOM['asFarenheit'], None)): | 248 for stmt in lhs.graph.triples((None, MATH['greaterThan'], None)): |
249 g = Graph() | |
250 g.add(stmt) | |
251 yield Evaluation([stmt[0], stmt[2]], g, stmt) | |
252 | |
253 for stmt in lhs.graph.triples((None, ROOM['asFarenheit'], None)): | |
256 g = Graph() | 254 g = Graph() |
257 g.add(stmt) | 255 g.add(stmt) |
258 yield Evaluation([stmt[0]], g, stmt) | 256 yield Evaluation([stmt[0]], g, stmt) |
259 | 257 |
260 # internal, use findEvals | 258 # internal, use findEvals |
261 def __init__(self, operands: List[Node], operandsStmts: Graph, stmt: Triple) -> None: | 259 def __init__(self, operands: List[Node], operandsStmts: Graph, stmt: Triple) -> None: |
262 self.operands = operands | 260 self.operands = operands |
263 self.operandsStmts = operandsStmts | 261 self.operandsStmts = operandsStmts # may grow |
264 self.stmt = stmt | 262 self.stmt = stmt |
265 | 263 |
266 def resultBindings(self, inputBindings) -> Tuple[Dict[BindableTerm, Node], Graph]: | 264 def resultBindings(self, inputBindings) -> Tuple[Dict[BindableTerm, Node], Graph]: |
267 """under the bindings so far, what would this evaluation tell us, and which stmts would be consumed from doing so?""" | 265 """under the bindings so far, what would this evaluation tell us, and which stmts would be consumed from doing so?""" |
268 pred = self.stmt[1] | 266 pred = self.stmt[1] |
269 objVar = self.stmt[2] | 267 objVar: Node = self.stmt[2] |
270 boundOperands = [] | 268 boundOperands = [] |
271 for o in self.operands: | 269 for o in self.operands: |
272 if isinstance(o, Variable): | 270 if isinstance(o, Variable): |
273 try: | 271 try: |
274 o = inputBindings[o] | 272 o = inputBindings[o] |
275 except KeyError: | 273 except KeyError: |
276 return {}, self.operandsStmts | 274 return {}, self.operandsStmts |
277 | 275 |
278 boundOperands.append(o) | 276 boundOperands.append(o) |
279 | 277 |
280 if not isinstance(objVar, Variable): | |
281 raise TypeError(f'expected Variable, got {objVar!r}') | |
282 | |
283 if pred == MATH['sum']: | 278 if pred == MATH['sum']: |
284 log.debug(f'{INDENT*4} sum {list(map(self.numericNode, boundOperands))}') | 279 obj = Literal(sum(map(numericNode, boundOperands))) |
285 obj = cast(Literal, Literal(sum(map(self.numericNode, boundOperands)))) | |
286 self.operandsStmts.add(self.stmt) | 280 self.operandsStmts.add(self.stmt) |
281 if not isinstance(objVar, Variable): | |
282 raise TypeError(f'expected Variable, got {objVar!r}') | |
287 return {objVar: obj}, self.operandsStmts | 283 return {objVar: obj}, self.operandsStmts |
288 elif pred == ROOM['asFarenheit']: | 284 elif pred == ROOM['asFarenheit']: |
289 if len(boundOperands) != 1: | 285 if len(boundOperands) != 1: |
290 raise ValueError(":asFarenheit takes 1 subject operand") | 286 raise ValueError(":asFarenheit takes 1 subject operand") |
291 f = Literal(Decimal(self.numericNode(boundOperands[0])) * 9 / 5 + 32) | 287 f = Literal(Decimal(numericNode(boundOperands[0])) * 9 / 5 + 32) |
292 g = Graph() | 288 if not isinstance(objVar, Variable): |
293 g.add(self.stmt) | 289 raise TypeError(f'expected Variable, got {objVar!r}') |
294 | 290 return {objVar: f}, self.operandsStmts |
295 log.debug('made 1 st graph') | 291 elif pred == MATH['greaterThan']: |
296 return {objVar: f}, g | 292 if not (numericNode(boundOperands[0]) > numericNode(boundOperands[1])): |
293 raise EvaluationFailed() | |
294 return {}, self.operandsStmts | |
297 else: | 295 else: |
298 raise NotImplementedError() | 296 raise NotImplementedError(repr(pred)) |
299 | 297 |
300 def numericNode(self, n: Node): | 298 |
301 if not isinstance(n, Literal): | 299 def numericNode(n: Node): |
302 raise TypeError(f'expected Literal, got {n=}') | 300 if not isinstance(n, Literal): |
303 val = n.toPython() | 301 raise TypeError(f'expected Literal, got {n=}') |
304 if not isinstance(val, (int, float, Decimal)): | 302 val = n.toPython() |
305 raise TypeError(f'expected number, got {val=}') | 303 if not isinstance(val, (int, float, Decimal)): |
306 return val | 304 raise TypeError(f'expected number, got {val=}') |
307 | 305 return val |
308 | |
309 # merge into evaluation, raising a Invalid for impossible stmts | |
310 def mathTest(subj, pred, obj): | |
311 x = subj.toPython() | |
312 y = obj.toPython() | |
313 if pred == MATH['greaterThan']: | |
314 return x > y | |
315 else: | |
316 raise NotImplementedError(pred) | |
317 | 306 |
318 | 307 |
319 class Inference: | 308 class Inference: |
320 | 309 |
321 def __init__(self) -> None: | 310 def __init__(self) -> None: |
332 | 321 |
333 # everything that is true: the input graph, plus every rule conclusion we can make | 322 # everything that is true: the input graph, plus every rule conclusion we can make |
334 workingSet = Graph() | 323 workingSet = Graph() |
335 workingSet += graph | 324 workingSet += graph |
336 | 325 |
337 # just the statements that came from rule RHS's. | 326 # just the statements that came from RHS's of rules that fired. |
338 implied = ConjunctiveGraph() | 327 implied = ConjunctiveGraph() |
339 | 328 |
340 bailout_iterations = 100 | 329 bailout_iterations = 100 |
341 delta = 1 | 330 delta = 1 |
342 while delta > 0 and bailout_iterations > 0: | 331 while delta > 0 and bailout_iterations > 0: |
351 log.info(f'{INDENT*2} {st}') | 340 log.info(f'{INDENT*2} {st}') |
352 return implied | 341 return implied |
353 | 342 |
354 def _iterateAllRules(self, workingSet: Graph, implied: Graph): | 343 def _iterateAllRules(self, workingSet: Graph, implied: Graph): |
355 for i, r in enumerate(self.rules): | 344 for i, r in enumerate(self.rules): |
356 log.debug('') | 345 self.logRuleApplicationHeader(workingSet, i, r) |
357 log.debug(f'{INDENT*2} workingSet:') | |
358 for i, stmt in enumerate(sorted(workingSet)): | |
359 log.debug(f'{INDENT*3} ({i}) {stmt}') | |
360 | |
361 log.debug('') | |
362 log.debug(f'{INDENT*2}-applying rule {i}') | |
363 log.debug(f'{INDENT*3} rule def lhs: {graphDump(r[0])}') | |
364 log.debug(f'{INDENT*3} rule def rhs: {graphDump(r[2])}') | |
365 if r[1] == LOG['implies']: | 346 if r[1] == LOG['implies']: |
366 applyRule(Lhs(r[0]), r[2], workingSet, implied) | 347 applyRule(Lhs(r[0]), r[2], workingSet, implied) |
367 else: | 348 else: |
368 log.info(f'{INDENT*2} {r} not a rule?') | 349 log.info(f'{INDENT*2} {r} not a rule?') |
369 | 350 |
351 def logRuleApplicationHeader(self, workingSet, i, r): | |
352 if not log.isEnabledFor(logging.DEBUG): | |
353 return | |
354 | |
355 log.debug('') | |
356 log.debug(f'{INDENT*2} workingSet:') | |
357 for i, stmt in enumerate(sorted(workingSet)): | |
358 log.debug(f'{INDENT*3} ({i}) {stmt}') | |
359 | |
360 log.debug('') | |
361 log.debug(f'{INDENT*2}-applying rule {i}') | |
362 log.debug(f'{INDENT*3} rule def lhs: {graphDump(r[0])}') | |
363 log.debug(f'{INDENT*3} rule def rhs: {graphDump(r[2])}') | |
364 | |
370 | 365 |
371 def applyRule(lhs: Lhs, rhs: Graph, workingSet: Graph, implied: Graph): | 366 def applyRule(lhs: Lhs, rhs: Graph, workingSet: Graph, implied: Graph): |
372 for binding in lhs.findCandidateBindings(ReadOnlyGraphAggregate([workingSet])): | 367 for binding in lhs.findCandidateBindings(ReadOnlyGraphAggregate([workingSet])): |
373 # log.debug(f' rule gave {binding=}') | |
374 for lhsBoundStmt in binding.apply(lhs.graphWithoutEvals(binding)): | 368 for lhsBoundStmt in binding.apply(lhs.graphWithoutEvals(binding)): |
375 workingSet.add(lhsBoundStmt) | 369 workingSet.add(lhsBoundStmt) |
376 for newStmt in binding.apply(rhs): | 370 for newStmt in binding.apply(rhs): |
377 workingSet.add(newStmt) | 371 workingSet.add(newStmt) |
378 implied.add(newStmt) | 372 implied.add(newStmt) |
382 """"Do like Collection(g, subj) but also return all the | 376 """"Do like Collection(g, subj) but also return all the |
383 triples that are involved in the list""" | 377 triples that are involved in the list""" |
384 out = [] | 378 out = [] |
385 used = set() | 379 used = set() |
386 cur = subj | 380 cur = subj |
387 while True: | 381 while cur != RDF.nil: |
388 # bug: mishandles empty list | |
389 out.append(graph.value(cur, RDF.first)) | 382 out.append(graph.value(cur, RDF.first)) |
390 used.add((cur, RDF.first, out[-1])) | 383 used.add((cur, RDF.first, out[-1])) |
391 | 384 |
392 next = graph.value(cur, RDF.rest) | 385 next = graph.value(cur, RDF.rest) |
393 used.add((cur, RDF.rest, next)) | 386 used.add((cur, RDF.rest, next)) |
394 | 387 |
395 cur = next | 388 cur = next |
396 if cur == RDF.nil: | |
397 break | |
398 return out, used | 389 return out, used |
399 | 390 |
400 | 391 |
401 def graphDump(g: Union[Graph, List[Triple]]): | 392 def graphDump(g: Union[Graph, List[Triple]]): |
402 if not isinstance(g, Graph): | 393 if not isinstance(g, Graph): |
403 g2 = Graph() | 394 g2 = Graph() |
404 for stmt in g: | 395 g2 += g |
405 g2.add(stmt) | |
406 g = g2 | 396 g = g2 |
407 g.bind('', ROOM) | 397 g.bind('', ROOM) |
408 g.bind('ex', Namespace('http://example.com/')) | 398 g.bind('ex', Namespace('http://example.com/')) |
409 lines = cast(bytes, g.serialize(format='n3')).decode('utf8').splitlines() | 399 lines = cast(bytes, g.serialize(format='n3')).decode('utf8').splitlines() |
410 lines = [line for line in lines if not line.startswith('@prefix')] | 400 lines = [line for line in lines if not line.startswith('@prefix')] |