This document is written for the beginning Loom user. It is intended to provide a hands-on introduction to the basic features in Loom and how they can be used to build systems. This document assumes a reader familiar with the CommonLisp language and environment, and having an awareness of the basic notions of knowledge representation and knowledge inference. The exercises given in this tutorial will require an installed and runnable Loom system.
For those familiar with the description logic branch of the knowledge representation research community, the concepts and features of Loom are old friends: Loom represents the next rung in an evolutionary ladder of knowledge representation system development. For others, Loom can initially present a formidable array of features. Even experienced knowledge system developers may at first have difficulty understanding just what the features of Loom provide and how to organize solutions to application problems using Loom.
This tutorial is intended to help the beginning Loom user overcome the initial problems of understanding what Loom features are available, how they can be applied to structure solutions to problems, and how they can be manipulated in the Loom programming environment. The examples given in this tutorial have been designed to demonstrate specific features in Loom, as clearly and as simply as possible. These basic constructs and operations will enable the user to build realistically complex application knowledge bases.
The Loom tutorial has been developed to guide an exploration of Loom's features and capabilities. It is intended that the user participate in this activity by actually typing and executing the examples provided. While there is some tedium in typing examples, much of the familiarity and expertise in Loom's use will directly derive from this participation. Besides the basic benefit of "learning by doing," this approach leads the user to face the problems that occur when typos and similar errors are introduced. This tutorial offers opportunities for the user to customize and extend the basic lessons, and poses problems for the user to "flesh out" more fully the example problem's development. These suggested exercises are denoted by the use of a (bullet).
Should the user reach an impasse or wish to expedite the input process, (s)he may refer to the file "tutorial.lisp", which contains all the input for the examples and solutions for the exercises in this tutorial.
Start Loom appropriately for your system environment.
There are four steps necessary to cleanly create and start working in a new knowledge base. The first four steps look like this: (Note that your typed input is shown in boldface, while the system-generated type is shown in standard face).
> (make-package "CRISIS-PLANNER") #<Package "CRISIS-PLANNER" 405DB196> > (in-package "CRISIS-PLANNER") #<Package "CRISIS-PLANNER" 405DB196> > (loom:use-loom "CRISIS-PLANNER") |TH|CRISIS-PLANNER-THEORY ;;; Substitute your favorite path name in the next call > (defkb "CRISIS-PLANNER-KB" nil :pathname "~/Loom/crisis-saved-kb") |K|LOOM::CRISIS-PLANNER-KBWith the first command, we built a new package in the CommonLisp environment for this system. Remember that strings in CommonLisp are case-sensitive. Packages provide a mechanism for defining lexical context in LISP; further discussion is available in any of the CommonLisp manuals. Having defined this new package, we set the current package to it and then we called the use-loom function. This accomplished two things. First, all symbols needed to interact with Loom (e.g. function names, data structure variables, etc.) were imported into the package with the name specified. Second, use-loom created a new Loom context (theory) and knowledge base; later additional knowledge bases may be defined within this package. Finally, we call defkb to provide additional information about the knowledge base we just defined. defkb has two required arguments, the name of the knowledge base and the parents of the knowledge base (in this particular case, the crisis-planner knowledge base has no parents). The keyword argument :pathname specifies a path name for use in saving and restoring the new knowledge base. This parameter should be set to the path name you wish the knowledge base to be stored in; the path name in the example is supplied for illustrative purposes only. Information in Loom is organized into hierarchical "contexts". Knowledge bases are used to add additional information and structure to the contexts.
The next step is simply to tell Loom which knowledge base should be the "current" knowledge base. This also makes the context "CRISIS-PLANNER-THEORY" the current context:
> (change-kb "CRISIS-PLANNER-KB") |K|LOOM::CRISIS-PLANNER-KBSince the goal of this tutorial is to demonstrate the widest extent of Loom's reasoning, we need to use the most complete. That means selecting "classified" instances instead of Loom's default of "lite" instances. The following function call establishes the default creation type for the instances that will be used in this tutorial. (Lite instances do not have as much inference done, but they have a smaller storage footprint and should be quicker for most operations than classified instances).
> (creation-policy :classified-instance) :CLASSIFIED-INSTANCE
There are a few basic ways of interacting with Loom: functions that let you examine the Loom knowledge base, functions that let you tell things to the knowledge base, and functions that let you ask things about the knowledge base. Different examples of these functions are appropriate for talking to Loom about different types and complexities of knowledge base entries. This section will introduce a few of the simple and commonly used mechanisms. More complex examples of these interactions will be scattered throughout this tutorial as the example knowledge base grows more complex.
To begin, look at the current state of the knowledge base by examining the contents of its context:
> (list-context) NILNaturally, since we have a newly initialized knowledge base, it is empty. We can begin to inform the knowledge base about objects that exist in the world, using the create function:
> (create 'ob-1 nil) |I|OB-1What you have just done is to tell Loom that there is a thing in the world, called ob-1. The first argument to create is a symbol or a string specifying the name of an object. The second argument is the type of the object. Since we don't care about the type of ob-1, we specify nil. Loom labels symbols representing knowledge base entries with a prefix enclosed in vertical bars: |C| for concepts, |I| or |i| for instances, |R| for relations, and |K| for knowledge bases. Another form of inference that Loom does when you tell it about something is to match it to the things it already knows about. Since we haven't yet embodied this knowledge base with any models of typical objects, ob-1 is "classified" as an instance of Thing, the most general concept.
List the knowledge base. Note the current set of entries.
Now tell the knowledge base about some basic entities in our air strike planner domain. Let's begin by defining a military-installation as a basic object in our domain. Then, we can define an air base, a type of a military installation that has a name and at least one runway length (obviously, this concept is simplified for the example). In Loom, this means we want a concept that has exactly one name relation to some other object (an instance of a string concept that contains the name), and at least one runway-length relation to some object that describes that concept.
> (defconcept military-installation) .+ |C|MILITARY-INSTALLATION > (defconcept air-base :is-primitive (and military-installation (exactly 1 name) (at-least 1 runway-length))) |C|AIR-BASEFirst, notice the output after the first defconcept. This output is typical of that generated when Loom accepts new knowledge base specifications and attempts to modify the knowledge base in response. The symbols tell the informed Loom observer (which you will become after reading later chapters in this tutorial) when Loom is classifying concepts (.), compiling constraints (+), compiling access functions (!), merging concepts (M), unclassifying (-), and classifying instances (*). Don't worry if the number and order of the symbols on your system differs from that in this tutorial.
Now, if you list the knowledge base, you will see some new entries. More about those in a minute. There is a mechanism for examining concepts, relations, and instances in detail. The pc ("print concept") macro, for example, lets you look at a concept. Its only argument is the concept object's name.
> (pc air-base) (defconcept Air-Base :is-primitive (:and Military-Installation (:exactly 1 Name) (:at-least 1 Runway-Length)) :annotations (Thing))Similarly, you can use pr for viewing relations, pi for looking at instances, pb for looking at actions or production rules (behaviors), and a more general macro, po, for looking at any type of Loom object. All of these macros except po will work with either an object or its name. PO must have an object.
Back to the knowledge base entries. You probably noticed that in addition to the concept air-base, two relations are also present. The existence of these relations was implied by their reference in the concept definition. If you examine these (and please do, using pr), you will see that they are system-defined placeholders. For now, take it on faith that these are not sufficient for the knowledge base to work. We must define these two relations. Name is intended to be a relation that relates any concept (by leaving the domain unspecified, it will default to the most general concept Thing) to a single string. Runway-length relates an airbase to one or more numbers (representing lengths of runways). In this case, the relation is meaningful only to concepts of type air-base.
> (defrelation name :range string) . |R|NAME > (defrelation runway-length :domain air-base :range number) ..+++ |R|RUNWAY-LENGTHUse pr to view these objects.
Now we can create some instances of the air base concept we have defined. Tellm and its partner function tell will be explained in later chapters. For now, we will just create a few instances:
> (tellm (create ab-1 air-base) (name ab-1 "Atlanta NAS") (runway-length ab-1 12050))) !!!!!****** Recognition changes at agent time 1: entry: AB-1 |C|AIR-BASE entry: AB-1 |C|MILITARY-INSTALLATION 2You have just created an instance labeled ab-1 of type air-base and given ab-1 a name and a runway-length. Loom is able to infer that ab-1 is a military-installation as well as an air-base, since by definition an air-base is a military-installation. Tellm returns 2, the number of the current knowledge base state. Note that since ab-1 was created to be of type air-base, it will not be possible to retract this fact. The type used in the create function is treated differently by Loom than a type used in an assertion. The latter types can be retracted, while the creation type cannot.
Try examining this instance with the pi function.
> (tellm (create ab-2 air-base) (name ab-2 "Mainland NAS") (runway-length ab-2 9000)) Recognition changes at agent time 2: entry: AB-2 |C|MILITARY-INSTALLATION entry: AB-2 |C|AIR-BASE 3 > (list-context) (|I|OB-1 |I|AB-2 |I|AB-1 |C|AIR-BASE |C|MILITARY-INSTALLATION |R|NAME |R|RUNWAY-LENGTH)Now, let's tell Loom about some new runway lengths, and then print the instance. Note that we can use tellm to specify new values for instance relations:
> (tellm (runway-length ab-2 9000)) 3 > (tellm (runway-length ab-2 10000)) 4 > (pi ab-2) (TELL (:ABOUT AB-2 AIR-BASE (RUNWAY-LENGTH 9000) (RUNWAY-LENGTH 10000) (NAME "Mainland NAS")))Note that Loom added the second runway-length, but not the first. This is because the the first runway-length is the same as an already existing runway length, so Loom assumes that you are merely repeating information that is already present in the knowledge base.
We can remove information from the knowledge base using forgetm. The syntax of forgetm is identical to tellm:
> (forgetm (runway-length ab-2 10000)) 5 > (pi ab-2) (TELL (:ABOUT AB-2 AIR-BASE (RUNWAY-LENGTH 9000) (NAME "Mainland NAS")))We can completely remove an instance from the knowledge base by using the forget-all-about function. This function requires either the instance object, or its identifier.
> (forget-all-about 'ab-2) TList the knowledge base to verify the deletion.
Now, let's re-tell Loom about ab-2.
> (tellm (create ab-2 air-base) (name ab-2 "Mainland AFB") (runway-length ab-2 9000)) !!!!*** Recognition changes at agent time 6: entry: AB-2 |C|MILITARY-INSTALLATION entry: AB-2 |C|AIR-BASE 7Finally, there is the problem of changing concepts and relations. The Loom knowledge base will respond to redefinitions of concepts and relations. For example, let's redefine an air-base and add a new relation.
> (defconcept air-base :is-primitive (and Military-Installation (exactly 1 name) (at-least 1 runway-length) (exactly 1 service-branch))) - |C|AIR-BASE > (defset us-service-branch :is (one-of 'army 'air-force 'navy 'marine)) . |C|US-SERVICE-BRANCH(ONE-OF 'ARMY 'AIR-FORCE 'NAVY 'MARINE) > (defrelation service-branch :domain air-base :range us-service-branch) . |R|SERVICE-BRANCHNotice the use of defset. defset is used to define a set of symbolic items. The items may be either ordered or unordered (in this particular case, they are unordered). Notice also that defset returns a concept; a set is a special kind of concept.
> (retrieve ?p (air-base ?p)) (|I|AB-1 |I|AB-2)This retrieves all instances of an air base.
> (retrieve ?p (and (air-base ?p) (for-some (?l) (and (runway-length ?p ?l) (> ?l 10000))))) (|I|AB-1)This case only returns air bases with at least one runway longer than 10000.
> (save-kb) |C|US-SERVICE-BRANCH(ONE-OF 'ARMY 'AIR-FORCE 'NAVY 'MARINE) |R|NAME |R|RUNWAY-LENGTH |R|SERVICE-BRANCH |C|AIR-BASE |C|MILITARY-INSTALLATION |I|OB-1 |I|AB-2 |I|AB-1 "~/Loom/crisis-saved-kb"The knowledge base saved is the current one, selected with the change-kb (or in-kb) expression. The default can be overridden by supplying a knowledge base name as an optional variable. The path name for the save file is generated from the name given earlier in the defkb expression. An alternate path name could have been specified at the time that the knowledge base was created, or it can be specified in the save-kb command as the value for the keyword argument :pathname.
Restoring the knowledge base is accomplished through the load-kb function, which requires the knowledge base name, and also offers the optional :pathname argument.
Earlier, in Chapter 1, we had an initial exposure to the defconcept and defrelation macros, the basic mechanisms by which knowledge structures are declared in Loom. We also caused an instance of an object to be created in the Loom knowledge base, and experimented a bit with ways to manipulate and view that object. This chapter of the Loom Tutorial will focus on a more complete picture of concepts and relations, and the ways that complex knowledge representation structures can be defined using these elements.
There are a number of variations in the way Loom knowledge base entries can be defined. Each variation allows the knowledge base developer to exploit specific features of Loom's inference capability. As definitions are added to the knowledge base, Loom performs a certain class of inference called classification. In classification, the definitions of concepts and relations are organized to form a domain-specific taxonomy of terms. This taxonomy is structured in accordance with the subsumption (superclass/subclass) relationships that exist between terms. When instances of objects are created in the knowledge base, they are classified into that taxonomy, either automatically or on request.
Since there are so many types of activity within Loom initiated by the definitions it is given, it is impossible to clearly separate examples of definition from examples of classification-based inference. For that reason, this chapter of the Loom Tutorial will explore both the declaration of objects in the modelling language and the related classification inferencing.
> (tell (create ab-3 air-base)) OK > (tell (runway-length ab-3 14000)) OK > (tellm (name ab-3 "Centrada AFB")) * Recognition changes at agent time 7: entry: AB-3 |C|MILITARY-INSTALLATION entry: AB-3 |C|AIR-BASE 8We could have done the same exercise using tellm for each statement. The only difference is that after each statement, the knowledge base would have been updated. In some circumstances, this causes Loom to make inferences that will change (or just be redundantly recomputed) as more information is given. Loom will always infer the "right things" based on its current information.
An alternative syntax would have embedded all these statements inside a single tellm statement. Try this yourself:
Create an instance called ab-4 with name "Nearby Shores NAS" and runway-length 9000.
Note how you were forced to redundantly type the instance name for each statement. This can be avoided by using the about construct inside your tell statements. This is illustrated in the next section.
> (defconcept small-air-base :is (and air-base (exactly 1 runway-length))) .+ |C|SMALL-AIR-BASENote the use of :is instead of :is-primitive as in our previous concept definitions. Since small-air-base is a "fully-characterizable" type of air-base, it is naturally conceptualized as a non-primitive concept. Primitive and non-primitive concepts will be discussed in more detail in a later section, but for now it is only important to realize that such a distinction exists. Now, let's tell the knowledge base about an instance of an object which, like a small air base, has one runway-length. We will use yet another variation of the tell construct, even though any of our previous methods would work here as well.
> (tellm (about ab-5 (runway-length 1100))) !* Recognition changes at agent time 9: entry: AB-5 |C|MILITARY-INSTALLATION entry: AB-5 |C|AIR-BASE * 10Notice that Loom has classified this instance as an air-base only, and has not classified the instance as a small-air-base, even though we have specified only one runway-length. Ask is a function that lets us pose queries to the knowledge base, in forms that in most ways resemble tell constructs. An ask will return either NIL, if the query is false or unknown, or T if the query is true. Let's use the ask function to inquire whether ab-5 is really a small-air-base:
> (ask (air-base ab-5)) T > (ask (small-air-base ab-5)) NILLoom has no way of knowing that there are not additional facts of which it has not been told. In short, Loom assumes open world semantics. For example, nothing we have stated so far precludes the possibility that ab-5 has more than one runway-length. Loom allows the user to define relations that assume closed world semantics. The inclusion of the :closed-world characteristic will affect only the objects to which it is added (relations or concepts); the rest of the knowledge base remains open world. The closed world assumption allows Loom to assume that what it has been told is complete with respect to that concept or relation.
Let's redefine the relations in this example to use closed-world semantics:
> (defrelation runway-length :domain air-base :range number :characteristics :closed-world) ---...++++ |R|RUNWAY-LENGTHUse the ask function as before to see if ab-5 can be classified as a small-air-base.
As you can see, Loom has been able to make a non-trivial inference to classify the instance as a small-air-base based on its roles, given the assumption of closed-world semantics.
The examples we will build later in this tutorial will draw heavily on this ability, and show how sophisticated and very useful classification based reasoning can be. In the meantime, let's continue to tease out some of the issues related to basic object definition and classification.
> (defconcept surveillance-plane :is (and airplane (at-least 1 surveillance-device))) |C|SURVEILLANCE-PLANE .+We have described a surveillance-plane in terms of the concept airplane that it specializes, and in terms of roles (similar to slots in other languages) whose existence distinguishes this concept from other concepts. We have specified that "a surveillance-plane is an airplane with at least one filler of the role surveillance-device." The key word in that phrase is the word is; this form of a concept definition is a fully-specified definition. We will explore a few variations that qualify that definition.
Our definition of a surveillance-plane is not quite complete. We have defined a simple concept, but we have yet to define how its roles can be satisfied. If we were to try to build an instance at this time, something like this would happen:
> (tellm (about a1)) >>Error: Attempt to seal the network when undefined concepts exist. For example, |C|AIRPLANE is undefined. Execute `(list-undefined-concepts)' to retrieve all undefined concepts. LOOM::WARN-OF-UNCLASSIFIED-CONCEPTS-DURING-SEALING: :C 0: Resume running LOOM :A 1: Abort to Lisp Top Level -> :aAs the error message suggests, we can ask the knowledge base about objects whose definitions are not complete:
> (list-undefined-concepts) (|C|AIRPLANE |R|SURVEILLANCE-DEVICE)Remember, what Loom does with concept and relation definitions is to build a domain-specific taxonomy that can be used to classify any instance. Note that Loom generated an error without regard to the statement in the tellm. We did not specify the new instance to be related in any way to the concepts we were recently defining; the error was simply attempting to update the state of the knowledge base while there were unclassified concepts. Unclassified concepts are those for which insufficient information has been provided to allow their inclusion in the taxonomy: the knowledge base is "unsealable" because a consistent taxonomy cannot be built from these unclassified concepts. We must, in this case, complete the definitions for airplane and surveillance-device before the concept surveillance-plane can be fully classified.
> (defconcept airplane) .. |C|AIRPLANE > (defrelation surveillance-device :range surveillance-hardware :characteristics :closed-world) .. |R|SURVEILLANCE-DEVICE > (defset surveillance-hardware :is (one-of 'radar 'infrared-spy-camera 'subsonic-message-unscrambler)) ..++++ |C|SURVEILLANCE-HARDWARE(ONE-OF 'RADAR 'INFRARED-SPY-CAMERA 'SUBSONIC-MESSAGE-UNSCRAMBLER) SURVEILLANCE-HARDWARECheck to verify that no unclassified concepts still exist.
A concept or relation is primitive if its definition is incompletely specified. By declaring a concept to be primitive, we are in effect telling Loom there are hidden attributes about objects of that type that we are not representing. The choice of which concepts to be declared primitive depends largely on the intended use of the represented concept and will vary across applications. For example, in an application concerned with the migratory patterns of birds, the concept bird may need no further definition and be considered primitive, while raptor may be defined as "a bird that is a predator". If the application is concerned instead with representing relationships and interactions within an ecosystem, it may be important to know that a bird is "an animal which has wings and reproduces by laying eggs" and the concepts animal, egg, and wing are primitive. Concepts composed of combinations and specializations of other concepts, such as bird in the latter case, are called defined concepts.
Let's see how changing the primitiveness of the concept affects Loom inferences. First, tell Loom about an instance which matches the surveillance-plane definition.
> (tellm (create a1 airplane) (surveillance-device a1 `radar)) !!!* Recognition changes at agent time 11: entry: A1 |C|AIRPLANE entry: A1 |C|SURVEILLANCE-PLANE *** 12As expected, Loom correctly classifies the instances as an surveillance-plane. Now, let's redefine surveillance-plane to be a primitive concept, and then tell Loom to update the state of the knowledge base.
> (defconcept surveillance-plane :is-primitive (and airplane (at-least 1 surveillance-device))) -.+ |C|SURVEILLANCE-PLANE > (tellm) !* Recognition changes at agent time 12: exit: A1 |C|SURVEILLANCE-PLANE * 13Loom has reclassified a1 as an airplane. Note that airplane is implicitly primitive, as are all "top-level" concepts. If we would like Loom to classify a1 as an surveillance-plane, we must directly assert this fact.
> (tellm (surveillance-plane a1)) * Recognition changes at agent time 13: entry: A1 |C|SURVEILLANCE-PLANE 14
In the previous chapter, we explored the basic mechanisms by which objects and relations are defined in Loom, how objects based on those definitions are instantiated, and how Loom performs classification-based reasoning to infer a mapping of those objects to a domain-specific taxonomy. This chapter will expand on those basic ideas by presenting a more complete view of Loom's definitional constructs and related inferential capabilities.
While Chapter 2 introduced the power of the basic Loom approach, Chapter 3 will expose some of the richness and breadth of knowledge representation possible. Much of this breadth comes from aspects of concept and relation definition not yet seen: the use of constraints, and a more complete set of definitional terms. We will also explore defined concepts and relations, where new knowledge base definitions are defined as compositions and specializations of existing definitions. Finally, we will start to look at the use of backward chaining inference to determine object classification. Sprinkled through this chapter will be discussions of the mechanisms for retrieval of information from the knowledge base.
In this tutorial, we will be building examples related to a crisis planning problem. Among the objectives of our tutorial is the construction of "realistic" examples of the way Loom knowledge representation features can be used to construct knowledge based solutions to real world problems. Against this objective is the desire to pose clear, simple examples that make Loom's features as easy as possible to understand. We provide examples based on a toy version of a crisis planning problem, designed to illustrate the scope of knowledge based representation that is possible in Loom. We have designed this toy problem around common sense knowledge, avoiding the problem of training the reader in the details of real world crisis planning.
Our toy domain is about planning transportation for an emergency relief effort. In general, our knowledge base will know about things like roads, airports, and cities. Air, ground, and sea transportation modes will be represented. Knowledge will be provided to develop an appropriate set of actions to transport construction and medical teams to appropriate locations in the face of damaged or otherwise limited transportation facilities.
Figure 1: The Crisis Island World
Of course, in this tutorial we will only address a narrow range of issues related to creating such a crisis planning knowledge base. The specific pieces of knowledge will be discussed as they arise in examples. Our goal is to show examples of how Loom's most important features work, both individually and in concert, to support the representation and manipulation of knowledge.
> (defconcept location) .+ |C|LOCATION > (defrelation latitude :domain location :range number) .+ |R|LATITUDE > (defrelation longitude :domain location :range number) .+ |R|LONGITUDE > (defrelation region :domain location :range string) .+ |R|REGIONNotice that we did not explicitly identify the above relations in location's concept definition. Instead, the relations are associated with location by virtue of the :domain field in the relation definitions. This is an example of indirect role definition.
Let's tell Loom about 3 different locations in Crisis Island.
> (tellm (create c1 location) (about c1 (latitude 23.2) (longitude 75.9) (region "Crisis Island"))) !!!!***** Recognition changes at agent time 15: entry: C1 |C|LOCATION 16 > (tellm (create c2 location) (about c2 (latitude 23.3) (longitude 75.6) (region "Crisis Island"))) * Recognition changes at agent time 16: entry: C2 |C|LOCATION 17 > (tellm (create c3 location) (about c3 (latitude 23.1) (longitude 75.3) (region "Crisis Island"))) * Recognition changes at agent time 17: entry: C3 |C|LOCATION 18Now, let's use location as a component of a more complex concept. City is a location that has additional roles that make it a unique new concept. This is accomplished by including the more general concept in the :is clause of the concept definition. For example:
> (defconcept city :is (and location (exactly 1 name) (exactly 1 population))) |C|CITYOf course, we still must define the relations that can fill these new roles.
> (defrelation name :range string :characteristics :closed-world) ---... |R|NAMEAnother aspect of roles is the cardinality of their fillers. Relations can be defined to restrict their filler to being single valued (the default is multi-valued). If a relation is known to be single valued, then asserting a new value for that relation will automatically cause the old value to be retracted. In Loom terminology, this behavior is referred to as "clipping". Let's use this in defining our population relation:
> (defrelation population :range integer :characteristics :single-valued) ..+++++++ |R|POPULATIONOf course, the interesting part of this exercise lies in manipulating the instances. We have created three instances of locations. Refer back to the "Crisis World" map, and find the names of the three cities. Let's fill in the name and population roles of c1, c2, and c3.
> (tellm (about c1 (name "Costa Via") (population 11000)) (about c2 (name "Centrada") (population 8000)) (about c3 (name "Otheria Beach") (population 4500))) !!!!!!* Recognition changes at agent time 18: entry: C3 |C|CITY * Recognition changes at agent time 18: entry: C2 |C|CITY * Recognition changes at agent time 18: entry: C1 |C|CITY ** 19Loom recognizes each instance qualifying to be classified as a city rather than just a location. Now, let's "flesh out" this type of knowledge about the crisis world by creating instances for all the cities described on the "Crisis World" map.
> (tellm (create c4 location) (about c4 (name "Nearby Shores") (population 17000) (latitude 20.9) (longitude 74.6) (region "Nearby Island"))) * Recognition changes at agent time 19: entry: C4 |C|LOCATION entry: C4 |C|CITY 20 > (tellm (create c5 location) (about c5 (name "Mainland AFB") (population 900) (latitude 23.0) (longitude 75.0) (region "Mainland"))) * Recognition changes at agent time 20: entry: C5 |C|LOCATION entry: C5 |C|CITY 21
First, let's retrieve all the knowledge base entries about cities:
> (retrieve ?c (city ?c)) (|I|C3 |I|C2 |I|C1 |I|C4 |I|C5)This retrieval asked for all instances of type city. The basic components of the retrieve statement are the pattern variables (the ?c) and the retrieval pattern. Such retrievals can be made more complex by using more than one pattern variable, or by combining multiple selection terms with the and construct. A more complex retrieval might ask for pairs of cities where one city's population is greater than 10,000, and the other's is between 5,000 and 10,000:
> (retrieve (?x ?y) (and (city ?x) (city ?y) (> (population ?x) 10000) (< (population ?y) 10000) (> (population ?y) 5000))) ((|I|C4 |I|C2) (|I|C1 |I|C2))Of course, the results will depend on the populations you assigned to your own instances. If you got NIL as your response, lower the 5000 number. Notice that the result is a list of answers. Each answer has the same pattern as the pattern variable specification (in this case, two objects). When the elements of each answer are substituted for their corresponding pattern variables in the retrieval pattern, the retrieval pattern is true. To say that another way, each answer satisfies the retrieval pattern.
Imagine that you were building an application on top of Loom. Just as you have queried Loom's knowledge base, your application program could issue similar LISP calls to retrieve to acquire this list of knowledge base entries.
One method of restriction is just to build the constraint into the :is clause that defines the concept. For example, let's redefine a city to be a location with a big population :
> (defconcept city :is (and location (exactly 1 name) (exactly 1 population) (>= population 5000))) -.+ |C|CITY > (tellm) !* Recognition changes at agent time 20: exit: C3 |C|CITY * Recognition changes at agent time 20: exit: C5 |C|CITY 21By running tellm without any arguments, we force the knowledge base to be updated. Now, ask about any of your instances:
> (ask (city c3)) NILAs you can see, our smaller cities lost their classification as city because they do not satisfy the constraint and revert to being merely a location. This potential problem can be avoided by creating a new concept for these instances:
> (defconcept town :is (and location (exactly 1 name) (exactly 1 population) (< population 5000))) .+ |C|TOWN > (tellm) !* Recognition changes at agent time 21: entry: C5 |C|TOWN * Recognition changes at agent time 21: entry: C3 |C|TOWN * 22Check to see if each instance is a city or a town.
To illustrate this idea, let's create two new concepts, called damaged and damage-level. Then, define the relation damage-estimate that links these concepts as a role owner / role filler.
> (defconcept damaged :is (exactly 1 damage-estimate)) |C|DAMAGED > (defconcept damage-level :is-primitive (one-of normal structural-damage injuries damage-and-injuries)) . |C|DAMAGE-LEVEL(ONE-OF NORMAL STRUCTURAL-DAMAGE INJURIES DAMAGE-AND-INJURIES) > (defrelation damage-estimate :domain damaged :range damage-level :characteristics :single-valued) ..+++ |R|DAMAGE-ESTIMATENotice something else new. We have used the construct one-of to define what is in a concept (we could have also used defset). In other words, we have defined a primitive concept that is described by enumerating its instances, not by specifying a set of restrictions. If we had wanted to express the damage level in terms low, medium, and high, we could have used the expression :the-ordered-set instead.
> (defconcept damaged-city :is (and city damaged)) .+ |C|DAMAGED-CITYThis example shows how a new concept can be defined by combining two concepts together. Let's say something about one of our city instances to cause it to be damaged:
> (tellm (damage-estimate c1 damage-and-injuries)) !!!!* Recognition changes at agent time 22: entry: NORMAL |C|DAMAGE-LEVEL(ONE-OF NORMAL STRUCTURAL-DAMAGE INJURIES DAMAGE-AND-INJURIES) * Recognition changes at agent time 22: entry: STRUCTURAL-DAMAGE |C|DAMAGE-LEVEL(ONE-OF NORMAL STRUCTURAL-DAMAGE INJURIES DAMAGE-AND-INJURIES) * Recognition changes at agent time 22: entry: INJURIES |C|DAMAGE-LEVEL(ONE-OF NORMAL STRUCTURAL-DAMAGE INJURIES DAMAGE-AND-INJURIES) * Recognition changes at agent time 22: entry: DAMAGE-AND-INJURIES |C|DAMAGE-LEVEL(ONE-OF NORMAL STRUCTURAL-DAMAGE INJURIES DAMAGE-AND-INJURIES) **** 23
> (defconcept city-needing-medical-help :is (and damaged-city (the damage-estimate (one-of injuries damage-and-injuries))))Experiment with the result of this definition. You should find that any city instance that you tellm about having an appropriate type of damage (e.g. injuries) will classify as a city-needing-medical-help. Note that c1 will not be classified as a city-needing-medical-help until you updated the knowledge base again, since this definition was added after the instance classifier has already run.
Try to build a retrieve that finds only large cities needing medical help.
> (defconcept priority-1-medical-need :is (and city-needing-medical-help (satisfies (?x) (> (population ?x) 10000))))We can test for an instance's classification as a priority-1-medical-need by using the ask or retrieve statements.
> (ask (priority-1-medical-need c1)) !* T
> (defconcept path) .+ |C|PATH > (defconcept transportation-path :is (and path (at-least 2 terminus) (exactly 1 name))) |C|TRANSPORTATION-PATH > (defrelation terminus :domain transportation-path :range location) ..++ |R|TERMINUSDefine your own concept of a medical-team. Use at-least and at-most to restrict the size and composition of doctors, nurses, etc. Give it a status that can be either available or committed. Create an instance and fill its roles.
We are about to take a major shift in our exploration of Loom. Thus far, we have exploited only one of Loom's sub-languages: the Modelling Language. This has provided a rich environment for defining a domain taxonomy and for describing specific instances of objects within that taxonomy. Loom's modelling language, with its powerful classification-based inference engine, is one feature that sets Loom apart from the pack of knowledge representation systems available. We are about to tackle a new collection of Loom features, known collectively as the Behavior Language.
Loom provides two paradigms for behavioral specification: object oriented programming, and rule based programming. This chapter will address object oriented programming features.
The basic data structure that a transportation planning expert deals with is called a TPFDD (pronounced "tip-fid"), for Time-Phased Force Deployment Data. This is a collection of records, each of which defines a single requirement to transport a unit of cargo or personnel. We can capture the most important characteristics of these data structures in the following definitions:
(defconcept tpfdd :constraints (and (exactly 1 name) (at-least 1 transportation-requirement))) (defrelation transportation-requirement :range tpfdd-record :characteristics :closed-world) (defconcept tpfdd-record) (defrelation cargo-type :range (:the-set medical-team engineering-team) :characteristics single-valued) (defrelation unit :range number :characteristics single-valued) (defrelation stons :range number :characteristics single-valued) (defrelation mtons :range number :characteristics single-valued) (defrelation point-of-disembarcation :range location :characteristics single-valued) (defrelation point-of-embarcation :range location :characteristics single-valued) (defrelation transportation-path-used :range transportation-path :characteristics single-valued) (defrelation destination :range location :characteristics single-valued) (defrelation ready-to-load-date :range number :characteristics single-valued) (defrelation earliest-arrival-date :range number :characteristics single-valued) (defrelation latest-arrival-date :range number :characteristics single-valued) (defrelation destination-date :range number :characteristics single-valued)The meaning of the roles of a TPFDD record will be described as they are addressed in our examples, here and in Chapter 5. Note that we chose to specify each relation to be single-valued, overriding the default of multiple-valued relations. The function set-features could be used to declare all relations to use closed-world semantics by: (set-features :closed-world-roles).
Let's create an instance of a TPFDD object:
> (tellm (create tf1 tpfdd) (name tf1 "Crisis Island TPFDD"))
The most basic mechanism for defining an action is simply to start creating methods. The action will then be built implicitly. Let's begin by building and executing a toy method:
> (defmethod build-tpfdd (?obj) :title "Build TPFDD if you are a TPFDD object." :situation (tpfdd ?obj) :response ((format t "~a is a TPFDD object.~%" ?obj))) |METHOD|BUILD-TPFDD-"Build TPFDD if you are a TPFDD object."Let's look at the components of this method definition. The first argument, build-tpfdd, is the operation (or, if you like, behavior or task) this method knows how to perform. More accurately, it is the name of the action that this method implements. It has an argument list, in the usual Lisp form, except that the ? mark on the variable is mandatory. In this example, we have not defined the action explicitly; the action is created implicitly with the build-tpfdd name. Any other methods with the same name and arguments will be included as methods for that action.
The :title keyword argument provides a way to name this method, apart from the operation it performs. This is important, since there may be many methods that perform the same operation, either in parallel, or in unique problem contexts. While the title is optional, its use is strongly recommended! When a method is defined, the title becomes part of its name in the knowledge base. If it is re-defined during debugging, the old instance will only be overwritten correctly if the titles, as well as the action names, match. In other words, it is best to use a title and never change it.
The :situation keyword provides an opportunity to restrict the firing of this method. This argument is optional, but if used will define conditions under which this method is an appropriate response to a task request. Note that, unlike most message passing systems, Loom methods are not attached to a specific object; they are really pattern-directed functions that operate on objects passed through the argument list.
Finally, the :response keyword supplies the functional body of the method. This contains a list of S-expressions that are evaluated within the scope of the bindings defined in the argument list.
> (perform (build-tpfdd (get-instance 'tf1))) |I|TF1 is a TPFDD object.Notice that the basic elements of the perform call look just like a function call: the generic function (i.e. action) name, followed by its arguments in a form that matches the argument list of the action's definition. Notice also that the method wants the instance object, rather than its name. This is provided by the get-instance function.
When this perform is executed, observe how the action is performed. Of course, this simple action is only for demonstration purposes. More complex examples will be demonstrated in later sections.
In Loom, we can specialize a generic action by providing multiple methods to perform the operation. Each method represents an alternative means of accomplishing the action's task. These methods are defined having the same name and argument list, but are distinguished by unique :title clauses. Their :situation clause specifies the context in which each method is applicable. Let's see how this works in practice. Let's define a new build-tpfdd method with a new title, with no :situation clause:
> (defmethod build-tpfdd (?obj) :title "This object is not necessarily a TPFDD." :response ((format t "Oops!!! Object ~a is not a TPFDD!!~%" ?obj)))This method will react to any invocation, without any situation. Experiment with invoking the two methods we have defined, and see when each fires:
Invoke the method using the TPFDD instance.
Invoke the method using one of your city instances.
As you will observe, these methods work quite nicely -- the appropriate method fires depending whether the object is or is not a TPFDD object. This may seem surprising, since the two situations are not disjoint: one essentially says "Fire if I am a TPFDD," while the other says "Fire all the time."
This example works because of the default behavior specified within the (implicitly defined) action. Remember, the generic function is represented by an action definition, in this case generated implicitly from our method definitions. One feature of an action is a filter function that chooses which method or methods to try to fire (yes, more than one can fire). The default filter selects the most specific methods to fire. In this example, the method with the "Build TPFDD if you are a TPFDD object." title is tried first, since its :situation clause is more specific than the competing method. In cases where a more specific method does not fire, the next most specific method is tried.
> (defaction build-tpfdd (?obj) :filters (:select-all))Experiment with the behavior of this new filter using our example methods. As you can see, it is possible for more than one method to fire under this filter. At this time, the filters supported are :most-specific, :select-one, :warning, :error, and :select-all, :overrides, and :last-one. They seem to cover the most useful cases. Other filters may be defined in the future if sufficient utility is identified.
Re-set the filter for our action to :most-specific.
> (defmethod build-tpfdd (?obj) :title "Construct TPFDD for high-priority cities." :situation (tpfdd ?obj) :response ((do-retrieve (?c) (priority-1-medical-need ?c) (format t "~a needs a tpfdd record.~%" ?c))))This action retrieves all the cities in the knowledge base classified as priority-1-medical-need.
Invoke build-tpfdd with tf1.
> (defmethod build-tpfdd (?obj) :title "Construct TPFDD for high-priority cities." :situation (tpfdd ?obj) :response ((do-retrieve (?c) (priority-1-medical-need ?c) (perform (build-record ?c ?obj)))))The embedded perform does not yet have a method defined. We want to define a method that creates a new instance, fills some of its roles, and then tells the TPFDD about the new record by adding a new role filler to it.
One way to do this is to build the entire method in one definition. The following example shows how this is done.
> (defmethod build-record (?need-obj ?tpfdd-obj) :title "Construct TPFDD for high-priority-cities." :situation (priority-1-medical-need ?need-obj) :response ((let ((?new-obj (createm nil 'tpfdd-record))) (format t "Creating record for ~a.~%" ?need-obj) (tellm (about ?new-obj (cargo-type medical-team) (destination ?need-obj))) (tellm (transportation-requirement ?tpfdd-obj ?new-obj)))))Test the new build-record action by performing build-tpfdd with tf1, and examine the records build-record has created.
Below is an equivalent means for defining our method. This definition illustrates how a method can invoke other methods using perform.
(defmethod build-record (?need-obj ?tpfdd-obj) :title "Create and add a TPFDD record to a TPFDD." :situation (priority-1-medical-need ?need-obj) :response ((let ((new-obj (create nil 'tpfdd-record))) (format t "Creating record for ~a.~%" ?need-obj) (set-value new-obj 'destination ?need-obj) (set-value new-obj 'cargo-type 'medical-team) (tellm) (perform (add-record ?tpfdd-obj new-obj)))))There are a few interesting things going on in this method. First, notice the create function call to create a new instance of type tpfdd-record. Then, notice the use of the built-in function set-value to assign a role-filler to an object. Finally, notice the reference to a new method which gives a more specific method to add a new TPFDD record to an existing TPFDD object:
(defmethod add-record (?tpfdd ?record) :title "Create and add a TPFDD record to a TPFDD." :situation (tpfdd ?tpfdd) :response ((format t " Adding ~a to ~a.~%" ?record ?tpfdd) (tellm (transportation-requirement ?tpfdd ?record))))Now, try the whole chain of methods:
> (perform (build-tpfdd (get-instance 'tf1)))Examine the resulting knowledge base entries using the pi function.
Loom provides powerful data-driven programming mechanisms that are closely integrated with all of the other representational facilities. In the previous chapter, we saw how Loom incorporates the object-oriented programming paradigm for behavioral programming. Now, we will see how data-driven programming is similarly incorporated in Loom through the use of defproduction and related constructs.
Figure 2: A More Detailed Look at Crisis Island
(defconcept transportation-path :is (and path (at-least 2 terminus) (exactly 1 name) (exactly 1 path-status))) (defrelation path-status :range (:one-of "normal" "degraded" "damaged" "unusable") :characteristics single-valued) (defconcept road :is (and transportation-path (at-least 1 land-crossed))) (defconcept sea-lane :is (and transportation-path (at-least 1 ocean-crossed))) (defconcept damaged-road :is (and road (the path-status (or "damaged" "unusable"))))Add the relations land-crossed and ocean-crossed with domain transportation-path and range string.
Add town instances c7 and c8 named "Santa Maria" and "Aqua Dolce" respectively
Now, lets represent the major roads on the island:
(tell (create r1 transportation-path) (about r1 (terminus c1)(terminus c7) (name "West Coastal Road") (land-crossed "Western Crisis Island"))) (tell (create r2 transportation-path) (about r2 (terminus c1)(terminus c8) (name "Aqua Dolce Road") (land-crossed "Western Crisis Island"))) (tell (create r3 transportation-path) (about r3 (terminus c7)(terminus c8) (name "Hill Road") (land-crossed "Western Crisis Island"))) (tell (create r4 transportation-path) (about r4 (terminus c8)(terminus c2) (name "Ridge Road West") (land-crossed "Central Crisis Island"))) (tellm (create r5 transportation-path) (about r5 (terminus c2)(terminus c3) (name "Ridge Road East") (land-crossed "Eastern Crisis Island"))) We now have a fairly good set of models describing the major components of our area of operation and the domain in general. Let's add a few concepts describing various types of reports and messages. (defconcept report) (defconcept message :is-primitive (and report (exactly 1 title) (at-most 1 sender) (at-least 1 recipient) (exactly 1 contents))) (defrelation title :range string) (defrelation sender :range string) (defrelation recipient :range string) (defrelation contents :range string)Okay! All of the above definitions should be familiar to you by now. Let's consider an example of the type of data-driven behavior we would like to represent. In general, when a transportation path has been degraded, the crisis planner needs to know about the change of status. We will write this rule as follows: when a road is known to be damaged or unusable, send a message to the "planning team".
> (defproduction P1-Report-Bad-Road :when (detects (damaged-road ?road)) :perform (send-status-message ?road "Planning Team" "is seriously damaged"))
(defaction send-status-message (?obj ?to ?contents) :filters (:most-specific)) (defmethod send-status-message (?obj ?to ?contents) :title "Send a Message about an Object" :response ((format t "To: ~a Re: ~a ~a.~%" ?to ?obj ?contents)))Give the road r1 a path-status of normal. The instance should look something like:
> (pi r1) (TELL (:ABOUT R1 ROAD (PATH-STATUS "normal") (LAND-CROSSED "Western Crisis Island") (NAME "West Coastal Road") (TERMINUS C1) (TERMINUS C7)))Check for the existence of any damaged roads using a retrieve.
ou should notice that the production has not yet fired because no damaged roads exist. Now tell the planner that the path-status for r1 has become "unusable".
> (tellm (about r1 (path-status "unusable"))) Rete match changes at agent time 38: > entry: |I|R1 |C|DAMAGED-ROAD To: Planning Team Re: |I|R1 is seriously damaged. 38We can modify the production in another important way: instead of executing immediately, we can :schedule a production's task for execution later according to a :priority which can be either :low or :high. Since sending this type of alert is not as critical as some other messages, we'll give it low priority, and the redefined production will look like this:
> (defproduction P2-Report-Bad-Road :when (damaged-road ?road) :schedule (send-status-message ?road "Planning Team" "is seriously damaged(2nd rule)") :priority :low)Tasks that are scheduled in this way will be executed at the end of the current match cycle after all other productions are fired.
Reset the status of r1 to normal.
Execute the following set of statements (or similar tells) and observe when the productions fire.
> (tell (about r1 (path-status "unusable"))) |I|R1 > (tell (about c7 (damage-estimate structural-damage))) |I|C7 > (tell (about r2 (path-status "normal"))) |I|R2 > (tell (about r3 (path-status "normal"))) |I|R3 > (tell (about r4 (path-status "degraded"))) |I|R4 > (tellm (about r5 (path-status "normal"))) Rete match changes at agent time 40: > entry: |I|R5 |C|TRANSPORTATION-PATH > entry: |I|R5 |C|ROAD > entry: |I|R4 |C|TRANSPORTATION-PATH > entry: |I|R4 |C|ROAD > entry: |I|R3 |C|TRANSPORTATION-PATH > entry: |I|R3 |C|ROAD > entry: |I|R2 |C|TRANSPORTATION-PATH > entry: |I|R2 |C|ROAD > entry: |I|R1 |C|DAMAGED-ROAD To: Planning Team Re: |I|R1 The road is seriously damaged. To: Planning Team Re: |I|R1 The road is seriously damaged(2nd rule). 40 > (retrieve ?bad-road (damaged-road ?bad-road)) (|I|R1)Notice that nothing happens until tellm is encountered. After Loom re-classifies the instances, productions are matched (see the lines marked "rete match changes"). The low priority rule fires last.