This module aims to embed a fully functional CLIPS engine in Python, and to give to the developer a more Python-compliant interface to CLIPS without cutting down on functionalities. In fact CLIPS is compiled into the module in its entirety, and most API functions are bound to Python methods. However the direct bindings to the CLIPS library (implemented as the _clips submodule) are not described here: each function is described by an appropriate documentation string, and accessible by means of the help() function or through the pydoc tool. Each direct binding maps to an API provided function. For a detailed reference1.1 for these functions see Clips Reference Guide Vol. II: Advanced Programming Guide, available for download at the CLIPS website.
PyCLIPS is also capable of generating CLIPS text and binary files: this allows the user to interact with sessions of the CLIPS system itself.
An important thing to know, is that PyCLIPS implements CLIPS as a separated1.2 engine: in the CLIPS module implementation, CLIPS ``lives'' in its own memory space, allocates its own objects. The module only provides a way to send information and commands to this engine and to retrieve results from it.
PyCLIPS is organized in a package providing several classes and top-level functions. Also, the module provides some objects that are already instanced and give access to some of the CLIPS internal functions and structures, including debug status and engine I/O.
CLIPS is accessible through these classes and functions, that send appropriate commands to the underlying engine and retrieve the available information. Many of the CLIPS classes, constructs and objects are shadowed by Python classes and objects. However, whereas PyCLIPS classes provide a comfortable way to create objects that reference the actual engine objects, there is no one-to-one mapping between the two memory spaces: for instance, when a Python object is deleted (via the del command), the corresponding CLIPS object will still remain alive in the CLIPS memory space. An appropriate command is necessary to remove the object from the underlying engine, and this is provided by the module interface.
The PyCLIPS package can also be used interactively, since it can inspect an underlying CLIPS session and give some of the output that CLIPS usually provides when used as an interactive shell.
A simple interactive session with PyCLIPS follows:
>>> import clips >>> clips.Reset() >>> clips.Assert("(duck)") <Fact 'f-1': fact object at 0x00DE4AE0> >>> clips.BuildRule("duck-rule", "(duck)", "(assert (quack))", "the Duck Rule") <Rule 'duck-rule': defrule object at 0x00DA7E00> >>> clips.PrintRules() MAIN: duck-rule >>> clips.PrintAgenda() MAIN: 0 duck-rule: f-1 For a total of 1 activation. >>> clips.PrintFacts() f-0 (initial-fact) f-1 (duck) For a total of 2 facts. >>> clips.Run() >>> clips.PrintFacts() f-0 (initial-fact) f-1 (duck) f-2 (quack) For a total of 3 facts.
Users of the CLIPS interactive shell will find the PyCLIPS output quite familiar. In fact the Print<object>() functions are provided for interactive use, and retrieve their output directly from the underlying CLIPS engine I/O subsystem, in order to resemble an interactive CLIPS session. Other functions are present to retrieve object names, values and the so called pretty-print forms for programmatic use.
This section describes the guidelines and considerations that lead to this CLIPS interface implementation. For the developers which normally use CLIPS as a development environment or expert systems shell, the architecture of the PyCLIPS module will look a little bit different, and in some ways could also seem confusing.
The main topics covered by these sections are the Implementation of Constructs as Classes, the Implementation of CLIPS I/O Subsystem, the Configuration and Debug Objects, the Coexistence of Global and Environment-Aware Engines and the Conventions used for Naming which explains the rules that helped choose the current naming scheme for classes, functions and objects implemented in PyCLIPS.
CLIPS users know that this shell offers several constructs to populate the system memory. These constructs are not described here, since a detailed explaination of the CLIPS language can be found in the official CLIPS documentation. These constructs, many of which have their particular syntax, create ``objects'' (not necessarily in the sense of OOP1.3, although some of these can be Instances of Classes) in the subsystem memory.
The choice of implementing most of these constructs as classes gives to PyCLIPS a more organic structure. Most of the construct classes share similarities which make the interface structure simpler and the access to CLIPS objects more systematic.
Most constructs are implemented as factory functions which return instances of Python classes. These Python instances (that shadow the corresponding CLIPS objects), on their turn, have methods and properties which operate directly on the objects they map in the CLIPS subsystem. Methods and properties provide both access and send messages to these objects.
An example of this follows:
>>> import clips >>> f0 = clips.Assert("(duck)") >>> print f0 f-0 >>> print f0.Exists() True >>> f0.Retract() >>> print f0.Exists() False
In the above example, a fact (
(duck)) is asserted and then
retracted. The assertion is done by means of a module-level function
(Assert()) and the fact is retracted using a method of the
shadow object (f0). A verification on the CLIPS
using the Python Fact instance1.4 f0's method
Exists(), shows that after invoking the Retract()
method of f0 the
fact object does no longer exists in the
CLIPS subsystem, thus it has been actually retracted.
As stated previously, this does not remove the Python object
(a Fact instance) from the namespace pertinent to Python itself:
as it can be seen from the code snip shown above,
f0 is still a
functional instance, and can be queried about the existence of the
corresponding object in CLIPS.
Objects in the CLIPS operating space can be referenced by more than a Python object (or even not be referenced at all, if CLIPS creation does not correspond to a Python assignment), as demonstrated by the following code:
>>> clips.Reset() >>> f1 = clips.Assert("(duck)") >>> clips.Assert("(quack)") <Fact 'f-2': fact object at 0x00DE8420> >>> f1 <Fact 'f-1': fact object at 0x00DE3020> >>> fl = clips.FactList() >>> f1b = fl >>> f1b <Fact 'f-1': fact object at 0x00E08C40>
Both f1 and f1b refer to the same object in CLIPS namespace,
but their address is different: in fact they are two different Python
objects (equality test fails) but correspond to the same
The CLIPS shell interacts with the user answering to typed in commands with some informational output. An interactive CLIPS session will show this:
CLIPS> (reset) CLIPS> (watch activations) CLIPS> (defrule duck-rule "the Duck Rule" (duck) => (assert (quack))) CLIPS> (assert (duck)) ==> Activation 0 duck-rule: f-1 <Fact-1> CLIPS> (run) CLIPS> (facts) f-0 (initial-fact) f-1 (duck) f-2 (quack) For a total of 3 facts.
Each time a
fact is asserted, CLIPS outputs a string containing
its index, and since we decided to show some debug output about
activations, CLIPS produces a line as soon as
duck is asserted,
duck-rule would be activated by this. Although in an
interactive session all of the output would go to the terminal, CLIPS
logically considers the ``streams'' for different output types as
separated: in fact, debug output (the one generated by the
command) goes to a special stream called
wtrace. In this special
case, for instance, the debug output can be captured by PyCLIPS
through a special stream-like Python object, which provides a
Read() function1.5. Comparing the behaviour of two interactive
sessions, the former in the CLIPS subsystem and the latter in Python,
will help to understand the close relationship between CLIPS I/O and
PyCLIPS stream objects. CLIPS will interact with the user as
CLIPS> (defrule sayhello (hello) => (printout t "hello, world!" crlf)) CLIPS> (assert (hello)) ==> Activation 0 sayhello: f-1 <Fact-1> CLIPS> (run) hello, world!
And the Python counterpart follows:
>>> import clips >>> clips.DebugConfig.ActivationsWatched = True >>> r0 = clips.BuildRule("sayhello", "(hello)", '(printout stdout "hello, world!" crlf)') >>> print r0.PPForm() (defrule MAIN::sayhello (hello) => (printout stdout "hello, world!" crlf)) >>> clips.Assert("(hello)") <Fact 'f-0': fact object at 0x00DE81C0> >>> t = clips.TraceStream.Read() >>> print t ==> Activation 0 sayhello: f-0 >>> clips.Run() >>> t = clips.StdoutStream.Read() >>> print t hello, world!
The I/O access objects can be used both in interactive and unattended sessions. In this case, they can be useful to retrieve periodical information about CLIPS internal status, since most of the output provided can be easily interpreted in a programmatic way. Also, there is one more stream called StdinStream (which has a Write() method) that might be useful to send input to the CLIPS engine when some ``user interaction'' is required1.6.
There is no way to create other instances of these streams: the high-level module hides the class used to build these objects. This is because the I/O streams have to be considered like ``physical devices'' whose use is reserved to the engine to report trace and debug information as well as user requested output.
These I/O streams will be described later in the detail, since each one can be used to report about a specific task.
Note: The streams have to be explicitly read: there is no way to receive a notification from CLIPS that some output has been written. In other words, the PyCLIPS engine is not event driven in its interaction with Python.
As well as I/O streams, there are two other objects directly provided by PyCLIPS. These objects provide access to the CLIPS engine global configuration. Many aspects of the CLIPS engine, that in the command line environment would be configured using particular commands, are accessible via the EngineConfig (global engine configuration) object and the DebugConfig (global debug and trace configuration) object. For example, we can take the code snip shown above:
>>> clips.DebugConfig.ActivationsWatched = True
>>> t = clips.TraceStream.Read() >>> print t ==> Activation 0 sayhello: f-0
clips.DebugConfig.ActivationsWatched = True line tells to the
underlying subsystem that debug information about rule activations
has to be written to the proper stream (the stream dedicated to debug
output in CLIPS is called
wtrace and is accessible in PyCLIPS
through the TraceStream object).
As it has been said for the I/O streams, these objects cannot be instanced by the user: access to these objects affects global (or at least environmental, we will see the difference later) configuration, so it would be of no meaning for the user to create more, possibly confusing, instances of such objects.
As of version 6.20, CLIPS API offers the possibility to have several environments in which to operate. We can consider environments as separate engines that only share the operating mode, in other words ``the code''. PyCLIPS also implements environments by means of a special Environment class. This class implements all the features provided by the top level methods and classes. The Environment class reimplements all classes provided by PyCLIPS, but - although their behaviour is quite similar - methods of classes provided by Environment only affect the CLIPS environment represented by the Environment instance itself.
There is normally no need to use environments. However, access to them is provided for CLIPS ``gurus'' who want to have more than one engine working at the same time. The end user of PyCLIPS will see no real difference between a call to a function and its environmental counterpart (defined as companion function in the official CLIPS documentation), apart from being called as a member function of an Environment object.
A simple example will be explanatory:
>>> clips.Clear() >>> clips.Reset() >>> e0 = clips.Environment() >>> e1 = clips.Environment() >>> e0.Assert("(duck)") <Fact 'f-0': fact object at 0x00E7D960> >>> e1.Assert("(quack)") <Fact 'f-0': fact object at 0x00E82220> >>> e0.PrintFacts() f-0 (duck) For a total of 1 fact. >>> e1.PrintFacts() f-0 (quack) For a total of 1 fact.
PyCLIPS gives the ability to users to call Python code from within
the CLIPS subsystem. Virtually every function defined in Python can be
called from CLIPS code using the special CLIPS function
However, since CLIPS has different basic types than Python, in most cases
it would be useful for modules that implement function to be called in
the CLIPS engine to import the PyCLIPS module themselves, in order to
be aware of the structures that CLIPS uses.
Functions have to be registered in PyCLIPS in order to be available to the underlying engine, and the registration process can dynamically occur at any moment.
A simple example follows:
>>> import clips >>> def py_square(x): return x * x >>> clips.RegisterPythonFunction(py_square) >>> print clips.Eval("(python-call py_square 7)") 49 >>> print clips.Eval("(python-call py_square 0.7)") 0.49
A more detailed description of the features provided by
can be found in the appendices.
In PyCLIPS, the simple convention that is used is that all valuable content exposed has a name beginning with a capital letter. Names beginning with a single underscore have normally no meaning for the PyCLIPS user. Functions, class names and objects use mixed capitals (as in Java), and manifest constants (names used in lieu of explicit values to pass instructions to CLIPS functions or properties) are all capitalized, as is usual for the C language.
CLIPS users will perhaps be confused because often the constructs in
CLIPS are expressed by keywords containing a
def prefix. The
choice was made in PyCLIPS to drop this prefix in many cases: the use
of this prefix has a strong logic in the CLIPS language, because in this
way the developer knows that a construct is used, that is, a
definition is made. The keyword used to instance this definition,
both encapsulates the meaning of
definition itself, and also
the type of construct that is being defined (e.g.
defrule), thus avoiding making constructs more
difficult by means of two separate keywords. In PyCLIPS, since the
definition happens at class declaration and the instantiation of classes
shadows a construct definition when it has already been performed, it
seemed unnecessary to keep the prefix: in fact, to follow the above
example, it does not seem correct to refer to a rule within the CLIPS
subsystem as a ``Defrule'' object, hence it is simply referred
to as a Rule.
Python objects cannot be pickled or unpickled. This is because, since pickling an object would save a reference to a CLIPS entity - which is useless across different PyCLIPS sessions - the unpickling process would feed the underlying engine in an unpredictable way, or at least would reference memory locations corresponding to previous CLIPS entities without the engine having them allocated.
One better way to achieve a similar goal is to use the Save() or BSave() (and related Load() or BLoad()) to save the engine1.7 status in its entirety.
If a single entity is needed, its pretty-print form can be used in most cases to recreate it using the Build() functions.
It is also interesting that, by using some particular functions and the provided I/O subsystem, even ``pure'' CLIPS programs can be executed by PyCLIPS, and while the simple output from CLIPS can be read to obtain feedback, the possibility of inspecting the internal CLIPS subsystem state remains.
The following example, taken from the CLIPS website1.8, illustrates this: first we take a full CLIPS program, saved as zebra.clp, and reported below:
;;;====================================================== ;;; Who Drinks Water? And Who owns the Zebra? ;;; ;;; Another puzzle problem in which there are five ;;; houses, each of a different color, inhabited by ;;; men of different nationalities, with different ;;; pets, drinks, and cigarettes. Given the initial ;;; set of conditions, it must be determined which ;;; attributes are assigned to each man. ;;; ;;; CLIPS Version 6.0 Example ;;; ;;; To execute, merely load, reset and run. ;;;====================================================== (deftemplate avh (field a) (field v) (field h)) (defrule find-solution ; The Englishman lives in the red house. (avh (a nationality) (v englishman) (h ?n1)) (avh (a color) (v red) (h ?c1&?n1)) ; The Spaniard owns the dog. (avh (a nationality) (v spaniard) (h ?n2& ?n1)) (avh (a pet) (v dog) (h ?p1&?n2)) ; The ivory house is immediately to the left of the green house, ; where the coffee drinker lives. (avh (a color) (v ivory) (h ?c2& ?c1)) (avh (a color) (v green) (h ?c3& ?c2& ?c1&=(+ ?c2 1))) (avh (a drink) (v coffee) (h ?d1&?c3)) ; The milk drinker lives in the middle house. (avh (a drink) (v milk) (h ?d2& ?d1&3)) ; The man who smokes Old Golds also keeps snails. (avh (a smokes) (v old-golds) (h ?s1)) (avh (a pet) (v snails) (h ?p2& ?p1&?s1)) ; The Ukrainian drinks tea. (avh (a nationality) (v ukrainian) (h ?n3& ?n2& ?n1)) (avh (a drink) (v tea) (h ?d3& ?d2& ?d1&?n3)) ; The Norwegian resides in the first house on the left. (avh (a nationality) (v norwegian) (h ?n4& ?n3& ?n2& ?n1&1)) ; Chesterfields smoker lives next door to the fox owner. (avh (a smokes) (v chesterfields) (h ?s2& ?s1)) (avh (a pet) (v fox) (h ?p3& ?p2& ?p1&:(or (= ?s2 (- ?p3 1)) (= ?s2 (+ ?p3 1))))) ; The Lucky Strike smoker drinks orange juice. (avh (a smokes) (v lucky-strikes) (h ?s3& ?s2& ?s1)) (avh (a drink) (v orange-juice) (h ?d4& ?d3& ?d2& ?d1&?s3)) ; The Japanese smokes Parliaments (avh (a nationality) (v japanese) (h ?n5& ?n4& ?n3& ?n2& ?n1)) (avh (a smokes) (v parliaments) (h ?s4& ?s3& ?s2& ?s1&?n5)) ; The horse owner lives next to the Kools smoker, ; whose house is yellow. (avh (a pet) (v horse) (h ?p4& ?p3& ?p2& ?p1)) (avh (a smokes) (v kools) (h ?s5& ?s4& ?s3& ?s2& ?s1&:(or (= ?p4 (- ?s5 1)) (= ?p4 (+ ?s5 1))))) (avh (a color) (v yellow) (h ?c4& ?c3& ?c2& ?c1&?s5)) ; The Norwegian lives next to the blue house. (avh (a color) (v blue) (h ?c5& ?c4& ?c3& ?c2& ?c1&:(or (= ?c5 (- ?n4 1)) (= ?c5 (+ ?n4 1))))) ; Who drinks water? And Who owns the zebra? (avh (a drink) (v water) (h ?d5& ?d4& ?d3& ?d2& ?d1)) (avh (a pet) (v zebra) (h ?p5& ?p4& ?p3& ?p2& ?p1)) => (assert (solution nationality englishman ?n1) (solution color red ?c1) (solution nationality spaniard ?n2) (solution pet dog ?p1) (solution color ivory ?c2) (solution color green ?c3) (solution drink coffee ?d1) (solution drink milk ?d2) (solution smokes old-golds ?s1) (solution pet snails ?p2) (solution nationality ukrainian ?n3) (solution drink tea ?d3) (solution nationality norwegian ?n4) (solution smokes chesterfields ?s2) (solution pet fox ?p3) (solution smokes lucky-strikes ?s3) (solution drink orange-juice ?d4) (solution nationality japanese ?n5) (solution smokes parliaments ?s4) (solution pet horse ?p4) (solution smokes kools ?s5) (solution color yellow ?c4) (solution color blue ?c5) (solution drink water ?d5) (solution pet zebra ?p5)) ) (defrule print-solution ?f1 <- (solution nationality ?n1 1) ?f2 <- (solution color ?c1 1) ?f3 <- (solution pet ?p1 1) ?f4 <- (solution drink ?d1 1) ?f5 <- (solution smokes ?s1 1) ?f6 <- (solution nationality ?n2 2) ?f7 <- (solution color ?c2 2) ?f8 <- (solution pet ?p2 2) ?f9 <- (solution drink ?d2 2) ?f10 <- (solution smokes ?s2 2) ?f11 <- (solution nationality ?n3 3) ?f12 <- (solution color ?c3 3) ?f13 <- (solution pet ?p3 3) ?f14 <- (solution drink ?d3 3) ?f15 <- (solution smokes ?s3 3) ?f16 <- (solution nationality ?n4 4) ?f17 <- (solution color ?c4 4) ?f18 <- (solution pet ?p4 4) ?f19 <- (solution drink ?d4 4) ?f20 <- (solution smokes ?s4 4) ?f21 <- (solution nationality ?n5 5) ?f22 <- (solution color ?c5 5) ?f23 <- (solution pet ?p5 5) ?f24 <- (solution drink ?d5 5) ?f25 <- (solution smokes ?s5 5) => (retract ?f1 ?f2 ?f3 ?f4 ?f5 ?f6 ?f7 ?f8 ?f9 ?f10 ?f11 ?f12 ?f13 ?f14 ?f15 ?f16 ?f17 ?f18 ?f19 ?f20 ?f21 ?f22 ?f23 ?f24 ?f25) (format t "HOUSE | %-11s | %-6s | %-6s | %-12s | %-13s%n" Nationality Color Pet Drink Smokes) (format t "---------------------------------------------------------------%n") (format t " 1 | %-11s | %-6s | %-6s | %-12s | %-13s%n" ?n1 ?c1 ?p1 ?d1 ?s1) (format t " 2 | %-11s | %-6s | %-6s | %-12s | %-13s%n" ?n2 ?c2 ?p2 ?d2 ?s2) (format t " 3 | %-11s | %-6s | %-6s | %-12s | %-13s%n" ?n3 ?c3 ?p3 ?d3 ?s3) (format t " 4 | %-11s | %-6s | %-6s | %-12s | %-13s%n" ?n4 ?c4 ?p4 ?d4 ?s4) (format t " 5 | %-11s | %-6s | %-6s | %-12s | %-13s%n" ?n5 ?c5 ?p5 ?d5 ?s5) (printout t crlf crlf)) (defrule startup => (printout t "There are five houses, each of a different color, inhabited by men of" crlf "different nationalities, with different pets, drinks, and cigarettes." crlf crlf "The Englishman lives in the red house. The Spaniard owns the dog." crlf "The ivory house is immediately to the left of the green house, where" crlf "the coffee drinker lives. The milk drinker lives in the middle house." crlf "The man who smokes Old Golds also keeps snails. The Ukrainian drinks" crlf "tea. The Norwegian resides in the first house on the left. The" crlf) (printout t "Chesterfields smoker lives next door to the fox owner. The Lucky" crlf "Strike smoker drinks orange juice. The Japanese smokes Parliaments." crlf "The horse owner lives next to the Kools smoker, whose house is yellow." crlf "The Norwegian lives next to the blue house." t crlf "Now, who drinks water? And who owns the zebra?" crlf crlf) (assert (value color red) (value color green) (value color ivory) (value color yellow) (value color blue) (value nationality englishman) (value nationality spaniard) (value nationality ukrainian) (value nationality norwegian) (value nationality japanese) (value pet dog) (value pet snails) (value pet fox) (value pet horse) (value pet zebra) (value drink water) (value drink coffee) (value drink milk) (value drink orange-juice) (value drink tea) (value smokes old-golds) (value smokes kools) (value smokes chesterfields) (value smokes lucky-strikes) (value smokes parliaments)) ) (defrule generate-combinations ?f <- (value ?s ?e) => (retract ?f) (assert (avh (a ?s) (v ?e) (h 1)) (avh (a ?s) (v ?e) (h 2)) (avh (a ?s) (v ?e) (h 3)) (avh (a ?s) (v ?e) (h 4)) (avh (a ?s) (v ?e) (h 5))))
then we execute all commands (using the BatchStar() function) in the current Environment of an interactive PyCLIPS session:
>>> clips.BatchStar("zebra.clp") >>> clips.Reset() >>> clips.Run() >>> s = clips.StdoutStream.Read() >>> print s There are five houses, each of a different color, inhabited by men of different nationalities, with different pets, drinks, and cigarettes. The Englishman lives in the red house. The Spaniard owns the dog. The ivory house is immediately to the left of the green house, where the coffee drinker lives. The milk drinker lives in the middle house. The man who smokes Old Golds also keeps snails. The Ukrainian drinks tea. The Norwegian resides in the first house on the left. The Chesterfields smoker lives next door to the fox owner. The Lucky Strike smoker drinks orange juice. The Japanese smokes Parliaments. The horse owner lives next to the Kools smoker, whose house is yellow. The Norwegian lives next to the blue house. Now, who drinks water? And who owns the zebra? HOUSE | Nationality | Color | Pet | Drink | Smokes -------------------------------------------------------------------- 1 | norwegian | yellow | fox | water | kools 2 | ukrainian | blue | horse | tea | chesterfields 3 | englishman | red | snails | milk | old-golds 4 | spaniard | ivory | dog | orange-juice | lucky-strikes 5 | japanese | green | zebra | coffee | parliaments >>> clips.PrintFacts() f-0 (initial-fact) f-26 (avh (a smokes) (v parliaments) (h 1)) f-27 (avh (a smokes) (v parliaments) (h 2)) f-28 (avh (a smokes) (v parliaments) (h 3))
[... a long list of facts ...]
f-150 (avh (a color) (v red) (h 5)) For a total of 126 facts. >>> li = clips.FactList() >>> for x in li: ... if str(x) == 'f-52': ... f52 = x >>> f52 <Fact 'f-52': fact object at 0x00E6AA10> >>> print f52.PPForm() f-52 (avh (a drink) (v tea) (h 2))
You can just copy the program above to a file, say zebra.clp as
in the example, and follow the same steps to experiment with PyCLIPS
objects and with the CLIPS subsystem.
imported the first time.