Reference¶
The following diagram shows all the system components and the relationships among them.
![digraph relation {
"Fact";
"KnowledgeEngine" [label="KnowledgeEngine"];
"CE" [label="Conditional Element (CE)"];
"Fact" [label="Fact"];
"FC" [label="Field Constraint (FC)"];
"Pattern" [label="Pattern", shape="box"];
"CE" -> "CE" [label="contains"];
"CE" -> "Pattern" [label="contains"];
"FC" -> "FC" [label="some of them contains"];
"Pattern" -> "FC" [label="contains"]
"Rule" -> "CE" [label="contains"];
"Rule" -> "Pattern" [label="contains"];
"Pattern" -> "Fact" [label="which is a special usage for"];
"KnowledgeEngine" -> "Rule" [label="contains"];
"Fact" -> "KnowledgeEngine" [label="declare"];
"Fact" -> "KnowledgeEngine" [label="retract"];
"KnowledgeEngine" -> "Agenda" [label="has"];
"KnowledgeEngine" -> "FactList" [label="has"];
"KnowledgeEngine" -> "Strategy" [label="has"];
"FactList" -> "Fact" [label="contains"];
"Agenda" -> "Activation" [label="contains"];
"Activation" -> "Rule" [label="contains"];
"Activation" -> "Fact" [label="contains"];
"Activation" -> "Context" [label="contains"];
"Strategy" -> "Agenda" [label="organize"];
}](_images/graphviz-4334b0386151bf55a0e0c654fff3f5acdda9b055.png)
Rule¶
Rule is the basic method of composing patterns. You can add as many patterns or conditional elements as you want to a Rule and it will fire if every one of them matches. Therefore, it behaves like AND by default.
@Rule(<pattern_1>,
<pattern_2>,
...
<pattern_n>)
def _():
pass
The following diagram shows the rules of composition of a rule:
![digraph relation {
"CE" [label="Conditional Element (CE)\nAND, OR, NOT"];
"CFC" [label="Composable FC\n&, \|, ~", shape="box"];
"FC" [label="Field Constraint (FC)\nL(), W(), P()"];
"Pattern" [label="Pattern", shape="box"];
"CE" -> "CE" [label="contains"];
"CE" -> "Pattern" [label="contains"];
"CFC" -> "FC" [label="contains"];
"Pattern" -> "FC" [label="contains"]
"Pattern" -> "CFC" [label="contains"]
"Rule" -> "CE" [label="contains"];
"Rule" -> "Pattern" [label="contains"];
}](_images/graphviz-59f8d0a8752c6b1f371c66d5beefb2bf85b114fb.png)
Conditional Elements: Composing Patterns Together¶
AND¶
AND creates a composed pattern containing all Facts passed as arguments. All of the passed patterns must match for the composed pattern to match.
@Rule(AND(Fact(1),
Fact(2)))
def _():
pass
OR¶
OR creates a composed pattern in which any of the given pattern will make the rule match.
@Rule(OR(Fact(1),
Fact(2)))
def _():
pass
Warning
If multiple facts match, the rule will be fired multiple times, one for each valid combination of matching facts.
NOT¶
This element matches if the given pattern does not match with any fact or combination of facts. Therefore this element matches the absence of the given pattern.
@Rule(NOT(Fact(1)))
def _():
pass
TEST¶
Check the received callable against the current binded values. If the execution returns True the evaluation will continue and stops otherwise.
@Rule(Number(MATCH.a),
Number(MATCH.b),
TEST(lambda a, b: a > b),
Number(MATCH.c),
TEST(lambda b, c: b > c))
def _(a, b, c):
pass
EXISTS¶
This CE receives a pattern and matches if one or more facts matches this pattern. This will match only once while one or more matching facts exists and will stop matching when there is no matching facts.
@Rule(EXISTS(Color()))
def _():
pass
FORALL¶
The FORALL conditional element provides a mechanism for determining if a group of specified CEs is satisfied for every occurence of another specified CE.
@Rule(FORALL(Student(MATCH.name),
Reading(MATCH.name),
Writing(MATCH.name),
Arithmetic(MATCH.name)))
def all_students_passed():
pass
Note
All binded variables captured inside a FORALL clause won’t be passed as context to the RHS of the rule.
Note
Any time the rule is activated the matching fact is the InitialFact.
Field Constraints: FC for sort¶
L (Literal Field Constraint)¶
This element performs an exact match with the given value. The matching is done using the equality operator ==.
@Rule(Fact(L(3)))
def _():
pass
Note
This is the default FC used when no FC is given as a pattern value. pattern.
W (Wildcard Field Constraint)¶
This element matches with any value.
@Rule(Fact(mykey=W()))
def _():
pass
Note
This element only matches if the element exist.
P (Predicate Field Constraint)¶
The match of this element is the result of applying the given callable to the fact-extracted value. If the callable returns True the FC will match, in other case the FC will not match.
@Rule(Fact(P(lambda x: isinstance(x, int))))
def _():
pass
Composing FCs: &, | and ~¶
All FC can be composed together using the composition operators &, | and ~.
ANDFC() a.k.a. &¶
The composed FC matches if all the given FC match.
@Rule(Fact(x=P(lambda x: x >= 0) & P(lambda x: x <= 255)))
def _():
pass
Variable Binding: The << Operator¶
Any pattern and some FCs can be binded to a name using the << operator.
@Rule(Fact('value' << W()))
def _(value):
pass
Deprecated since version 1.2.0: Use MATCH object instead.
@Rule('f1' << Fact())
def _(f1):
pass
Deprecated since version 1.2.0: Use AS object instead.
MATCH object¶
The MATCH objects helps generating more readable name bindings. Is syntactic sugar for a Wildcard Field Constraint binded to a name. For example:
@Rule(Fact(MATCH.myvalue))
def _(myvalue):
pass
Is exactly the same as:
@Rule(Fact("myvalue" << W()))
def _(myvalue):
pass
AS object¶
The AS object like the MATCH object is syntactic sugar for generating bindable names. In this case any attribute requested to the AS object will return a string with the same name.
@Rule(AS.myfact << Fact(W()))
def _(myfact):
pass
Is exactly the same as:
@Rule("myfact" << Fact(W()))
def _(myfact):
pass
Warning
This behavior will vary in future releases of Experta and the string flavour of the operator may disappear.
Nested matching¶
New in version 1.3.0.
Nested matching is useful to match against Fact values which contains nested structures like dicts or lists.
>>> Fact(name="scissors", against={"scissors": 0, "rock": -1, "paper": 1})
>>> Fact(name="paper", against={"scissors": -1, "rock": 1, "paper": 0})
>>> Fact(name="rock", against={"scissors": 1, "rock": 0, "paper": -1})
Nested matching take the form field__subkey=value. (That’s a double-underscore). For example:
>>> @Rule(Fact(name=MATCH.name, against__scissors=1, against__paper=-1))
... def what_wins_to_scissors_and_losses_to_paper(self, name):
... print(name)
Is possible to match against an arbitrary deep structure following the same method.
>>> class Ship(Fact):
... pass
...
>>> Ship(data={
... "name": "SmallShip",
... "position": {
... "x": 300,
... "y": 200},
... "parent": {
... "name": "BigShip",
... "position": {
... "x": 150,
... "y": 300}}})
In this example we can check for collision between a ship and its parent with the following rule:
>>> @Rule(Ship(data__name=MATCH.name1,
... data__position__x=MATCH.x,
... data__position__y=MATCH.y,
... data__parent__name=MATCH.name2,
... data__parent__position__x=MATCH.x,
... data__parent__position__y=MATCH.y))
... def collision_detected(self, name1, name2, **_):
... print("COLLISION!", name1, name2)
If the nested data structure contains list, tuples or any other sequence you can use numeric indexes as needed.
>>> Ship(data={
... "name": "SmallShip",
... "position": {
... "x": 300,
... "y": 200},
... "enemies": [
... {"name": "Destroyer"},
... {"name": "BigShip"}]})
>>>
>>> @Rule(Ship(data__enemies__0__name="Destroyer"))
... def next_enemy_is_destroyer(self):
... print("Bye byee!")
Mutable objects¶
Experta’s matching algorithm depends on the values of the declared facts being immutable.
When a Fact is created, all its values are transformed to an immutable type if they are not. For this matter the method experta.utils.freeze is used internally.
>>> class MutableTest(KnowledgeEngine):
... @Rule(Fact(v1=MATCH.v1, v2=MATCH.v2, v3=MATCH.v3))
... def is_immutable(self, v1, v2, v3):
... print(type(v1), "is Immutable!")
... print(type(v2), "is Immutable!")
... print(type(v3), "is Immutable!")
...
>>> ke = MutableTest()
>>> ke.reset()
>>> ke.declare(Fact(v1={"a": 1, "b": 2}, v2=[1, 2, 3], v3={1, 2, 3}))
>>> ke.run()
frozendict is Immutable
frozenlist is Immutable
frozenset is Immutable
>>>
Note
You can import frozendict and frozenlist from experta.utils module. However frozenset is a Python built-in type.
Register your own mutable freezer¶
If you need to include your own custom mutable types as fact values you have to register a specialized type freezer for your custom type.
>>> from experta.utils import freeze
>>> @freeze.register(MyType)
... def freeze_mytype(obj):
... return ... # My frozen version of my type
Unfreeze frozen objects¶
To easily unfreeze the frozen objects experta.utils contains an unfreeze method.
>>> class MutableTest(KnowledgeEngine):
... @Rule(Fact(v1=MATCH.v1, v2=MATCH.v2, v3=MATCH.v3))
... def is_immutable(self, v1, v2, v3):
... print(type(unfreeze(v1)), "is Mutable!")
... print(type(unfreeze(v2)), "is Mutable!")
... print(type(unfreeze(v3)), "is Mutable!")
...
>>> ke = MutableTest()
>>> ke.reset()
>>> ke.declare(Fact(v1={"a": 1, "b": 2}, v2=[1, 2, 3], v3={1, 2, 3}))
>>> ke.run()
dict is Mutable
list is Mutable
set is Mutable
>>>
Note
The same freeze registration procedure shown above also applies to unfreeze.