changeset 1668:89e53cb8a01c

fix some harder tests. Mostly, _advanceTheStack needed to spin the odometer rings starting from the other side, to get all the right combos
author drewp@bigasterisk.com
date Tue, 21 Sep 2021 22:29:19 -0700
parents a2347393b43e
children 9d00adef0b22
files service/mqtt_to_rdf/inference.py service/mqtt_to_rdf/stmt_chunk.py
diffstat 2 files changed, 58 insertions(+), 42 deletions(-) [+]
line wrap: on
line diff
--- a/service/mqtt_to_rdf/inference.py	Tue Sep 21 22:19:11 2021 -0700
+++ b/service/mqtt_to_rdf/inference.py	Tue Sep 21 22:29:19 2021 -0700
@@ -61,7 +61,8 @@
         self._shortId = next(_chunkLooperShortId)
         self._alignedMatches = list(self.lhsChunk.ruleMatchesFrom(self.workingSet))
 
-        self._current = CandidateBinding({}) # only ours- do not store prev, since it could change without us
+        # only ours- do not store prev, since it could change without us
+        self._current = CandidateBinding({})
         self._pastEnd = False
         self._seenBindings: List[CandidateBinding] = []  # combined bindings (up to our ring) that we've returned
 
@@ -78,7 +79,9 @@
         return self.prev.currentBinding()
 
     def advance(self):
-        """update to a new set of bindings we haven't seen (since last restart), or go into pastEnd mode"""
+        """update _current to a new set of valid bindings we haven't seen (since
+        last restart), or go into pastEnd mode. Note that _current is just our
+        contribution, but returned valid bindings include all prev rings."""
         if self._pastEnd:
             raise NotImplementedError('need restart')
         ringlog.debug('')
@@ -89,7 +92,6 @@
             augmentedWorkingSet = list(
                 applyChunky(self.prev.currentBinding(), self._alignedMatches, returnBoundStatementsOnly=False))
 
-
         if self._advanceWithPlainMatches(augmentedWorkingSet):
             ringlog.debug(f'{INDENT*6} <-- {self}.advance finished with plain matches')
             return
@@ -108,19 +110,13 @@
 
         for aligned in augmentedWorkingSet:
             try:
-                fullBinding = aligned.totalBindingIfThisStmtWereTrue(self._prevBindings())
-            except Inconsistent:
-                ringlog.debug(f'{INDENT*7} ChunkLooper{self._shortId} - {aligned} would be inconsistent with prev bindings')
+                newBinding = aligned.newBindingIfMatched(self._prevBindings())
+            except Inconsistent as exc:
+                ringlog.debug(
+                    f'{INDENT*7} ChunkLooper{self._shortId} - {aligned} would be inconsistent with prev bindings ({exc})')
                 continue
 
-            newBinding = fullBinding.copy()
-            newBinding.subtract(self._prevBindings())
-
-            ringlog.debug(f'{INDENT*7} {newBinding=} {self._seenBindings=}')
-            if fullBinding not in self._seenBindings:
-                self._seenBindings.append(fullBinding.copy())
-                self._current = newBinding
-                ringlog.debug(f'{INDENT*7} new binding from {self} -> {fullBinding}')
+            if self._testAndKeepNewBinding(newBinding):
                 return True
         return False
 
@@ -142,16 +138,22 @@
                 pass
             else:
                 if newBinding is not None:
-                    fullBinding: CandidateBinding = self._prevBindings().copy()
-                    fullBinding.addNewBindings(newBinding)
-                    if fullBinding not in self._seenBindings:
-                        self._seenBindings.append(fullBinding)
-                        self._current = newBinding
-                        ringlog.debug(f'{INDENT*7} new binding from {self} -> {fullBinding}')
+                    if self._testAndKeepNewBinding(newBinding):
                         return True
 
         return False
 
+    def _testAndKeepNewBinding(self, newBinding):
+        fullBinding: CandidateBinding = self._prevBindings().copy()
+        fullBinding.addNewBindings(newBinding)
+        isNew = fullBinding not in self._seenBindings
+        ringlog.debug(f'{INDENT*7} {self} considering {newBinding=} to make {fullBinding}. {isNew=}')
+        if isNew:
+            self._seenBindings.append(fullBinding.copy())
+            self._current = newBinding
+            return True
+        return False
+
     def _boundOperands(self, operands) -> List[Node]:
         pb: CandidateBinding = self._prevBindings()
 
@@ -296,15 +298,15 @@
         raise NoOptions()
 
     def _advanceTheStack(self, looperRings: List[ChunkLooper]) -> bool:
-        carry = True  # 1st elem always must advance
-        for i, ring in enumerate(looperRings):
+        carry = True  # last elem always must advance
+        for i, ring in reversed(list(enumerate(looperRings))):
             # unlike normal odometer, advancing any earlier ring could invalidate later ones
             if carry:
                 odolog.debug(f'{INDENT*4} advanceAll [{i}] {ring} carry/advance')
                 ring.advance()
                 carry = False
             if ring.pastEnd():
-                if ring is looperRings[-1]:
+                if ring is looperRings[0]:
                     allRingsDone = [r.pastEnd() for r in looperRings]
                     odolog.debug(f'{INDENT*4} advanceAll [{i}] {ring} says we done   {allRingsDone=}')
                     return True
--- a/service/mqtt_to_rdf/stmt_chunk.py	Tue Sep 21 22:19:11 2021 -0700
+++ b/service/mqtt_to_rdf/stmt_chunk.py	Tue Sep 21 22:29:19 2021 -0700
@@ -19,23 +19,39 @@
 
 @dataclass
 class AlignedRuleChunk:
-    """a possible association between a rule chunk and a workingSet chunk. Use
-    matches() to see if the rule actually fits (and then we might cache some of
-    that work when computing the new bindings"""
+    """a possible association between a rule chunk and a workingSet chunk. You can test
+    whether the association would still be possible under various additional bindings."""
     ruleChunk: 'Chunk'
     workingSetChunk: 'Chunk'
 
-    def totalBindingIfThisStmtWereTrue(self, prevBindings: CandidateBinding) -> CandidateBinding:
-        outBinding = prevBindings.copy()
+    def __post_init__(self):
+        if not self.matches():
+            raise Inconsistent()
+
+    def newBindingIfMatched(self, prevBindings: CandidateBinding) -> CandidateBinding:
+        """supposing this rule did match the statement, what new bindings would
+        that produce?
+
+        raises Inconsistent if the existing bindings mean that our aligned
+        chunks can no longer match.
+        """
+        outBinding = CandidateBinding({})
         for rt, ct in zip(self.ruleChunk._allTerms(), self.workingSetChunk._allTerms()):
             if isinstance(rt, (Variable, BNode)):
+                if prevBindings.contains(rt) and prevBindings.applyTerm(rt) != ct:
+                    msg = f'{rt=} {ct=} {prevBindings=}' if log.isEnabledFor(logging.DEBUG) else ''
+                    raise Inconsistent(msg)
                 if outBinding.contains(rt) and outBinding.applyTerm(rt) != ct:
-                    msg = f'{rt=} {ct=} {outBinding=}' if log.isEnabledFor(logging.DEBUG) else ''
-                    raise Inconsistent(msg)
+                    # maybe this can happen, for stmts like ?x :a ?x .
+                    raise Inconsistent("outBinding inconsistent with itself")
                 outBinding.addNewBindings(CandidateBinding({rt: ct}))
+            else:
+                if rt != ct:
+                    # getting here means prevBindings was set to something our
+                    # rule statement disagrees with.
+                    raise Inconsistent(f'{rt=} != {ct=}')
         return outBinding
 
-    # could combine this and totalBindingIf into a single ChunkMatch object
     def matches(self) -> bool:
         """could this rule, with its BindableTerm wildcards, match workingSetChunk?"""
         for selfTerm, otherTerm in zip(self.ruleChunk._allTerms(), self.workingSetChunk._allTerms()):
@@ -95,9 +111,11 @@
         if "stable failures please":
             allChunksIter = sorted(allChunksIter)
         for chunk in allChunksIter:
-            aligned = AlignedRuleChunk(self, chunk)
-            if aligned.matches():
-                yield aligned
+            try:
+                aligned = AlignedRuleChunk(self, chunk)
+            except Inconsistent:
+                continue
+            yield aligned
 
     def __repr__(self):
         pre = ('+'.join('%s' % elem for elem in self.subjList) + '+' if self.subjList else '')
@@ -132,15 +150,11 @@
                 g: Iterable[AlignedRuleChunk],
                 returnBoundStatementsOnly=True) -> Iterator[AlignedRuleChunk]:
     for aligned in g:
+        bound = aligned.ruleChunk.apply(cb, returnBoundStatementsOnly=returnBoundStatementsOnly)
         try:
-            bound = aligned.ruleChunk.apply(cb, returnBoundStatementsOnly=returnBoundStatementsOnly)
-        except BindingUnknown:
-            log.debug(f'{INDENT*7} CB.apply cant bind {aligned} using {cb.binding}')
-
-            continue
-        log.debug(f'{INDENT*7} CB.apply took {aligned} to {bound}')
-
-        yield AlignedRuleChunk(bound, aligned.workingSetChunk)
+            yield AlignedRuleChunk(bound, aligned.workingSetChunk)
+        except Inconsistent:
+            pass
 
 
 class ChunkedGraph: