This describes how to call Pyke from within your Python program.
There are two steps to initializing a Pyke knowledge engine:
- knowledge_engine.engine(*paths, **kws)
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('doc.examples')
Pyke requires that all Pyke source files be placed (either directly or indirectly) under a Python package directory. These packages must be on Python's search path for modules, and the directory for each dotted name must include an __init__.py file.
Each paths argument represents a different Python package directory to search for Pyke source files. These source packages may be passed either as package names (as a string in Python's standard dotted notation), as the packages themselves, or as any module within the package. Thus, the example above could also be written:
>>> from doc import examples >>> my_engine = knowledge_engine.engine(examples)
>>> from doc.examples import some_module >>> my_engine = knowledge_engine.engine(some_module)
Pyke recursively walks each source package 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 the keyword arguments, see below). This causes all of the rule bases to be loaded and made ready to activate.
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.
It is OK for your source package directory to be part of a zipped Python egg file. But, in this case, Pyke can't rebuild and recompile the source files. It will just import the compiled file that is there without complaining if the source file has a later modification time than the compiled file.
All of the Python source files and pickle files generated from each source package are placed, by default, in a compiled_krb package directly subordinate to that source package. You may specify a different destination package for any source package by passing that source package along with the destination package name as a 2-tuple. The destination package name may be specified as an absolute Python module path; or as a relative path (with leading dots), relative to the source package. Thus, specifying the default destination explicitly would look like:
>>> my_engine = knowledge_engine.engine(('doc.examples', '.compiled_krb'))
You may specify the same destination package for multiple source packages.
The last component of the destination package will be created automatically if it does not already exist.
You probably want to add compiled_krb (or whatever you've chosen to call it) to your source code repository's list of files to ignore.
There are four optional keyword arguments that you may also pass to the engine constructor:
- load_fc -- load forward-chaining rules
- load_bc -- load backward-chaining rules
- load_fb -- load fact bases and
- load_qb -- load question bases
These parameters must be passed as keyword parameters. They all default to True.
- some_engine.add_universal_fact(kb_name, fact_name, arguments)
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
Three functions initialize each case:
- 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 case specific facts are used. The only difference between the two types of facts is that the case 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')
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.
Your Pyke engine is now ready to prove goals for this case!
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)
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): ... pyke.knowledge_engine.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. The plans are constructed out of functools.partial functions, which had to be registered with copyreg before you could pickle them in older Python releases. But in Python3 it is no longer necessary to register functools.partial to be able to pickle them.
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).
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)
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('doc.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>", line 2, in <module> my_engine.prove_1('error_test', 'goal', (), 0) File "...knowledge_engine.py", line 288, in prove_1 return next(iter(it)) File "...knowledge_engine.py", line 272, in gen for plan in it: File "...rule_base.py", line 46, in __next__ return next(self.iterator) 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'
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.
- 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.
- 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:
- 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.
- 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.
- 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:
You'll need to save this context to lookup your variable values after each proof is yielded. This is done by either: