comparison service/mqtt_to_rdf/inference.py @ 1694:73abfd4cf5d0

new html log and other refactoring as i work on the advanceTheStack problems https://bigasterisk.com/post/inference/2021-09-27_11-11.png
author drewp@bigasterisk.com
date Mon, 27 Sep 2021 11:22:09 -0700
parents 0455a1e18e4f
children 5c2565e63297
comparison
equal deleted inserted replaced
1693:0455a1e18e4f 1694:73abfd4cf5d0
6 import logging 6 import logging
7 import time 7 import time
8 from collections import defaultdict 8 from collections import defaultdict
9 from dataclasses import dataclass 9 from dataclasses import dataclass
10 from typing import Dict, Iterator, List, Optional, Tuple, Union, cast 10 from typing import Dict, Iterator, List, Optional, Tuple, Union, cast
11 from pathlib import Path
11 12
12 from prometheus_client import Histogram, Summary 13 from prometheus_client import Histogram, Summary
13 from rdflib import Graph, Namespace 14 from rdflib import Graph, Namespace
14 from rdflib.graph import ConjunctiveGraph 15 from rdflib.graph import ConjunctiveGraph
15 from rdflib.term import Node, URIRef, Variable 16 from rdflib.term import Node, URIRef, Variable
16 17
17 from candidate_binding import BindingConflict, CandidateBinding 18 from candidate_binding import CandidateBinding
18 from inference_types import (BindingUnknown, Inconsistent, RhsBnode, RuleOutBnode, RuleUnboundBnode, Triple, WorkingSetBnode) 19 from inference_types import (BindingUnknown, Inconsistent, RhsBnode, RuleUnboundBnode, Triple, WorkingSetBnode)
19 from lhs_evaluation import functionsFor 20 from lhs_evaluation import functionsFor
20 from rdf_debug import graphDump 21 from rdf_debug import graphDump
21 from stmt_chunk import AlignedRuleChunk, Chunk, ChunkedGraph, applyChunky 22 from stmt_chunk import AlignedRuleChunk, Chunk, ChunkedGraph, applyChunky
23 from structured_log import StructuredLog
22 24
23 log = logging.getLogger('infer') 25 log = logging.getLogger('infer')
24 odolog = logging.getLogger('infer.odo') # the "odometer" logic 26 odolog = logging.getLogger('infer.odo') # the "odometer" logic
25 ringlog = logging.getLogger('infer.ring') # for ChunkLooper 27 ringlog = logging.getLogger('infer.ring') # for ChunkLooper
26 28
34 MATH = Namespace('http://www.w3.org/2000/10/swap/math#') 36 MATH = Namespace('http://www.w3.org/2000/10/swap/math#')
35 37
36 38
37 class NoOptions(ValueError): 39 class NoOptions(ValueError):
38 """ChunkLooper has no possibilites to add to the binding; the whole rule must therefore not apply""" 40 """ChunkLooper has no possibilites to add to the binding; the whole rule must therefore not apply"""
41
42
43 def debug(logger, slog: Optional[StructuredLog], msg):
44 logger.debug(msg)
45 if slog:
46 slog.say(msg)
39 47
40 48
41 _chunkLooperShortId = itertools.count() 49 _chunkLooperShortId = itertools.count()
42 50
43 51
51 59
52 This iterator is restartable.""" 60 This iterator is restartable."""
53 lhsChunk: Chunk 61 lhsChunk: Chunk
54 prev: Optional['ChunkLooper'] 62 prev: Optional['ChunkLooper']
55 workingSet: 'ChunkedGraph' 63 workingSet: 'ChunkedGraph'
64 slog: Optional[StructuredLog]
56 65
57 def __repr__(self): 66 def __repr__(self):
58 return f'{self.__class__.__name__}{self._shortId}{"<pastEnd>" if self.pastEnd() else ""}' 67 return f'{self.__class__.__name__}{self._shortId}{"<pastEnd>" if self.pastEnd() else ""}'
59 68
60 def __post_init__(self): 69 def __post_init__(self):
61 self._shortId = next(_chunkLooperShortId) 70 self._shortId = next(_chunkLooperShortId)
62 self._alignedMatches = list(self.lhsChunk.ruleMatchesFrom(self.workingSet)) 71 self._alignedMatches = list(self.lhsChunk.ruleMatchesFrom(self.workingSet))
72 del self.workingSet
63 73
64 # only ours- do not store prev, since it could change without us 74 # only ours- do not store prev, since it could change without us
65 self._current = CandidateBinding({}) 75 self._current = CandidateBinding({})
76 self.currentSourceChunk: Optional[Chunk] = None # for debugging only
66 self._pastEnd = False 77 self._pastEnd = False
67 self._seenBindings: List[CandidateBinding] = [] # combined bindings (up to our ring) that we've returned 78 self._seenBindings: List[CandidateBinding] = [] # combined bindings (up to our ring) that we've returned
68 79
69 if ringlog.isEnabledFor(logging.DEBUG): 80 if ringlog.isEnabledFor(logging.DEBUG):
70 ringlog.debug('') 81 ringlog.debug('')
71 ringlog.debug(f'{INDENT*6} introducing {self!r}({self.lhsChunk}, {self._alignedMatches=})') 82 msg = f'{INDENT*6} introducing {self!r}({self.lhsChunk}, {self._alignedMatches=})'
83 msg = msg.replace('AlignedRuleChunk', f'\n{INDENT*12}AlignedRuleChunk')
84 ringlog.debug(msg)
72 85
73 self.restart() 86 self.restart()
74 87
75 def _prevBindings(self) -> CandidateBinding: 88 def _prevBindings(self) -> CandidateBinding:
76 if not self.prev or self.prev.pastEnd(): 89 if not self.prev or self.prev.pastEnd():
83 last restart), or go into pastEnd mode. Note that _current is just our 96 last restart), or go into pastEnd mode. Note that _current is just our
84 contribution, but returned valid bindings include all prev rings.""" 97 contribution, but returned valid bindings include all prev rings."""
85 if self._pastEnd: 98 if self._pastEnd:
86 raise NotImplementedError('need restart') 99 raise NotImplementedError('need restart')
87 ringlog.debug('') 100 ringlog.debug('')
101 debug(ringlog, self.slog, f'{INDENT*6} --> {self}.advance start:')
102
88 self._currentIsFromFunc = None 103 self._currentIsFromFunc = None
89 augmentedWorkingSet: List[AlignedRuleChunk] = [] 104 augmentedWorkingSet: List[AlignedRuleChunk] = []
90 if self.prev is None: 105 if self.prev is None:
91 augmentedWorkingSet = self._alignedMatches 106 augmentedWorkingSet = self._alignedMatches
92 else: 107 else:
93 augmentedWorkingSet = list(applyChunky(self.prev.currentBinding(), self._alignedMatches)) 108 augmentedWorkingSet = list(applyChunky(self.prev.currentBinding(), self._alignedMatches))
94 109
95 if self._advanceWithPlainMatches(augmentedWorkingSet): 110 if self._advanceWithPlainMatches(augmentedWorkingSet):
96 ringlog.debug(f'{INDENT*6} <-- {self}.advance finished with plain matches') 111 debug(ringlog, self.slog, f'{INDENT*6} <-- {self}.advance finished with plain matches')
97 return 112 return
98 113
99 if self._advanceWithFunctions(): 114 if self._advanceWithFunctions():
100 ringlog.debug(f'{INDENT*6} <-- {self}.advance finished with function matches') 115 debug(ringlog, self.slog, f'{INDENT*6} <-- {self}.advance finished with function matches')
101 return 116 return
102 117
103 ringlog.debug(f'{INDENT*6} <-- {self}.advance had nothing and is now past end') 118 debug(ringlog, self.slog, f'{INDENT*6} <-- {self}.advance had nothing and is now past end')
104 self._pastEnd = True 119 self._pastEnd = True
105 120
106 def _advanceWithPlainMatches(self, augmentedWorkingSet: List[AlignedRuleChunk]) -> bool: 121 def _advanceWithPlainMatches(self, augmentedWorkingSet: List[AlignedRuleChunk]) -> bool:
107 ringlog.debug(f'{INDENT*7} {self} mines {len(augmentedWorkingSet)} matching augmented statements') 122 # if augmentedWorkingSet:
108 for s in augmentedWorkingSet: 123 # debug(ringlog, self.slog, f'{INDENT*7} {self} mines {len(augmentedWorkingSet)} matching augmented statements')
109 ringlog.debug(f'{INDENT*8} {s}') 124 # for s in augmentedWorkingSet:
125 # debug(ringlog, self.slog, f'{INDENT*8} {s}')
110 126
111 for aligned in augmentedWorkingSet: 127 for aligned in augmentedWorkingSet:
112 try: 128 try:
113 newBinding = aligned.newBindingIfMatched(self._prevBindings()) 129 newBinding = aligned.newBindingIfMatched(self._prevBindings())
114 except Inconsistent as exc: 130 except Inconsistent as exc:
115 ringlog.debug( 131 debug(ringlog, self.slog,
116 f'{INDENT*7} ChunkLooper{self._shortId} - {aligned} would be inconsistent with prev bindings ({exc})') 132 f'{INDENT*7} ChunkLooper{self._shortId} - {aligned} would be inconsistent with prev bindings ({exc})')
117 continue 133 continue
118 134
119 if self._testAndKeepNewBinding(newBinding): 135 if self._testAndKeepNewBinding(newBinding, aligned.workingSetChunk):
120 return True 136 return True
121 return False 137 return False
122 138
123 def _advanceWithFunctions(self) -> bool: 139 def _advanceWithFunctions(self) -> bool:
124 pred: Node = self.lhsChunk.predicate 140 pred: Node = self.lhsChunk.predicate
125 if not isinstance(pred, URIRef): 141 if not isinstance(pred, URIRef):
126 raise NotImplementedError 142 raise NotImplementedError
127 143
128 ringlog.debug(f'{INDENT*6} advanceWithFunctions {pred!r}')
129
130 for functionType in functionsFor(pred): 144 for functionType in functionsFor(pred):
131 fn = functionType(self.lhsChunk) 145 fn = functionType(self.lhsChunk)
132 ringlog.debug(f'{INDENT*7} ChunkLooper{self._shortId} advanceWithFunctions, {functionType=}') 146 # debug(ringlog, self.slog, f'{INDENT*7} ChunkLooper{self._shortId} advanceWithFunctions, {functionType=}')
133 147
134 try: 148 try:
135 log.debug(f'fn.bind {self._prevBindings()} ...') 149 log.debug(f'fn.bind {self._prevBindings()} ...')
136 #fullBinding = self._prevBindings().copy() 150 #fullBinding = self._prevBindings().copy()
137 newBinding = fn.bind(self._prevBindings()) 151 newBinding = fn.bind(self._prevBindings())
139 except BindingUnknown: 153 except BindingUnknown:
140 pass 154 pass
141 else: 155 else:
142 if newBinding is not None: 156 if newBinding is not None:
143 self._currentIsFromFunc = fn 157 self._currentIsFromFunc = fn
144 if self._testAndKeepNewBinding(newBinding): 158 if self._testAndKeepNewBinding(newBinding, self.lhsChunk):
145 return True 159 return True
146 160
147 return False 161 return False
148 162
149 def _testAndKeepNewBinding(self, newBinding: CandidateBinding): 163 def _testAndKeepNewBinding(self, newBinding: CandidateBinding, sourceChunk: Chunk):
150 fullBinding: CandidateBinding = self._prevBindings().copy() 164 fullBinding: CandidateBinding = self._prevBindings().copy()
151 fullBinding.addNewBindings(newBinding) 165 fullBinding.addNewBindings(newBinding)
152 isNew = fullBinding not in self._seenBindings 166 isNew = fullBinding not in self._seenBindings
153 ringlog.debug(f'{INDENT*7} {self} considering {newBinding=} to make {fullBinding}. {isNew=}') 167 ringlog.debug(f'{INDENT*7} {self} considering {newBinding=} to make {fullBinding}. {isNew=}')
168 # if self.slog:
169 # self.slog.looperConsider(self, newBinding, fullBinding, isNew)
170
154 if isNew: 171 if isNew:
155 self._seenBindings.append(fullBinding.copy()) 172 self._seenBindings.append(fullBinding.copy())
156 self._current = newBinding 173 self._current = newBinding
174 self.currentSourceChunk = sourceChunk
157 return True 175 return True
158 return False 176 return False
159 177
160 def _boundOperands(self, operands) -> List[Node]: 178 def localBinding(self) -> CandidateBinding:
161 pb: CandidateBinding = self._prevBindings() 179 if self.pastEnd():
162 180 raise NotImplementedError()
163 boundOperands: List[Node] = [] 181 return self._current
164 for op in operands:
165 if isinstance(op, (Variable, RuleUnboundBnode)):
166 boundOperands.append(pb.applyTerm(op))
167 else:
168 boundOperands.append(op)
169 return boundOperands
170 182
171 def currentBinding(self) -> CandidateBinding: 183 def currentBinding(self) -> CandidateBinding:
172 if self.pastEnd(): 184 if self.pastEnd():
173 raise NotImplementedError() 185 raise NotImplementedError()
174 together = self._prevBindings().copy() 186 together = self._prevBindings().copy()
177 189
178 def pastEnd(self) -> bool: 190 def pastEnd(self) -> bool:
179 return self._pastEnd 191 return self._pastEnd
180 192
181 def restart(self): 193 def restart(self):
182 self._pastEnd = False 194 try:
183 self._seenBindings = [] 195 self._pastEnd = False
184 self.advance() 196 self._seenBindings = []
185 if self.pastEnd(): 197 self.advance()
186 raise NoOptions() 198 if self.pastEnd():
199 raise NoOptions()
200 finally:
201 debug(ringlog, self.slog, f'{INDENT*7} ChunkLooper{self._shortId} restarts: pastEnd={self.pastEnd()}')
187 202
188 def prevMayHaveChanged(self): 203 def prevMayHaveChanged(self):
204 pass
205 # self._advanceWithFunctions()
189 # This is a total patch for a test failure. This should be generalized 206 # This is a total patch for a test failure. This should be generalized
190 # to a Looper that can keep itself correct when prev changes. 207 # to a Looper that can keep itself correct when prev changes.
191 if self._currentIsFromFunc: 208 # if self.pastEnd():
192 self._advanceWithFunctions() 209 # self.restart()
193 if self.pastEnd(): 210 # else:
194 self.restart() 211 # self.advance()
212 # if self._currentIsFromFunc:
213 # self._advanceWithFunctions()
214 # if self.pastEnd():
215 # self.restart()
195 216
196 217
197 @dataclass 218 @dataclass
198 class Lhs: 219 class Lhs:
199 graph: ChunkedGraph # our full LHS graph, as input. See below for the statements partitioned into groups. 220 graph: ChunkedGraph # our full LHS graph, as input. See below for the statements partitioned into groups.
202 223
203 self.myPreds = self.graph.allPredicatesExceptFunctions() 224 self.myPreds = self.graph.allPredicatesExceptFunctions()
204 225
205 def __repr__(self): 226 def __repr__(self):
206 return f"Lhs({self.graph!r})" 227 return f"Lhs({self.graph!r})"
207 228 def findCandidateBindings(self, knownTrue: ChunkedGraph, stats, slog: Optional[StructuredLog],
208 def findCandidateBindings(self, knownTrue: ChunkedGraph, stats, ruleStatementsIterationLimit) -> Iterator['BoundLhs']: 229 ruleStatementsIterationLimit) -> Iterator['BoundLhs']:
209 """distinct bindings that fit the LHS of a rule, using statements from 230 """distinct bindings that fit the LHS of a rule, using statements from
210 workingSet and functions from LHS""" 231 workingSet and functions from LHS"""
211 if not self.graph: 232 if not self.graph:
212 # special case- no LHS! 233 # special case- no LHS!
213 yield BoundLhs(self, CandidateBinding({})) 234 yield BoundLhs(self, CandidateBinding({}))
227 yield BoundLhs(self, CandidateBinding({})) 248 yield BoundLhs(self, CandidateBinding({}))
228 return 249 return
229 250
230 log.debug('') 251 log.debug('')
231 try: 252 try:
232 chunkStack = self._assembleRings(knownTrue, stats) 253 chunkStack = self._assembleRings(knownTrue, stats, slog)
233 except NoOptions: 254 except NoOptions:
234 ringlog.debug(f'{INDENT*5} start up with no options; 0 bindings') 255 ringlog.debug(f'{INDENT*5} start up with no options; 0 bindings')
235 return 256 return
236 log.debug('') 257 log.debug('')
237 log.debug('') 258 log.debug('')
238 self._debugChunkStack('time to spin: initial odometer is', chunkStack) 259 self._debugChunkStack('time to spin: initial odometer is', chunkStack)
260
261 if slog:
262 slog.say('time to spin')
263 slog.odometer(chunkStack)
239 self._assertAllRingsAreValid(chunkStack) 264 self._assertAllRingsAreValid(chunkStack)
240 265
241 lastRing = chunkStack[-1] 266 lastRing = chunkStack[-1]
242 iterCount = 0 267 iterCount = 0
243 while True: 268 while True:
252 # self._debugChunkStack('odometer', chunkStack) 277 # self._debugChunkStack('odometer', chunkStack)
253 278
254 done = self._advanceTheStack(chunkStack) 279 done = self._advanceTheStack(chunkStack)
255 280
256 self._debugChunkStack(f'odometer after ({done=})', chunkStack) 281 self._debugChunkStack(f'odometer after ({done=})', chunkStack)
282 if slog:
283 slog.odometer(chunkStack)
257 284
258 log.debug(f'{INDENT*4} ^^ findCandBindings iteration done') 285 log.debug(f'{INDENT*4} ^^ findCandBindings iteration done')
259 if done: 286 if done:
260 break 287 break
261 288
262 def _debugChunkStack(self, label: str, chunkStack: List[ChunkLooper]): 289 def _debugChunkStack(self, label: str, chunkStack: List[ChunkLooper]):
263 odolog.debug(f'{INDENT*4} {label}:') 290 odolog.debug(f'{INDENT*4} {label}:')
264 for i, l in enumerate(chunkStack): 291 for i, l in enumerate(chunkStack):
265 odolog.debug(f'{INDENT*5} [{i}] {l} curbind={l.currentBinding() if not l.pastEnd() else "<end>"}') 292 odolog.debug(f'{INDENT*5} [{i}] {l} curbind={l.localBinding() if not l.pastEnd() else "<end>"}')
266 293
267 def _checkPredicateCounts(self, knownTrue): 294 def _checkPredicateCounts(self, knownTrue):
268 """raise NoOptions quickly in some cases""" 295 """raise NoOptions quickly in some cases"""
269 296
270 if self.graph.noPredicatesAppear(self.myPreds): 297 if self.graph.noPredicatesAppear(self.myPreds):
271 log.debug(f'{INDENT*3} checkPredicateCounts does cull because not all {self.myPreds=} are in knownTrue') 298 log.debug(f'{INDENT*3} checkPredicateCounts does cull because not all {self.myPreds=} are in knownTrue')
272 return True 299 return True
273 log.debug(f'{INDENT*3} checkPredicateCounts does not cull because all {self.myPreds=} are in knownTrue') 300 log.debug(f'{INDENT*3} checkPredicateCounts does not cull because all {self.myPreds=} are in knownTrue')
274 return False 301 return False
275 302
276 def _assembleRings(self, knownTrue: ChunkedGraph, stats) -> List[ChunkLooper]: 303 def _assembleRings(self, knownTrue: ChunkedGraph, stats, slog) -> List[ChunkLooper]:
277 """make ChunkLooper for each stmt in our LHS graph, but do it in a way that they all 304 """make ChunkLooper for each stmt in our LHS graph, but do it in a way that they all
278 start out valid (or else raise NoOptions). static chunks have already been confirmed.""" 305 start out valid (or else raise NoOptions). static chunks have already been confirmed."""
279 306
280 log.debug(f'{INDENT*4} stats={dict(stats)}') 307 log.debug(f'{INDENT*4} stats={dict(stats)}')
281 odolog.debug(f'{INDENT*3} build new ChunkLooper stack') 308 odolog.debug(f'{INDENT*3} build new ChunkLooper stack')
295 for ruleChunk in perm: 322 for ruleChunk in perm:
296 try: 323 try:
297 # These are getting rebuilt a lot which takes time. It would 324 # These are getting rebuilt a lot which takes time. It would
298 # be nice if they could accept a changing `prev` order 325 # be nice if they could accept a changing `prev` order
299 # (which might already be ok). 326 # (which might already be ok).
300 looper = ChunkLooper(ruleChunk, prev, knownTrue) 327 looper = ChunkLooper(ruleChunk, prev, knownTrue, slog)
301 except NoOptions: 328 except NoOptions:
302 odolog.debug(f'{INDENT*5} permutation didnt work, try another') 329 odolog.debug(f'{INDENT*5} permutation didnt work, try another')
303 break 330 break
304 looperRings.append(looper) 331 looperRings.append(looper)
305 prev = looperRings[-1] 332 prev = looperRings[-1]
389 p = RhsBnode(p) 416 p = RhsBnode(p)
390 if isinstance(o, BNode): 417 if isinstance(o, BNode):
391 o = RhsBnode(o) 418 o = RhsBnode(o)
392 self.rhsGraphConvert.append((s, p, o)) 419 self.rhsGraphConvert.append((s, p, o))
393 420
394 def applyRule(self, workingSet: Graph, implied: Graph, stats: Dict, ruleStatementsIterationLimit): 421 def applyRule(self, workingSet: Graph, implied: Graph, stats: Dict, slog: Optional[StructuredLog],
422 ruleStatementsIterationLimit):
395 # this does not change for the current applyRule call. The rule will be 423 # this does not change for the current applyRule call. The rule will be
396 # tried again in an outer loop, in case it can produce more. 424 # tried again in an outer loop, in case it can produce more.
397 workingSetChunked = ChunkedGraph(workingSet, WorkingSetBnode, functionsFor) 425 workingSetChunked = ChunkedGraph(workingSet, WorkingSetBnode, functionsFor)
398 426
399 for bound in self.lhs.findCandidateBindings(workingSetChunked, stats, ruleStatementsIterationLimit): 427 for bound in self.lhs.findCandidateBindings(workingSetChunked, stats, slog, ruleStatementsIterationLimit):
428 if slog:
429 slog.foundBinding(bound)
400 log.debug(f'{INDENT*5} +rule has a working binding: {bound}') 430 log.debug(f'{INDENT*5} +rule has a working binding: {bound}')
401 431
402 newStmts = self.generateImpliedFromRhs(bound.binding) 432 newStmts = self.generateImpliedFromRhs(bound.binding)
403 433
404 for newStmt in newStmts: 434 for newStmt in newStmts:
457 487
458 def nonRuleStatements(self) -> List[Triple]: 488 def nonRuleStatements(self) -> List[Triple]:
459 return self._nonRuleStmts 489 return self._nonRuleStmts
460 490
461 @INFER_CALLS.time() 491 @INFER_CALLS.time()
462 def infer(self, graph: Graph): 492 def infer(self, graph: Graph, htmlLog: Optional[Path] = None):
463 """ 493 """
464 returns new graph of inferred statements. 494 returns new graph of inferred statements.
465 """ 495 """
466 n = graph.__len__() 496 n = graph.__len__()
467 INFER_GRAPH_SIZE.observe(n) 497 INFER_GRAPH_SIZE.observe(n)
475 workingSet += graph 505 workingSet += graph
476 506
477 # just the statements that came from RHS's of rules that fired. 507 # just the statements that came from RHS's of rules that fired.
478 implied = ConjunctiveGraph() 508 implied = ConjunctiveGraph()
479 509
510 slog = StructuredLog(htmlLog) if htmlLog else None
511
480 rulesIterations = 0 512 rulesIterations = 0
481 delta = 1 513 delta = 1
482 stats['initWorkingSet'] = cast(int, workingSet.__len__()) 514 stats['initWorkingSet'] = cast(int, workingSet.__len__())
515 if slog:
516 slog.workingSet = workingSet
517
483 while delta > 0: 518 while delta > 0:
484 log.debug('') 519 log.debug('')
485 log.info(f'{INDENT*1}*iteration {rulesIterations}') 520 log.info(f'{INDENT*1}*iteration {rulesIterations}')
521 if slog:
522 slog.startIteration(rulesIterations)
486 523
487 delta = -len(implied) 524 delta = -len(implied)
488 self._iterateAllRules(workingSet, implied, stats) 525 self._iterateAllRules(workingSet, implied, stats, slog)
489 delta += len(implied) 526 delta += len(implied)
490 rulesIterations += 1 527 rulesIterations += 1
491 log.info(f'{INDENT*2} this inference iteration added {delta} more implied stmts') 528 log.info(f'{INDENT*2} this inference iteration added {delta} more implied stmts')
492 if rulesIterations >= self.rulesIterationLimit: 529 if rulesIterations >= self.rulesIterationLimit:
493 raise ValueError(f"rule too complex after {rulesIterations=}") 530 raise ValueError(f"rule too complex after {rulesIterations=}")
495 stats['timeSpent'] = round(time.time() - startTime, 3) 532 stats['timeSpent'] = round(time.time() - startTime, 3)
496 stats['impliedStmts'] = len(implied) 533 stats['impliedStmts'] = len(implied)
497 log.info(f'{INDENT*0} Inference done {dict(stats)}.') 534 log.info(f'{INDENT*0} Inference done {dict(stats)}.')
498 log.debug('Implied:') 535 log.debug('Implied:')
499 log.debug(graphDump(implied)) 536 log.debug(graphDump(implied))
537
538 if slog:
539 slog.render()
540 log.info(f'wrote {htmlLog}')
541
500 return implied 542 return implied
501 543
502 def _iterateAllRules(self, workingSet: Graph, implied: Graph, stats): 544 def _iterateAllRules(self, workingSet: Graph, implied: Graph, stats, slog: Optional[StructuredLog]):
503 for i, rule in enumerate(self.rules): 545 for i, rule in enumerate(self.rules):
504 self._logRuleApplicationHeader(workingSet, i, rule) 546 self._logRuleApplicationHeader(workingSet, i, rule)
505 rule.applyRule(workingSet, implied, stats, self.ruleStatementsIterationLimit) 547 if slog:
548 slog.rule(workingSet, i, rule)
549 rule.applyRule(workingSet, implied, stats, slog, self.ruleStatementsIterationLimit)
506 550
507 def _logRuleApplicationHeader(self, workingSet, i, r: Rule): 551 def _logRuleApplicationHeader(self, workingSet, i, r: Rule):
508 if not log.isEnabledFor(logging.DEBUG): 552 if not log.isEnabledFor(logging.DEBUG):
509 return 553 return
510 554