Inference Explanation#

When EQL infers a new object via an inference(...) rule, it automatically attaches an InferenceExplanation to that object. The explanation records:

  • Which query node produced the instance.

  • The source statement where the query was written.

  • Which conditions in the query were satisfied (and their ConditionAndBindings representation).

  • The OperationResult from the evaluation, carrying the complete variable bindings.

explain_inference returns None for instances that were not produced by an inference variable (e.g. plain instances constructed directly).


Data Model#

All examples on this page share the same domain. We model bodies, handles, containers, and two kinds of kinematic connections between them. The hierarchy mirrors the dataset used in the test_meta_queries test suite so readers can cross-reference the tests directly.

from dataclasses import dataclass

from krrood.entity_query_language.factories import (
    entity, variable, an, inference, exists, or_,
)
from krrood.entity_query_language.explanation.explanation import (
    explain_inference, ConditionAndBindings,
)
from krrood.symbol_graph.symbol_graph import Symbol


@dataclass
class ExampleBody(Symbol):
    name: str


@dataclass
class ExampleHandle(ExampleBody): ...


@dataclass
class ExampleContainer(ExampleBody): ...


@dataclass
class ExampleConnection(Symbol):
    parent: ExampleBody
    child: ExampleBody


@dataclass
class ExampleFixedConnection(ExampleConnection): ...


@dataclass
class ExamplePrismaticConnection(ExampleConnection): ...


@dataclass
class ExampleDrawer(Symbol):
    container: ExampleContainer
    handle: ExampleHandle


# Concrete domain objects
base = ExampleBody("base")
containers = [ExampleContainer("C1"), ExampleContainer("C2")]
handles = [ExampleHandle("H1"), ExampleHandle("H2")]
prismatic_conns = [
    ExamplePrismaticConnection(parent=base, child=containers[0]),
    ExamplePrismaticConnection(parent=base, child=containers[1]),
]
fixed_conns = [
    ExampleFixedConnection(parent=containers[0], child=handles[0]),
    ExampleFixedConnection(parent=containers[1], child=handles[1]),
]

Drawer Rule#

A Drawer is inferred wherever a FixedConnection shares a parent with a PrismaticConnection’s child, and its other end connects to a Handle. We use an(Type)(...).from_(domain) to express this structural constraint directly in the variable definition rather than a separate .where() clause.

handle_var = variable(ExampleHandle, handles)
prismatic_conn = variable(ExamplePrismaticConnection, prismatic_conns)
fixed_conn = an(ExampleFixedConnection)(
    parent=prismatic_conn.child, child=handle_var
).from_(fixed_conns)
# .expression lowers the Match to its selection Entity for symbolic attribute access
drawers = inference(ExampleDrawer)(
    container=fixed_conn.expression.parent, handle=fixed_conn.expression.child
).tolist()

print(f"Inferred {len(drawers)} drawer(s): {drawers}")
Inferred 2 drawer(s): [ExampleDrawer(container=ExampleContainer(name='C1'), handle=ExampleHandle(name='H1')), ExampleDrawer(container=ExampleContainer(name='C2'), handle=ExampleHandle(name='H2'))]

The an(Type)(...).from_(domain) call implicitly generates two equality conditions that must hold for each candidate FixedConnection:

  1. fixed_conn.parent == prismatic_conn.child

  2. fixed_conn.child == handle_var

These are the conditions the explanation will report.


Simple Usage#

Retrieving and printing the explanation#

expl = explain_inference(drawers[0])
print(type(expl))
print(expl is None)  # False — the drawer was produced by inference
<class 'krrood.entity_query_language.explanation.explanation.InferenceExplanation'>
False

as_string() prints the inferred instance, the inference variable that produced it, the source statement where the query was written, and the satisfied conditions rendered via ConditionAndBindings:

