Advanced Example: Rule Trees with Alternatives and Exceptions

Contents

Advanced Example: Rule Trees with Alternatives and Exceptions#

This example demonstrates how to build a rule tree using refinement (specialization) and alternatives (mutually exclusive branches). It shows how to:

  • Start from a base conclusion;

  • Add a refined exception (more specific case) that overrides the base when a further condition is met;

  • Add alternatives that apply under different conditions.

We will construct objects symbolically using symbolic_rule and Add, with let placeholders to describe relationships.

Example Usage#

from entity_query_language import entity, an, let, and_, symbolic_mode, symbol, refinement, alternative, Add, rule_mode, \
    HasType, infer
from dataclasses import dataclass, field
from typing_extensions import List


# --- Domain model
@symbol
@dataclass
class Body:
    name: str
    size: int = 1


@dataclass
class Container(Body):
    ...


@dataclass
class Handle(Body):
    ...

@symbol
@dataclass
class Connection:
    parent: Body
    child: Body


@dataclass
class FixedConnection(Connection):
    ...


@dataclass
class RevoluteConnection(Connection):
    ...

@symbol
@dataclass
class World:
    id_: int
    bodies: List[Body]
    connections: List[Connection] = field(default_factory=list)

@symbol
@dataclass
class View:  # A common super-type for Drawer/Door/Wardrobe in this example
    ...


# Views we will construct symbolically
@dataclass
class Drawer(View):
    handle: Body
    container: Body


@dataclass
class Door(View):
    handle: Body
    body: Body


@dataclass
class Wardrobe(View):
    handle: Body
    body: Body
    container: Body


# --- Build a small "world"
container1, body2, body3, container2 = Container("Container1"), Body("Body2", size=2), Body("Body3"), Container("Container2")
handle1, handle2, handle3 = Handle("Handle1"), Handle("Handle2"), Handle("Handle3")
world = World(1, [container1, container2, body2, body3, handle1, handle2, handle3])

# Connections between bodies/handles
fixed_1 = FixedConnection(container1, handle1)
fixed_2 = FixedConnection(body2, handle2)
fixed_3 = FixedConnection(body3, handle3)
revolute_1 = RevoluteConnection(container2, body3)
world.connections = [fixed_1, fixed_2, fixed_3, revolute_1]

with symbolic_mode():
    # Declare the variables
    fixed_connection = let(type_=FixedConnection, domain=world.connections)
    revolute_connection = let(type_=RevoluteConnection, domain=world.connections)
    views = let(type_=View)

    # Define aliases for convenience
    handle = fixed_connection.child
    body = fixed_connection.parent
    container = revolute_connection.parent

    # Describe base query
    # We use a single selected variable that we will Add to in the rule tree.
    query = infer(entity(views, HasType(fixed_connection.child, Handle)))

# --- Build the rule tree
with rule_mode(query):
    # Base conclusion: if a fixed connection exists between body and handle,
    # we consider it a Drawer by default.
    Add(views, Drawer(handle=handle, container=body))

    # Exception (refinement): If the body is "bigger" (size > 1), instead add a Door.
    # This refinement branch is more specific and can be seen as a refinement to the base rule.
    with refinement(body.size > 1):
        Add(views, Door(handle=handle, body=body))

        # Alternative refinement when the first refinement didn't fire: if the body is also connected to a parent
        # container via a revolute connection (alternative pattern), add a Wardrobe instead.
        with alternative(body == revolute_connection.child, container == revolute_connection.parent):
            Add(views, Wardrobe(handle=handle, body=body, container=container))

# query._render_tree_()

# Evaluate the rule tree
results = list(query.evaluate())

# The results include objects built from different branches of the rule tree.
# Depending on the world, you should observe a mix of Drawer, Door, and Wardrobe instances.
assert len(results) == 3
assert any(isinstance(v, Drawer) and v.handle.name == "Handle1" for v in results)
assert any(isinstance(v, Door) and v.handle.name == "Handle2" for v in results)
assert any(isinstance(v, Wardrobe) and v.handle.name == "Handle3" for v in results)

Notes#

  • refinement(*conditions): narrows the context with an additional condition (like an exception/specialization).

  • alternative(*conditions): introduces a sibling branch with its own conditions; only if those are satisfied will that branch contribute conclusions.

  • Add(target, value): materializes a conclusion into the selected variable (here, a collection-like placeholder views).