Skip to content

Extend annotation function capabilites#149

Merged
ColtonPayne merged 13 commits into
mainfrom
update-annotation-bounds
May 13, 2026
Merged

Extend annotation function capabilites#149
ColtonPayne merged 13 commits into
mainfrom
update-annotation-bounds

Conversation

@ColtonPayne
Copy link
Copy Markdown
Collaborator

@ColtonPayne ColtonPayne commented May 6, 2026

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_clauses optimization can rewrite).

Fully backward compatible: the existing 2-arg (annotations, weights) signature still works. annotate() dispatches on co_argcount and 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_rule and 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):

hasLabel(a, l1):[0.5,1]   hasLabel(a, l2):[0.6,1]
hasLabel(b, l4):[0.3,1]   hasLabel(b, l5):[0.8,1]
conn(l1, l4)  conn(l2, l4)  conn(l1, l5)  conn(l1, l6)

hackerAt(CB2):ann_fn <- hasLabel(CB1, X), hasLabel(CB2, Y), conn(X, Y)

What the annotation function sees under the 2-arg interface:

  • annotations[0] = [[0.5,1], [0.6,1]]hasLabel(CB1, X) bounds, X values discarded
  • annotations[1] = [[0.3,1], [0.8,1]]hasLabel(CB2, Y) bounds, Y values discarded
  • annotations[2] = [[1,1], [1,1], [1,1]]conn(X, Y) bounds, (X, Y) pairs discarded

The correct answer for hackerAt(b) is [0.5, 1.0], derived from the single grounding X=l1, Y=l5 (because conn(l1, l5) exists and pairs the strongest hasLabel(a, l1)=[0.5,1] with the strongest available hasLabel(b, l5)=[0.8,1], giving min(0.5, 0.8) = 0.5). But with only the flat lists above:

  • Any aggregation that mins across clauses (e.g. weakest-link) takes min(0.3, 0.5, 1.0) = 0.3 — the [0.3, 1] from hasLabel(b, l4) poisons the result, even though l4 was only paired with l1 and l2, not with the Y=l5 grounding that actually wins.
  • Any aggregation that maxes across clauses ignores the join entirely.
  • No purely positional function over annotations[i] can tell that X=l1 paired with Y=l5 (not Y=l4), because that pairing lives in conn'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_paired iterates over conn's (X, Y) edges, then looks up each side's hasLabel bound 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)

  • Extend node_applicable_rule_type / edge_applicable_rule_type numba tuples with two new fields:
    • clause_labels: ListType(label.label_type) — per-clause predicate label
    • clause_variables: ListType(ListType(string)) — per-clause variable names
  • _ground_rule now populates these alongside qualified_nodes / qualified_edges. The collection guard is widened from if atom_trace: to if 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.
    • else: legacy (annotations, weights) — unchanged behavior.
  • All call sites in the reasoning loops unpack the extended tuple and forward the new args.

Tests (tests/unit/disable_jit/interpretations/)

  • Updated applicable-rule tuple destructuring across test_ground_rule_helpers.py, test_old_interp_file.py, and test_reason_core.py to account for the two new trailing fields.
  • Updated test_interpretation_common.py annotate(...) call signatures.

Example (examples/test_groundings.py)

  • New example demonstrating the 6-arg annotation function (ann_fn_paired) on a rule with a conn(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

  • Existing 2-arg annotation functions continue to work unchanged — the dispatch in annotate keys off co_argcount.
  • Applicable-rule tuple shape changed; any external code that unpacked the 5-tuple from _ground_rule needs two extra _ placeholders. The in-tree tests in this PR show the pattern.

Test plan

  • pytest tests/ — full unit suite (including the updated disable_jit tests).
  • Run examples/test_groundings.py and confirm hackerAt(CB2) is derived from the conn-driven pairings (expected bounds printed by ann_fn_paired).
  • Run an existing 2-arg ann_fn example to confirm the legacy path is unaffected.
  • Spot-check interpretation_fp and interpretation_parallel paths on a small graph to confirm the numba tuple type change compiles in both backends.

@ColtonPayne ColtonPayne changed the base branch from main to cikm-paper May 6, 2026 20:55
@ColtonPayne ColtonPayne changed the base branch from cikm-paper to main May 6, 2026 20:57
@ColtonPayne ColtonPayne merged commit e1a94af into main May 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants