Using Pyke
This describes how to use Pyke from within your Python program.
Initializing Pyke
There are two steps to initializing a Pyke knowledge engine:
- knowledge_engine.engine(paths = ('.',), generated_root_pkg = 'compiled_krb', load_fc = True, load_bc = True, load_fb = True, load_qb = True)
The Pyke inference engine is offered as a class so that you can instantiate multiple copies of it with different rule bases to accomplish different tasks. Once you have a knowledge_engine.engine object; generally, all of the functions that you need are provided directly by this object:
>>> from pyke import knowledge_engine >>> my_engine = knowledge_engine.engine('examples')
This expects either a single directory or a sequence of directories as the paths argument. It recursively walks each directory looking for Pyke source files (.kfb files, .krb files, and .kqb files). Each source file that it finds is compiled, if out of date, and then imported (depending on load_fc, load_bc, load_fb and load_qb). This causes all of the rule bases to be loaded and made ready to activate.
All generated Python source files and pickle files are placed in the generated_root_pkg. By default, this is the package "compiled_krb" in the program's current working directory. The generated_root_pkg may be a dotted module path. In this case, the module path must be on Python's search path for modules.
The last component of the generated_root_pkg will be created automatically if it does not already exist.
You probably want to add compiled_krb to your subversion global-ignores option.
If you change some of your Pyke source files, you can create a new engine object to compile and reload the generated Python modules without restarting your program. But note that you'll need to rerun the add_universal_fact calls that you made outside of your .kfb files.
- some_engine.add_universal_fact(kb_name, fact_name, arguments)
The add_universal_fact function is called once per fact. These facts are never deleted and apply to all cases.
Alternatively, you can place universal facts in a .kfb file so that they are loaded automatically.
>>> my_engine.add_universal_fact('family', 'son_of', ('bruce', 'thomas'))
Multiple facts with the same name are allowed.
>>> my_engine.add_universal_fact('family', 'son_of', ('david', 'bruce'))
But duplicate facts (with the same arguments) are silently ignored.
>>> my_engine.add_universal_fact('family', 'son_of', ('david', 'bruce')) >>> my_engine.get_kb('family').dump_universal_facts() son_of('bruce', 'thomas') son_of('david', 'bruce')
These facts are accessed as kb_name.fact_name(arguments) within the .krb files.
Setting up Each Case
Pyke is designed to be run multiple times for multiple cases. In general each case has its own set of starting facts and may use different rule bases, depending upon the situation.
Three functions initialize each case:
- some_engine.reset()
- The reset function is called once to delete all of the case specific facts from the last run. It also deactivates all rule bases.
- some_engine.assert_(kb_name, fact_name, arguments)
Call assert_ (or the equivalent, add_case_specific_fact, see Other Functions, below) for each starting fact for this case. Like universal facts, you may have multiple facts with the same name so long as they have different arguments.
>>> my_engine.assert_('family', 'son_of', ('michael', 'bruce')) >>> my_engine.assert_('family', 'son_of', ('fred', 'thomas')) >>> my_engine.assert_('family', 'son_of', ('fred', 'thomas'))
Duplicates with universal facts are also ignored.
>>> my_engine.assert_('family', 'son_of', ('bruce', 'thomas')) >>> my_engine.get_kb('family').dump_specific_facts() son_of('michael', 'bruce') son_of('fred', 'thomas') >>> my_engine.get_kb('family').dump_universal_facts() son_of('bruce', 'thomas') son_of('david', 'bruce')
There is no difference within the .krb files of how universal facts verses specific facts are used. The only difference between the two types of facts is that the specific facts are deleted when a reset is done.
>>> my_engine.reset() >>> my_engine.get_kb('family').dump_specific_facts() >>> my_engine.get_kb('family').dump_universal_facts() son_of('bruce', 'thomas') son_of('david', 'bruce')
- some_engine.activate(*rb_names)
Then call activate to activate the appropriate rule bases. This may be called more than once, if desired, or it can simply take multiple arguments.
>>> my_engine.activate('bc_example')
Your Pyke engine is now ready to prove goals for this case!
Proving Goals
Two functions are provided that cover the easy cases. More general functions are provided in Other Functions, below.
- some_engine.prove_1(kb_name, entity_name, fixed_args, num_returns)
Kb_name may name either a fact base, question base or rule base category.
The entity_name is the fact name for fact bases, question name for question bases or the name of the backward chaining goal for rule bases.
The fixed_args are a tuple of Python values. These form the first set of arguments to the proof. Num_returns specifies the number of additional pattern variables to be appended to these arguments for the proof. The bindings of these pattern variables will be returned as a tuple in the answer for the proof. For example:
some_engine.prove_1(some_rule_base_category, some_goal, (1, 2, 3), 2)
Proves the goal:
some_rule_base_category.some_goal (1, 2, 3, $ans_0, $ans_1)
And will return the bindings produced by the proof as ($ans_0, $ans_1).
Returns the first proof found as a 2-tuple: a tuple of the bindings for the num_returns pattern variables, and a plan. The plan is None if no plan was generated; otherwise, it is a Python function as described below.
>>> my_engine.prove_1('bc_example', 'father_son', ('thomas', 'david'), 1) ((('grand',),), None)Raises pyke.knowledge_engine.CanNotProve if no proof is found.
>>> my_engine.prove_1('bc_example', 'father_son', ('thomas', 'bogus'), 1) Traceback (most recent call last): ... CanNotProve: Can not prove bc_example.father_son(thomas, bogus, $ans_0)
- some_engine.prove_n(kb_name, entity_name, fixed_args, num_returns)
This returns a context manager for a generator yielding 2-tuples, a tuple whose length == num_returns and a plan, for each possible proof. Like prove_1, the plan is None if no plan was generated. Unlike prove_1 it does not raise an exception if no proof is found.
>>> from __future__ import with_statement >>> with my_engine.prove_n('bc_example', 'father_son', ('thomas',), 2) as gen: ... for ans in gen: ... print ans (('bruce', ()), None) (('david', ('grand',)), None)
Running and Pickling Plans
Once you've obtained a plan from prove_1 or prove_n, you just call it like a normal Python function. The arguments required are simply those specified, if any, in the taking clause of the rule proving the top-level goal.
You may call the plan function any number of times. You may even pickle the plan for later use. But the plans are constructed out of functools.partial functions, so you need to register this with copy_reg before pickling the plan:
>>> import copy_reg >>> import functools >>> copy_reg.pickle(functools.partial, ... lambda p: (functools.partial, (p.func,) + p.args))
No special code is required to unpickle a plan. Just unpickle and call it. (Unpickling the plan only imports one small Pyke module to be able to run the plan).
Tracing Rules
Individual rules may be traced to aid in debugging. The trace function takes two arguments: the rule base name, and the name of the rule to trace:
>>> my_engine.trace('bc_example', 'grand_father_son') >>> my_engine.prove_1('bc_example', 'father_son', ('thomas', 'david'), 1) bc_example.grand_father_son('thomas', 'david', '$ans_0') bc_example.grand_father_son succeeded with ('thomas', 'david', ('grand',)) ((('grand',),), None)
This can be done either before or after rule base activation and will remain in effect until you call untrace:
>>> my_engine.untrace('bc_example', 'grand_father_son') >>> my_engine.prove_1('bc_example', 'father_son', ('thomas', 'david'), 1) ((('grand',),), None)
Krb_traceback
A handy traceback module is provided to convert Python functions, lines and line numbers to the .krb file rule names, lines and line numbers in a Python traceback. This makes it much easier to read the tracebacks that occur during proofs.
The krb_traceback module has exactly the same functions as the standard Python traceback module, but they convert the generated Python function information into .krb file information. They also delete the intervening Python functions between subgoal proofs.
>>> import sys >>> from pyke import knowledge_engine >>> from pyke import krb_traceback >>> >>> my_engine = knowledge_engine.engine('examples') >>> my_engine.activate('error_test') >>> try: # doctest: +ELLIPSIS ... my_engine.prove_1('error_test', 'goal', (), 0) ... except: ... krb_traceback.print_exc(None, sys.stdout) # sys.stdout needed for doctest Traceback (most recent call last): File "<doctest using_pyke.txt[32]>", line 2, in <module> my_engine.prove_1('error_test', 'goal', (), 0) File "...knowledge_engine.py", line 234, in prove_1 return iter(it).next() File "...knowledge_engine.py", line 218, in gen for plan in it: File "...rule_base.py", line 46, in next return self.iterator.next() File "...knowledge_engine.py", line 40, in from_iterable for x in iterable: yield x File "...knowledge_engine.py", line 40, in from_iterable for x in iterable: yield x File "...error_test.krb", line 26, in rule1 goal2() File "...error_test.krb", line 31, in rule2 goal3() File "...error_test.krb", line 36, in rule3 goal4() File "...error_test.krb", line 41, in rule4 check $bar > 0 File "...contexts.py", line 227, in lookup_data raise KeyError("$%s not bound" % var_name) KeyError: '$bar not bound'
Other Functions
There are a few more functions that may be useful in special situations.
The first two of these provide more general access to the fact lookup and goal proof mechanisms. The catch is that you must first convert all arguments into patterns and create a context for these patterns. This is discussed below.
- some_engine.lookup(kb_name, entity_name, pattern_context, patterns)
- This returns a context manager for a generator that binds patterns to successive facts. Yields None for each successful match.
- some_engine.prove(kb_name, entity_name, pattern_context, patterns)
- Returns a context manager for a generator that binds patterns to successive proofs. Yields a prototype_plan or None for each successful match. To turn the prototype_plan into a Python function, use prototype_plan.create_plan(). This returns the plan function.
The remaining functions are:
- some_engine.add_case_specific_fact(kb_name, fact_name, args)
- This is an alternate to the assert_ function.
- some_engine.get_kb(kb_name)
- Finds and returns the knowledge base by the name kb_name. Raises KeyError if not found. Note that for rule bases, this returns the active rule base where kb_name is the rule base category name. Thus, not all rule bases are accessible through this call.
- some_engine.get_rb(rb_name)
- Finds and returns the rule base by the name rb_name. Raises KeyError if not found. This works for any rule base, whether it is active or not.
- some_engine.print_stats([f = sys.stdout])
- Prints a brief set of statistics for each knowledge base to file f. These are reset by the reset function. This will show how many facts were asserted, and counts of how many forward-chaining rules were fired and rerun, as well as counts of how many backward-chaining goals were tried, and how many backward-chaining rules matched, succeeded and failed. Note that one backward-chaining rule may succeed many times through backtracking.
Creating Your Own Patterns
You'll need two more Pyke modules to create your own patterns and contexts:
>>> from pyke import pattern, contexts
There are four kinds of patterns:
- pattern.pattern_literal(data)
- This matches the data provided.
- pattern.pattern_tuple((elements), rest_var = None)
- This matches a tuple. Elements must each be a pattern and must match the first n elements of the tuple. Rest_var must be a variable (or anonymous). It will match the rest of the tuple and is always bound to a (possibly empty) tuple.
- contexts.variable(name)
- This will match anything the first time it is encountered and becomes bound to that value. After that, it only matches this bound value each additional time it is encountered. Calling the constructor twice with the same name produces the same variable and must match the same value in all of the places that it is used.
- contexts.anonymous(name)
- This will match anything each time it is encountered. Calling the constructor many times with the same name is not a problem. The name must start with an underscore.
Finally, to create a pattern context, you need:
contexts.simple_context()
You'll need to save this context to lookup your variable values after each proof is yielded. This is done by either:
some_context.lookup_data(variable_name)some_variable.as_data(some_context)