print(expl.as_string())
Instance: ExampleDrawer(container=ExampleContainer(name='C1'), handle=ExampleHandle(name='H1'))
Produced by inference variable: ExampleDrawer
Query source (<module>, 553795225.py:7):
  drawers = inference(ExampleDrawer)(
      container=fixed_conn.expression.parent, handle=fixed_conn.expression.child
  ).tolist()
Satisfied conditions:
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)
  AND (ExampleFixedConnection.child == ExampleHandle)

Calling explain_inference on a directly constructed object returns None:

direct = ExampleDrawer(container=containers[0], handle=handles[0])
print(f"{explain_inference(direct)} -> Was not Inferred by an EQL rule")  # None
None -> Was not Inferred by an EQL rule

Satisfied conditions via ConditionAndBindings#

get_satisfied_conditions_and_their_bindings() returns a list of ConditionAndBindings objects. Each one renders its condition expression using ConditionAndBindings.__repr__, which formats comparators as (left operator right):

for cond in expl.get_satisfied_conditions_and_their_bindings():
    print(cond)
(ExampleFixedConnection.parent == ExamplePrismaticConnection.child)
(ExampleFixedConnection.child == ExampleHandle)

Condition graph#

condition_graph() returns a QueryGraph with is_satisfied flags on every node. Satisfied condition nodes keep their type-based colour; unsatisfied (or short-circuited) ones are faded.

import io
import matplotlib.pyplot as plt
from IPython.display import Image

graph = expl.condition_graph()
fig, ax = graph.visualize(filename="drawer_explanation.pdf")

buf = io.BytesIO()
fig.savefig(buf, format="png", dpi=100, bbox_inches="tight", facecolor="white")
buf.seek(0)
plt.close(fig)
Image(buf.read())
findfont: Failed to find font weight medium, now using 400.
../../_images/a88c58b6cfa8785f392f4c81e6ece27f4a621ffacfe572078c1250eb362a07e1.png

Including the call stack in the output#

as_string() accepts two optional parameters for richer output. Pass show_trace=True to append the call stack recorded at the time the query was defined. Optionally filter the stack to frames from a specific package with focus_package:

# Show the full explanation including the call stack, filtered to user frames only
print(expl.as_string(show_trace=True))
Instance: ExampleDrawer(container=ExampleContainer(name='C1'), handle=ExampleHandle(name='H1'))
Produced by inference variable: ExampleDrawer
Query source (<module>, 553795225.py:7):
  drawers = inference(ExampleDrawer)(
      container=fixed_conn.expression.parent, handle=fixed_conn.expression.child
  ).tolist()
Satisfied conditions:
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)
  AND (ExampleFixedConnection.child == ExampleHandle)
Call stack at definition:
  File "<string>", line 14, in __init__
    ???
  File "/home/runner/work/cognitive_robot_abstract_machine/cognitive_robot_abstract_machine/krrood/src/krrood/entity_query_language/factories.py", line 545, in <lambda>
    return lambda **kwargs: InstantiatedVariable(
  File "/tmp/ipykernel_3743/553795225.py", line 7, in <module>
    drawers = inference(ExampleDrawer)(
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/asyncio/base_events.py", line 1999, in _run_once
    handle._run()
  File "/opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/asyncio/base_events.py", line 645, in run_forever
    self._run_once()
  File "<frozen runpy>", line 88, in _run_code
    ???
  File "<frozen runpy>", line 198, in _run_module_as_main
    ???

Comparator-only conditions#

get_satisfied_comparator_conditions() is a convenience filter that returns only the satisfied conditions that are Comparator nodes (equality or ordering checks between two expressions), dropping higher-level logical wrappers such as OR or Exists:

from krrood.entity_query_language.explanation.explanation import ConditionAndBindings

comparator_conds = expl.get_satisfied_comparator_conditions().tolist()
print(f"{len(comparator_conds)} satisfied comparator condition(s):")
for c in comparator_conds:
    print(" ", ConditionAndBindings(c, expl.operation_result.all_bindings))
2 satisfied comparator condition(s):
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)
  (ExampleFixedConnection.child == ExampleHandle)

get_satisfied_comparator_conditions_between_attributes() narrows this further to comparators whose both sides are Attribute nodes — that is, conditions of the form variable_a.some_field == variable_b.some_field. These are the structural join conditions that the planner uses to link variables together:

attr_conds = expl.get_satisfied_comparator_conditions_between_attributes().tolist()
print(f"{len(attr_conds)} attribute-to-attribute comparator(s):")
for c in attr_conds:
    print(" ", ConditionAndBindings(c, expl.operation_result.all_bindings))
1 attribute-to-attribute comparator(s):
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)

OR Branch Short-Circuit#

To show the short-circuit behaviour of or_() with structurally different arms we use a separate, simpler query. The left arm uses exists() to check whether there is any PrismaticConnection whose child is the parent of the fixed connection (a relational check); the right arm falls back to a string predicate on a completely different attribute.

fixed_var = variable(ExampleFixedConnection, fixed_conns)
prismatic_var = variable(ExamplePrismaticConnection, prismatic_conns)
or_drawers = (
    entity(inference(ExampleDrawer)(
        container=fixed_var.parent, handle=fixed_var.child))
    .where(or_(
        exists(prismatic_var, prismatic_var.child == fixed_var.parent),  # True → short-circuits
        fixed_var.parent.name.startswith("X"),                           # never entered
    ))
    .tolist()
)

print(f"Inferred {len(or_drawers)} drawer(s) via OR rule")
Inferred 2 drawer(s) via OR rule

In the condition graph for this explanation the OR and Exists nodes are satisfied and rendered at full opacity. The right arm (startswith("X")) was short-circuited by the successful exists(...) check and is faded.

or_expl = explain_inference(or_drawers[0])
or_graph = or_expl.condition_graph()
fig2, ax2 = or_graph.visualize(filename="or_drawer_explanation.pdf")

buf2 = io.BytesIO()
fig2.savefig(buf2, format="png", dpi=100, bbox_inches="tight", facecolor="white")
buf2.seek(0)
plt.close(fig2)
Image(buf2.read())
../../_images/6c2aa3de419a2c6f171c780ef2d491cd866f4db4b46e7edd9b4e6484a489f970.png

Meta-queries#

InferenceExplanation inherits from Symbol, making it a first-class entity in the SymbolGraph. Its methods return EQL Entity descriptors that can be chained, filtered, and composed just like ordinary queries. All examples below operate on expl — the explanation for the first drawer from the drawer rule above.

Which conditions were satisfied?#

Returns all satisfied condition expressions, including LogicalOperator wrappers produced by an(Type)(...).from_(domain) and the inference root.

conditions = expl.get_satisfied_condition_expressions_for_the_instance().tolist()
print(f"{len(conditions)} condition expression(s) (comparators + logical wrappers):")
for c in conditions:
    print(" ", ConditionAndBindings(c, expl.operation_result.all_bindings))
3 condition expression(s) (comparators + logical wrappers):
  AND (==,==)
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)
  (ExampleFixedConnection.child == ExampleHandle)

Which variable nodes participated (by type)?#

Returns the symbolic variable nodes whose _type_ is a subclass of the given type. Because ExampleFixedConnection and ExamplePrismaticConnection both inherit from ExampleConnection, the base-type query returns both:

conn_nodes = expl.get_variable_nodes_of_given_type(ExampleConnection).tolist()
print("Connection variable nodes:", [n._type_.__name__ for n in conn_nodes])

handle_nodes = expl.get_variable_nodes_of_given_type(ExampleHandle).tolist()
print("Handle variable nodes:", [n._type_.__name__ for n in handle_nodes])
Connection variable nodes: ['ExampleFixedConnection', 'ExamplePrismaticConnection']
Handle variable nodes: ['ExampleHandle']

What were the actual bound values?#

Returns the concrete domain objects that were bound to variable nodes of the given type:

bound_handles = expl.get_values_of_variable_nodes_of_given_type(ExampleHandle).tolist()
print("Bound handle value(s):", bound_handles)
Bound handle value(s): [ExampleHandle(name='H1')]

Which conditions relate two variables of the same type?#

Returns satisfied conditions that have at least two distinct variable nodes of the given type in their descendant tree.

Because ExampleFixedConnection and ExamplePrismaticConnection are both subclasses of ExampleConnection, the condition fixed_conn.parent == prismatic_conn.child has two ExampleConnection-typed descendants. It is therefore returned here:

same_conn = expl.get_conditions_that_relate_the_variables_of_type(ExampleConnection).tolist()
print("Conditions relating two Connection variables:")
for c in same_conn:
    print(" ", ConditionAndBindings(c, expl.operation_result.all_bindings))
Conditions relating two Connection variables:
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)

Which conditions relate variables of two different types?#

Returns satisfied conditions that have at least one descendant of each specified type. Here fixed_conn.parent == prismatic_conn.child joins an ExampleFixedConnection to an ExamplePrismaticConnection:

cross = expl.get_conditions_that_relate_variables_of_types(
    ExampleFixedConnection, ExamplePrismaticConnection
).tolist()
print("Conditions relating FixedConnection ↔ PrismaticConnection:")
for c in cross:
    print(" ", ConditionAndBindings(c, expl.operation_result.all_bindings))
Conditions relating FixedConnection ↔ PrismaticConnection:
  (ExampleFixedConnection.parent == ExamplePrismaticConnection.child)

The method is symmetric — swapping the two types returns the same set:

cross_rev = expl.get_conditions_that_relate_variables_of_types(
    ExamplePrismaticConnection, ExampleFixedConnection
).tolist()
print("Symmetric result:", {c._id_ for c in cross} == {c._id_ for c in cross_rev})
Symmetric result: True

And the condition that links ExampleFixedConnection to ExampleHandle:

fixed_to_handle = expl.get_conditions_that_relate_variables_of_types(
    ExampleFixedConnection, ExampleHandle
).tolist()
print("Conditions relating FixedConnection ↔ Handle:")
for c in fixed_to_handle:
    print(" ", ConditionAndBindings(c, expl.operation_result.all_bindings))
Conditions relating FixedConnection ↔ Handle:
  (ExampleFixedConnection.child == ExampleHandle)

Stack Queries#

Every InferenceExplanation also captures the Python call stack at the moment the query was defined. The following methods let you inspect that provenance without converting it to a string.

How many frames were captured?#

print(f"Captured {expl.frame_count} call stack frame(s)")
Captured 8 call stack frame(s)

Was the inference triggered from inside a class method?#

print(f"Triggered from a method: {expl.is_triggered_from_method()}")
Triggered from a method: True

Which classes appear in the call stack?#

triggering_classes() returns the distinct class objects that appear in the captured call stack, in order of first occurrence (innermost first):

classes = expl.triggering_classes()
print("Triggering class(es):", [c.__name__ for c in classes])
Triggering class(es): ['InstantiatedVariable', 'Handle', '_UnixSelectorEventLoop']

Which functions appear in the call stack?#

triggering_functions() returns the distinct resolved callable objects, innermost first. Nested functions defined inside other functions may not be resolvable and will be absent from this list:

funcs = expl.triggering_functions()
print("Triggering function(s):", [f.__name__ if hasattr(f, "__name__") else repr(f) for f in funcs])
Triggering function(s): ['__init__', '_run', '_run_code', '_run_module_as_main']

Locating the outermost entry point into a package#

root_frame_in(package) returns the outermost (highest in the call hierarchy) frame whose module_name contains the given package string. This identifies the highest-level entry point into your library that triggered the inference — useful when you want to know where inside your own codebase the query originated:

frame = expl.root_frame_in("krrood")
if frame is not None:
    import os
    print(f"Root krrood frame: {frame.function_name} at {os.path.basename(frame.filename)}:{frame.lineno}")
else:
    print("No krrood frame found")
Root krrood frame: <lambda> at factories.py:545

API Reference#