Extend annotation function capabilites#149
Merged
Merged
Conversation
… into update-annotation-bounds
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… into update-annotation-bounds
jaikrishnap98
approved these changes
May 13, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Extends annotation functions so they can recover the per-grounding join structure of a rule body, not just a flat per-clause list of bounds. Annotation functions now optionally receive the qualified nodes/edges, per-clause predicate label, and per-clause variable names, so user code can identify clauses by predicate name + variable role instead of body position (which the
reorder_clausesoptimization can rewrite).Fully backward compatible: the existing 2-arg
(annotations, weights)signature still works.annotate()dispatches onco_argcountand only passes the new arguments when the function accepts them.Problem
The old 2-arg interface receives
annotations: List[List[Interval]]— for each clause, a flat list of the bounds of the atoms that satisfied that clause. The relational join across body variables is performed inside_ground_ruleand then projected away before the annotation function runs. That projection erases the only information needed to answer "which grounding does this bound belong to?".Concrete failure case (see
examples/test_groundings.py):What the annotation function sees under the 2-arg interface:
annotations[0]=[[0.5,1], [0.6,1]]—hasLabel(CB1, X)bounds, X values discardedannotations[1]=[[0.3,1], [0.8,1]]—hasLabel(CB2, Y)bounds, Y values discardedannotations[2]=[[1,1], [1,1], [1,1]]—conn(X, Y)bounds, (X, Y) pairs discardedThe correct answer for
hackerAt(b)is[0.5, 1.0], derived from the single groundingX=l1, Y=l5(becauseconn(l1, l5)exists and pairs the strongesthasLabel(a, l1)=[0.5,1]with the strongest availablehasLabel(b, l5)=[0.8,1], givingmin(0.5, 0.8) = 0.5). But with only the flat lists above:min(0.3, 0.5, 1.0) = 0.3— the[0.3, 1]fromhasLabel(b, l4)poisons the result, even thoughl4was only paired withl1andl2, not with theY=l5grounding that actually wins.annotations[i]can tell thatX=l1paired withY=l5(notY=l4), because that pairing lives inconn's(X, Y)tuples, which were thrown away.You can decompose the rule into two rules (an intermediate
isConnected(X, Y)) and the math works out — but only because the intermediate predicate carries the (X, Y) pair as part of its head, smuggling the join information back through the interpretation. That's a workaround, not a fix; it forces every join-aware annotation to manufacture an intermediate predicate solely to thread variables through.This PR exposes the structure the engine already computes (
qualified_edges,clause_labels,clause_variables) so the annotation function can do the join lookup itself. In the example,ann_fn_pairediterates overconn's(X, Y)edges, then looks up each side'shasLabelbound by matching the edge endpoint — and returns[0.5, 1.0]from a single rule.Changes
Engine (all three interpretation backends —
pyreason/scripts/interpretation/interpretation.py,interpretation_fp.py,interpretation_parallel.py)node_applicable_rule_type/edge_applicable_rule_typenumba tuples with two new fields:clause_labels: ListType(label.label_type)— per-clause predicate labelclause_variables: ListType(ListType(string))— per-clause variable names_ground_rulenow populates these alongsidequalified_nodes/qualified_edges. The collection guard is widened fromif atom_trace:toif atom_trace or ann_fn != '':so the metadata is available whenever an annotation function is attached, even if atom tracing is off.annotate(...)gains four new parameters (qualified_nodes, qualified_edges, clause_labels, clause_variables) and arity-dispatches:>=6 args: new signature — passes everything through.(annotations, weights)— unchanged behavior.Tests (
tests/unit/disable_jit/interpretations/)test_ground_rule_helpers.py,test_old_interp_file.py, andtest_reason_core.pyto account for the two new trailing fields.test_interpretation_common.pyannotate(...)call signatures.Example (
examples/test_groundings.py)ann_fn_paired) on a rule with aconn(X, Y)join, showing how to recover per-grounding pairings via predicate name + variable role rather than body position. Includes two legacy 2-arg functions for comparison.Backward compatibility
annotatekeys offco_argcount._ground_ruleneeds two extra_placeholders. The in-tree tests in this PR show the pattern.Test plan
pytest tests/— full unit suite (including the updateddisable_jittests).examples/test_groundings.pyand confirmhackerAt(CB2)is derived from the conn-driven pairings (expected bounds printed byann_fn_paired).ann_fnexample to confirm the legacy path is unaffected.interpretation_fpandinterpretation_parallelpaths on a small graph to confirm the numba tuple type change compiles in both backends.