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.
Lets define our domain model and build a small world. We will then build a rule tree that adds Drawer, Door, and Wardrobe instances to the world.
from krrood.entity_query_language.entity import entity, an, let, and_, Symbol, inference
from krrood.entity_query_language.rule import refinement, alternative
from krrood.entity_query_language.conclusion import Add
from dataclasses import dataclass, field
from typing_extensions import List
# --- Domain model
@dataclass
class Body(Symbol):
name: str
size: int = 1
@dataclass
class Container(Body):
...
@dataclass
class Handle(Body):
...
@dataclass
class Connection(Symbol):
parent: Body
child: Body
@dataclass
class FixedConnection(Connection):
...
@dataclass
class RevoluteConnection(Connection):
...
@dataclass
class World(Symbol):
id_: int
bodies: List[Body]
connections: List[Connection] = field(default_factory=list)
@dataclass
class View(Symbol): # 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]
First build the base query.
from krrood.entity_query_language.predicate import HasType
# --- Build the starting query
# Declare the variables
fixed_connection = let(type_=FixedConnection, domain=world.connections)
revolute_connection = let(type_=RevoluteConnection, domain=world.connections)
views = inference(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 = an(entity(views, HasType(fixed_connection.child, Handle)))
Then we build the rule tree.
# --- Build the rule tree
with query:
# Base conclusion: if a fixed connection exists between body and handle,
# we consider it a Drawer by default.
Add(views, inference(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, inference(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, inference(Wardrobe)(handle=handle, body=body, container=container))
Finally, we evaluate the rule 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)
print(*results, sep="\n")
Drawer(handle=Handle(name='Handle1', size=1), container=Container(name='Container1', size=1))
Door(handle=Handle(name='Handle2', size=1), body=Body(name='Body2', size=2))
Wardrobe(handle=Handle(name='Handle3', size=1), body=Body(name='Body3', size=1), container=Container(name='Container2', size=1))
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).