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')]